From 8b3a9303496dc6fbf1a4b47ae6e4dd244fb0fdf1 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 13 May 2024 15:55:13 +0100 Subject: [PATCH 1/6] fix(sanity): respect Studio configuration when rendering "restore" document action --- .../header/DocumentPanelHeader.tsx | 12 ++++++-- .../statusBar/DocumentStatusBarActions.tsx | 29 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx index c2e03672da1..411dff27530 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentPanelHeader.tsx @@ -17,6 +17,7 @@ import {isMenuNodeButton, isNotMenuNodeButton, resolveMenuNodes} from '../../../ import {type PaneMenuItem} from '../../../../types' import {useStructureTool} from '../../../../useStructureTool' import {ActionDialogWrapper, ActionMenuListItem} from '../../statusBar/ActionMenuButton' +import {isRestoreAction} from '../../statusBar/DocumentStatusBarActions' import {TimelineMenu} from '../../timeline' import {useDocumentPane} from '../../useDocumentPane' import {DocumentHeaderTabs} from './DocumentHeaderTabs' @@ -34,7 +35,7 @@ export const DocumentPanelHeader = memo( ) { const {menuItems} = _props const { - actions, + actions: allActions, editState, onMenuAction, onPaneClose, @@ -51,6 +52,13 @@ export const DocumentPanelHeader = memo( const {actions: fieldActions} = useFieldActions() const [referenceElement, setReferenceElement] = useState(null) + // The restore action has a dedicated place in the UI; it's only visible when the user is + // viewing a different document revision. It must be omitted from this collection. + const actions = useMemo( + () => (allActions ?? []).filter((action) => !isRestoreAction(action)), + [allActions], + ) + const menuNodes = useMemo( () => resolveMenuNodes({actionHandler: onMenuAction, fieldActions, menuItems, menuItemGroups}), @@ -135,7 +143,7 @@ export const DocumentPanelHeader = memo( ))} {editState && ( diff --git a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx index 9a4719591bc..c93ea83ff7f 100644 --- a/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx +++ b/packages/sanity/src/structure/panes/document/statusBar/DocumentStatusBarActions.tsx @@ -1,7 +1,11 @@ /* eslint-disable camelcase */ import {Flex, Hotkeys, LayerProvider, Stack, Text} from '@sanity/ui' import {memo, useMemo, useState} from 'react' -import {type DocumentActionDescription, useTimelineSelector} from 'sanity' +import { + type DocumentActionComponent, + type DocumentActionDescription, + useTimelineSelector, +} from 'sanity' import {Button, Tooltip} from '../../../../ui-components' import {RenderActionCollectionState} from '../../../components' @@ -75,13 +79,20 @@ function DocumentStatusBarActionsInner(props: DocumentStatusBarActionsInnerProps } export const DocumentStatusBarActions = memo(function DocumentStatusBarActions() { - const {actions, connectionState, documentId, editState} = useDocumentPane() + const {actions: allActions, connectionState, documentId, editState} = useDocumentPane() // const [isMenuOpen, setMenuOpen] = useState(false) // const handleMenuOpen = useCallback(() => setMenuOpen(true), []) // const handleMenuClose = useCallback(() => setMenuOpen(false), []) // const handleActionComplete = useCallback(() => setMenuOpen(false), []) - if (!actions || !editState) { + // The restore action has a dedicated place in the UI; it's only visible when the user is viewing + // a different document revision. It must be omitted from this collection. + const actions = useMemo( + () => (allActions ?? []).filter((action) => !isRestoreAction(action)), + [allActions], + ) + + if (actions.length === 0 || !editState) { return null } @@ -110,7 +121,7 @@ export const DocumentStatusBarActions = memo(function DocumentStatusBarActions() }) export const HistoryStatusBarActions = memo(function HistoryStatusBarActions() { - const {connectionState, editState, timelineStore} = useDocumentPane() + const {actions, connectionState, editState, timelineStore} = useDocumentPane() // Subscribe to external timeline state changes const revTime = useTimelineSelector(timelineStore, (state) => state.revTime) @@ -118,7 +129,9 @@ export const HistoryStatusBarActions = memo(function HistoryStatusBarActions() { const revision = revTime?.id || '' const disabled = (editState?.draft || editState?.published || {})._rev === revision const actionProps = useMemo(() => ({...(editState || {}), revision}), [editState, revision]) - const historyActions = useMemo(() => [HistoryRestoreAction], []) + + // If multiple `restore` actions are defined, ensure only the final one is used. + const historyActions = useMemo(() => (actions ?? []).filter(isRestoreAction).slice(-1), [actions]) return ( ) }) + +export function isRestoreAction( + action: DocumentActionComponent, +): action is DocumentActionComponent & {action: 'restore'} { + return action.action === HistoryRestoreAction.action +} From 5602e1ba8a0c06f9428363bdd24c305fd9d14a36 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 14 May 2024 14:19:57 +0100 Subject: [PATCH 2/6] feat(test-studio): add custom `restore` document action --- .../actions/TestCustomRestoreAction.tsx | 16 ++++++++++++++++ dev/test-studio/documentActions/index.ts | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 dev/test-studio/documentActions/actions/TestCustomRestoreAction.tsx diff --git a/dev/test-studio/documentActions/actions/TestCustomRestoreAction.tsx b/dev/test-studio/documentActions/actions/TestCustomRestoreAction.tsx new file mode 100644 index 00000000000..4d7c45185df --- /dev/null +++ b/dev/test-studio/documentActions/actions/TestCustomRestoreAction.tsx @@ -0,0 +1,16 @@ +import {RocketIcon} from '@sanity/icons' +import {type DocumentActionComponent} from 'sanity' + +export const TestCustomRestoreAction: ( + action: DocumentActionComponent, +) => DocumentActionComponent = (restoreAction) => { + const action: DocumentActionComponent = (props) => ({ + ...restoreAction(props), + label: 'Custom restore', + tone: 'positive', + icon: RocketIcon, + }) + + action.action = 'restore' + return action +} diff --git a/dev/test-studio/documentActions/index.ts b/dev/test-studio/documentActions/index.ts index 4aae79f625f..d81c4328958 100644 --- a/dev/test-studio/documentActions/index.ts +++ b/dev/test-studio/documentActions/index.ts @@ -2,6 +2,7 @@ import {type DocumentActionsResolver} from 'sanity' import {TestConfirmDialogAction} from './actions/TestConfirmDialogAction' import {TestCustomComponentAction} from './actions/TestCustomComponentAction' +import {TestCustomRestoreAction} from './actions/TestCustomRestoreAction' import {TestModalDialogAction} from './actions/TestModalDialogAction' import {TestPopoverDialogAction} from './actions/TestPopoverDialogAction' @@ -13,7 +14,13 @@ export const resolveDocumentActions: DocumentActionsResolver = (prev, {schemaTyp TestPopoverDialogAction, TestCustomComponentAction, ...prev, - ] + ].map((action) => { + if (action.action === 'restore') { + return TestCustomRestoreAction(action) + } + + return action + }) } return prev From 08aaf715c04da4d40d9dd206b482680332764931 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 20 May 2024 11:22:38 +0100 Subject: [PATCH 3/6] feat(test-studio): add `removeRestoreActionTest` debug type --- dev/test-studio/documentActions/index.ts | 4 ++++ dev/test-studio/schema/debug/removeRestoreAction.ts | 12 ++++++++++++ dev/test-studio/schema/index.ts | 2 ++ dev/test-studio/structure/constants.ts | 1 + 4 files changed, 19 insertions(+) create mode 100644 dev/test-studio/schema/debug/removeRestoreAction.ts diff --git a/dev/test-studio/documentActions/index.ts b/dev/test-studio/documentActions/index.ts index d81c4328958..8c2baad981b 100644 --- a/dev/test-studio/documentActions/index.ts +++ b/dev/test-studio/documentActions/index.ts @@ -23,5 +23,9 @@ export const resolveDocumentActions: DocumentActionsResolver = (prev, {schemaTyp }) } + if (schemaType === 'removeRestoreActionTest') { + return prev.filter(({action}) => action !== 'restore') + } + return prev } diff --git a/dev/test-studio/schema/debug/removeRestoreAction.ts b/dev/test-studio/schema/debug/removeRestoreAction.ts new file mode 100644 index 00000000000..f4e5735ef80 --- /dev/null +++ b/dev/test-studio/schema/debug/removeRestoreAction.ts @@ -0,0 +1,12 @@ +export default { + type: 'document', + name: 'removeRestoreActionTest', + title: 'Remove Restore Action', + fields: [ + { + type: 'string', + name: 'title', + title: 'Title', + }, + ], +} diff --git a/dev/test-studio/schema/index.ts b/dev/test-studio/schema/index.ts index a408b5eb47f..5f3cac519d7 100644 --- a/dev/test-studio/schema/index.ts +++ b/dev/test-studio/schema/index.ts @@ -62,6 +62,7 @@ import recursive from './debug/recursive' import recursiveArray from './debug/recursiveArray' import recursiveObjectTest, {recursiveObject} from './debug/recursiveObject' import recursivePopover from './debug/recursivePopover' +import removeRestoreAction from './debug/removeRestoreAction' import reservedFieldNames from './debug/reservedFieldNames' import review from './debug/review' import * as scrollBugTypes from './debug/scrollBug' @@ -197,6 +198,7 @@ export const schemaTypes = [ fieldActionsTest, fieldComponentsTest, fieldsets, + removeRestoreAction, fieldValidationInferReproSharedObject, fieldValidationInferReproDoc, diff --git a/dev/test-studio/structure/constants.ts b/dev/test-studio/structure/constants.ts index ef5a75c606f..c621db8999f 100644 --- a/dev/test-studio/structure/constants.ts +++ b/dev/test-studio/structure/constants.ts @@ -74,6 +74,7 @@ export const DEBUG_INPUT_TYPES = [ 'recursiveDocument', 'recursiveObjectTest', 'recursivePopoverTest', + 'removeRestoreActionTest', 'reservedKeywordsTest', 'scrollBug', 'select', From 6bc16a62265d71b9e9629c74ee86354cf938974d Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 20 May 2024 11:25:48 +0100 Subject: [PATCH 4/6] test(e2e): add test for custom `restore` document action --- .../tests/document-actions/restore.spec.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/e2e/tests/document-actions/restore.spec.ts b/test/e2e/tests/document-actions/restore.spec.ts index 82dac82ad88..2fa880fea69 100644 --- a/test/e2e/tests/document-actions/restore.spec.ts +++ b/test/e2e/tests/document-actions/restore.spec.ts @@ -44,3 +44,56 @@ test(`documents can be restored to an earlier revision`, async ({page, createDra await confirmButton.click() await expect(title).toHaveText(titleA) }) + +test(`respects overridden restore action`, async ({page, createDraftDocument}) => { + const titleA = 'Title A' + const titleB = 'Title B' + + const publishKeypress = () => page.locator('body').press('Control+Alt+p') + const documentStatus = page.getByTestId('pane-footer-document-status') + const restoreButton = page.getByTestId('action-Restore') + const customRestoreButton = page.getByRole('button').getByText('Custom restore') + const confirmButton = page.getByTestId('confirm-dialog-confirm-button') + const timelineMenuOpenButton = page.getByTestId('timeline-menu-open-button') + const timelineItemButton = page.getByTestId('timeline-item-button') + const previousRevisionButton = timelineItemButton.nth(2) + const titleInput = page.getByTestId('field-title').getByTestId('string-input') + + await createDraftDocument('/test/content/input-debug;documentActionsTest') + const title = page.getByTestId('document-panel-document-title') + await titleInput.fill(titleA) + + // Wait for the document to be published. + // + // Note: This is invoked using the publish keyboard shortcut, because the publish document action + // has been overridden for the `documentActionsTest` type, and is not visible without opening the + // document actions menu. + await page.waitForTimeout(1_000) + await publishKeypress() + await expect(documentStatus).toContainText('Published just now') + + // Change the title. + await titleInput.fill(titleB) + await expect(title).toHaveText(titleB) + + // Wait for the document to be published. + await page.waitForTimeout(1_000) + await publishKeypress() + await expect(documentStatus).toContainText('Published just now') + + // Pick the previous revision from the revision timeline. + await timelineMenuOpenButton.click() + await expect(previousRevisionButton).toBeVisible() + await previousRevisionButton.click({force: true}) + + await expect(titleInput).toHaveValue(titleA) + + // Ensure the custom restore button is rendered instead of the default restore button. + await expect(customRestoreButton).toBeVisible() + await expect(restoreButton).not.toBeVisible() + + // Ensure the custom restore action can invoke the system restore action. + await customRestoreButton.click() + await confirmButton.click() + await expect(title).toHaveText(titleA) +}) From 98a43531c7f85db6924bf66ad1aacd5a6778e92b Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 20 May 2024 11:29:17 +0100 Subject: [PATCH 5/6] test(e2e): add test for removed `restore` document action --- .../tests/document-actions/restore.spec.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/e2e/tests/document-actions/restore.spec.ts b/test/e2e/tests/document-actions/restore.spec.ts index 2fa880fea69..9a075602339 100644 --- a/test/e2e/tests/document-actions/restore.spec.ts +++ b/test/e2e/tests/document-actions/restore.spec.ts @@ -97,3 +97,44 @@ test(`respects overridden restore action`, async ({page, createDraftDocument}) = await confirmButton.click() await expect(title).toHaveText(titleA) }) + +test(`respects removed restore action`, async ({page, createDraftDocument}) => { + const titleA = 'Title A' + const titleB = 'Title B' + + const documentStatus = page.getByTestId('pane-footer-document-status') + const publishButton = page.getByTestId('action-Publish') + const restoreButton = page.getByTestId('action-Restore') + const timelineMenuOpenButton = page.getByTestId('timeline-menu-open-button') + const timelineItemButton = page.getByTestId('timeline-item-button') + const previousRevisionButton = timelineItemButton.nth(2) + const title = page.getByTestId('document-panel-document-title') + const titleInput = page.getByTestId('field-title').getByTestId('string-input') + + await createDraftDocument('/test/content/input-debug;removeRestoreActionTest') + await titleInput.fill(titleA) + + // Wait for the document to be published. + await page.waitForTimeout(1_000) + await publishButton.click() + await expect(documentStatus).toContainText('Published just now') + + // Change the title. + await titleInput.fill(titleB) + await expect(title).toHaveText(titleB) + + // Wait for the document to be published. + await page.waitForTimeout(1_000) + await publishButton.click() + await expect(documentStatus).toContainText('Published just now') + + // Pick the previous revision from the revision timeline. + await timelineMenuOpenButton.click() + await expect(previousRevisionButton).toBeVisible() + await previousRevisionButton.click({force: true}) + + await expect(titleInput).toHaveValue(titleA) + + // Ensure the restore button is not displayed. + await expect(restoreButton).not.toBeVisible() +}) From fb42772075fc8dd33b3a9478c696efe884f37a86 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 21 May 2024 09:38:10 +0100 Subject: [PATCH 6/6] test(e2e): ensure custom restore action does not appear in `DocumentStatusBarActions` menu --- test/e2e/tests/document-actions/restore.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/e2e/tests/document-actions/restore.spec.ts b/test/e2e/tests/document-actions/restore.spec.ts index 9a075602339..7950af0f5ee 100644 --- a/test/e2e/tests/document-actions/restore.spec.ts +++ b/test/e2e/tests/document-actions/restore.spec.ts @@ -138,3 +138,19 @@ test(`respects removed restore action`, async ({page, createDraftDocument}) => { // Ensure the restore button is not displayed. await expect(restoreButton).not.toBeVisible() }) + +test(`user defined restore actions should not appear in any other document action group UI`, async ({ + page, + createDraftDocument, +}) => { + const actionMenuButton = page.getByTestId('action-menu-button') + const customRestoreButton = page.getByTestId('action-Customrestore') + const paneContextMenu = page.locator('[data-ui="MenuButton__popover"]') + + await createDraftDocument('/test/content/input-debug;documentActionsTest') + + await actionMenuButton.click() + + await expect(paneContextMenu).toBeVisible() + await expect(customRestoreButton).not.toBeVisible() +})