Skip to content

Commit

Permalink
fix(sanity): respect Studio configuration when rendering "restore" do…
Browse files Browse the repository at this point in the history
…cument action (#6637)

* fix(sanity): respect Studio configuration when rendering "restore" document action

* feat(test-studio): add custom `restore` document action

* feat(test-studio): add `removeRestoreActionTest` debug type

* test(e2e): add test for custom `restore` document action

* test(e2e): add test for removed `restore` document action

* test(e2e): ensure custom restore action does not appear in `DocumentStatusBarActions` menu
  • Loading branch information
juice49 authored May 23, 2024
1 parent 7b90cad commit 6ba71f2
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 12 additions & 1 deletion dev/test-studio/documentActions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -13,7 +14,17 @@ export const resolveDocumentActions: DocumentActionsResolver = (prev, {schemaTyp
TestPopoverDialogAction,
TestCustomComponentAction,
...prev,
]
].map((action) => {
if (action.action === 'restore') {
return TestCustomRestoreAction(action)
}

return action
})
}

if (schemaType === 'removeRestoreActionTest') {
return prev.filter(({action}) => action !== 'restore')
}

return prev
Expand Down
12 changes: 12 additions & 0 deletions dev/test-studio/schema/debug/removeRestoreAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
type: 'document',
name: 'removeRestoreActionTest',
title: 'Remove Restore Action',
fields: [
{
type: 'string',
name: 'title',
title: 'Title',
},
],
}
2 changes: 2 additions & 0 deletions dev/test-studio/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -197,6 +198,7 @@ export const schemaTypes = [
fieldActionsTest,
fieldComponentsTest,
fieldsets,
removeRestoreAction,

fieldValidationInferReproSharedObject,
fieldValidationInferReproDoc,
Expand Down
1 change: 1 addition & 0 deletions dev/test-studio/structure/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const DEBUG_INPUT_TYPES = [
'recursiveDocument',
'recursiveObjectTest',
'recursivePopoverTest',
'removeRestoreActionTest',
'reservedKeywordsTest',
'scrollBug',
'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -34,7 +35,7 @@ export const DocumentPanelHeader = memo(
) {
const {menuItems} = _props
const {
actions,
actions: allActions,
editState,
onMenuAction,
onPaneClose,
Expand All @@ -51,6 +52,13 @@ export const DocumentPanelHeader = memo(
const {actions: fieldActions} = useFieldActions()
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(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}),
Expand Down Expand Up @@ -135,7 +143,7 @@ export const DocumentPanelHeader = memo(
))}
{editState && (
<RenderActionCollectionState
actions={actions || []}
actions={actions}
actionProps={editState}
group="paneActions"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -110,15 +121,17 @@ 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)

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 (
<RenderActionCollectionState
Expand All @@ -136,3 +149,9 @@ export const HistoryStatusBarActions = memo(function HistoryStatusBarActions() {
</RenderActionCollectionState>
)
})

export function isRestoreAction(
action: DocumentActionComponent,
): action is DocumentActionComponent & {action: 'restore'} {
return action.action === HistoryRestoreAction.action
}
110 changes: 110 additions & 0 deletions test/e2e/tests/document-actions/restore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,113 @@ 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)
})

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()
})

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()
})

0 comments on commit 6ba71f2

Please sign in to comment.