diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index c3e04ba136f..93e5ec503d8 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -108,8 +108,8 @@ export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardSce dashboardEditActions.changeTitle({ source: dashboard, - oldTitle: valueBeforeEdit.current, - newTitle: e.currentTarget.value, + oldValue: valueBeforeEdit.current, + newValue: e.currentTarget.value, }); }} /> @@ -139,8 +139,8 @@ export function DashboardDescriptionInput({ dashboard, id }: { dashboard: Dashbo dashboardEditActions.changeDescription({ source: dashboard, - oldDescription: valueBeforeEdit.current, - newDescription: e.currentTarget.value, + oldValue: valueBeforeEdit.current, + newValue: e.currentTarget.value, }); }} /> diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index dc5b9553142..1171d9ef776 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -1,3 +1,4 @@ +/* eslint-disable @grafana/i18n/no-translation-top-level */ import { useSessionStorage } from 'react-use'; import { BusEventWithPayload } from '@grafana/data'; @@ -191,6 +192,15 @@ export const dashboardEditActions = { }); }, + changeTitle: makeEditAction({ + description: t('dashboard.title.action', 'Change dashboard title'), + prop: 'title', + }), + changeDescription: makeEditAction({ + description: t('dashboard.description.action', 'Change dashboard description'), + prop: 'description', + }), + addVariable({ source, addedObject }: AddVariableActionHelperProps) { const varsBeforeAddition = [...source.state.variables]; @@ -219,32 +229,22 @@ export const dashboardEditActions = { }, }); }, - - changeTitle({ source, oldTitle, newTitle }: ChangeTitleActionHelperProps) { - dashboardEditActions.edit({ - description: t('dashboard.title.action', 'Change dashboard title'), - source, - perform: () => { - source.setState({ title: newTitle }); - }, - undo: () => { - source.setState({ title: oldTitle }); - }, - }); - }, - - changeDescription({ source, oldDescription, newDescription }: ChangeDescriptionActionHelperProps) { - dashboardEditActions.edit({ - description: t('dashboard.description.action', 'Change dashboard description'), - source, - perform: () => { - source.setState({ description: newDescription }); - }, - undo: () => { - source.setState({ description: oldDescription }); - }, - }); - }, + changeVariableName: makeEditAction({ + description: t('dashboard.variable.name.action', 'Change variable name'), + prop: 'name', + }), + changeVariableLabel: makeEditAction({ + description: t('dashboard.variable.label.action', 'Change variable label'), + prop: 'label', + }), + changeVariableDescription: makeEditAction({ + description: t('dashboard.variable.description.action', 'Change variable description'), + prop: 'description', + }), + changeVariableHideValue: makeEditAction({ + description: t('dashboard.variable.hide.action', 'Change variable hide option'), + prop: 'hide', + }), moveElement(props: MoveElementActionHelperProps) { const { movedObject, source, perform, undo } = props; @@ -266,6 +266,35 @@ export const dashboardEditActions = { }, }; +interface MakeEditActionProps { + description: string; + prop: T; +} + +interface EditActionProps { + source: Source; + oldValue: Source['state'][T]; + newValue: Source['state'][T]; +} + +function makeEditAction({ + description, + prop, +}: MakeEditActionProps) { + return ({ source, oldValue, newValue }: EditActionProps) => { + dashboardEditActions.edit({ + description, + source, + perform: () => { + source.setState({ [prop]: newValue }); + }, + undo: () => { + source.setState({ [prop]: oldValue }); + }, + }); + }; +} + export function undoRedoWasClicked(e: React.FocusEvent) { return e.relatedTarget && (e.relatedTarget.id === undoButtonID || e.relatedTarget.id === redoButtonId); } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index fa72de1f36e..86450b91f92 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -1,4 +1,4 @@ -import { FormEvent, useMemo, useState } from 'react'; +import { FormEvent, useMemo, useRef, useState } from 'react'; import { VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -9,7 +9,7 @@ import { Input, TextArea, Button, Field, Box, Stack } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { dashboardEditActions } from '../../edit-pane/shared'; +import { dashboardEditActions, undoRedoWasClicked } from '../../edit-pane/shared'; import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils'; import { BulkActionElement } from '../../scene/types/BulkActionElement'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement'; @@ -123,36 +123,49 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable const { name } = variable.useState(); const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement }); const [nameError, setNameError] = useState(); - const [validName, setValidName] = useState(variable.state.name); const onChange = (e: FormEvent) => { const result = validateVariableName(variable, e.currentTarget.value); if (result.errorMessage !== nameError) { setNameError(result.errorMessage); - } else { - setValidName(variable.state.name); } variable.setState({ name: e.currentTarget.value }); }; - // Restore valid name if bluring while invalid - const onBlur = () => { - if (nameError) { - variable.setState({ name: validName }); - setNameError(undefined); - } - }; + const oldName = useRef(name); return ( { + oldName.current = name; + }} onChange={onChange} - required - onBlur={onBlur} + onBlur={(e) => { + const labelUnchanged = oldName.current === name; + const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + + if (nameError) { + setNameError(undefined); + variable.setState({ name: oldName.current }); + return; + } + + if (shouldSkip) { + return; + } + + dashboardEditActions.changeVariableName({ + source: variable, + oldValue: oldName.current, + newValue: name, + }); + }} data-testid={selectors.components.PanelEditor.ElementEditPane.variableNameInput} + required /> ); @@ -160,10 +173,29 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable function VariableLabelInput({ variable }: VariableInputProps) { const { label } = variable.useState(); + const oldLabel = useRef(label ?? ''); + return ( { + oldLabel.current = label ?? ''; + }} onChange={(e) => variable.setState({ label: e.currentTarget.value })} + onBlur={(e) => { + const labelUnchanged = oldLabel.current === e.currentTarget.value; + const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + + if (shouldSkip) { + return; + } + + dashboardEditActions.changeVariableLabel({ + source: variable, + oldValue: oldLabel.current, + newValue: e.currentTarget.value, + }); + }} data-testid={selectors.components.PanelEditor.ElementEditPane.variableLabelInput} /> ); @@ -171,13 +203,31 @@ function VariableLabelInput({ variable }: VariableInputProps) { function VariableDescriptionTextArea({ variable }: VariableInputProps) { const { description } = variable.useState(); + const oldDescription = useRef(description ?? ''); return (