From 3c0d0871866f861532c69433d6de34e9eef7ba89 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:05:27 +0100 Subject: [PATCH] Dashboards: Add undo/redo actions for changing dashboard title (#106574) * Dashboards: Add undo/redo actions for changing dashboard title * Run make i18n-extract * Prevent empty undo/redo actions --- .../edit-pane/DashboardEditableElement.tsx | 38 ++++++++++++++++++- .../dashboard-scene/edit-pane/shared.ts | 23 ++++++++++- .../scene/new-toolbar/RightActions.tsx | 8 +++- public/locales/en-US/grafana.json | 3 ++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index d3df22e439c..7e392b04eb8 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo } from 'react'; +import { ReactNode, useMemo, useRef } from 'react'; import { Trans, t } from '@grafana/i18n'; import { Button, Input, TextArea } from '@grafana/ui'; @@ -7,9 +7,12 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; +import { redoButtonId, undoButtonID } from '../scene/new-toolbar/RightActions'; import { EditSchemaV2Button } from '../scene/new-toolbar/actions/EditSchemaV2Button'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; +import { dashboardEditActions } from './shared'; + export class DashboardEditableElement implements EditableDashboardElement { public readonly isEditableDashboardElement = true; @@ -79,7 +82,38 @@ export class DashboardEditableElement implements EditableDashboardElement { export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardScene; id?: string }) { const { title } = dashboard.useState(); - return dashboard.setState({ title: e.currentTarget.value })} />; + // We want to save the unchanged value for the 'undo' action + const valueBeforeEdit = useRef(''); + + return ( + { + dashboard.setState({ title: e.currentTarget.value }); + }} + onFocus={(e) => { + valueBeforeEdit.current = e.currentTarget.value; + }} + onBlur={(e) => { + // If the title input is currently focused and we click undo/redo + // we don't want to mess with the stack + const clickedUndoRedo = + e.relatedTarget && (e.relatedTarget.id === undoButtonID || e.relatedTarget.id === redoButtonId); + const titleUnchanged = valueBeforeEdit.current === e.currentTarget.value; + const shouldSkip = titleUnchanged || clickedUndoRedo; + if (shouldSkip) { + return; + } + + dashboardEditActions.changeTitle({ + source: dashboard, + oldTitle: valueBeforeEdit.current, + newTitle: e.currentTarget.value, + }); + }} + /> + ); } export function DashboardDescriptionInput({ dashboard, id }: { dashboard: DashboardScene; id?: string }) { diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 33ef498e402..a2bd2c94544 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -98,17 +98,23 @@ export interface RemoveElementActionHelperProps { undo: () => void; } +export interface ChangeTitleActionHelperProps { + oldTitle: string; + newTitle: string; + source: DashboardScene; +} + export const dashboardEditActions = { /** * Registers and peforms an edit action */ - edit: function (props: DashboardEditActionEventPayload) { + edit(props: DashboardEditActionEventPayload) { props.source.publishEvent(new DashboardEditActionEvent(props), true); }, /** * Helper for makeEdit that adds elements */ - addElement: function (props: AddElementActionHelperProps) { + addElement(props: AddElementActionHelperProps) { const { addedObject, source, perform, undo } = props; const element = getEditableElementFor(addedObject); @@ -145,4 +151,17 @@ export const dashboardEditActions = { undo, }); }, + + changeTitle({ source, oldTitle, newTitle }: ChangeTitleActionHelperProps) { + dashboardEditActions.edit({ + description: t('dashboard.title.action', 'Change dashboard title'), + source: source, + perform: () => { + source.setState({ title: newTitle }); + }, + undo: () => { + source.setState({ title: oldTitle }); + }, + }); + }, }; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx index 721c3396a28..76cb969eeaf 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx @@ -165,14 +165,16 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { ); }; +export const undoButtonID = 'undo-button'; function UndoButton({ dashboard }: ToolbarActionProps) { const editPane = dashboard.state.editPane; const { undoStack } = editPane.useState(); const undoAction = undoStack[undoStack.length - 1]; - const tooltip = undoAction ? `Undo '${undoAction.description}'` : 'Undo'; + const tooltip = `Undo${undoAction?.description ? ` '${undoAction.description}'` : ''}`; return ( editPane.undoAction()} @@ -181,14 +183,16 @@ function UndoButton({ dashboard }: ToolbarActionProps) { ); } +export const redoButtonId = 'redo-button'; function RedoButton({ dashboard }: ToolbarActionProps) { const editPane = dashboard.state.editPane; const { redoStack } = editPane.useState(); const redoAction = redoStack[redoStack.length - 1]; - const tooltip = redoAction ? `Redo '${redoAction?.description}'` : 'Redo'; + const tooltip = `Redo${redoAction?.description ? ` '${redoAction.description}'` : ''}`; return (