Skip to content

Commit

Permalink
fix(structure): live edit on draft documents (#7526)
Browse files Browse the repository at this point in the history
* !TEMP(dev): add liveEdit to simple block

* refactor(sanity): make livedit drafts readOnly

* refactor(sanity): add banner explaining what needs to be done to get the live edit to work

* refactor(sanity): add publish button to banner

* feat(structure): allow discard draft in banner

* test(e2e): add e2e test to check banner exists

* refactor(e2e): add telemetry

* refactor(structure): change names of telemetry events

* refactor(structure): update banner text

* refactor(structure): send in useDocumentPane info as props vs fetching them again

* refactor(structure): update telemetry names and event properties
  • Loading branch information
RitaDias authored Sep 23, 2024
1 parent 7c814a8 commit 7bf9995
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const myStringType = defineArrayMember({
})

export default defineType({
liveEdit: true,
name: 'simpleBlock',
title: 'Simple block',
type: 'document',
Expand Down
7 changes: 7 additions & 0 deletions packages/sanity/src/structure/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ const structureLocaleStrings = defineLocalesResources('structure', {
'banners.deleted-document-banner.text': 'This document has been deleted.',
/** The text content for the deprecated document type banner */
'banners.deprecated-document-type-banner.text': 'This document type has been deprecated.',
/** The text for publish action for discarding the version */
'banners.live-edit-draft-banner.discard.tooltip': 'Discard draft',
/** The text for publish action for the draft banner */
'banners.live-edit-draft-banner.publish.tooltip': 'Publish to continue editing',
/** The text content for the live edit document when it's a draft */
'banners.live-edit-draft-banner.text':
'The type <strong>{{schemaType}}</strong> has <code>liveEdit</code> enabled, but a draft version of this document exists. Publish or discard the draft in order to continue live editing it.',
/** The text for the permission check banner if the user only has one role, and it does not allow updating this document */
'banners.permission-check-banner.missing-permission_create_one':
'Your role <Roles/> does not have permissions to create this document.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,9 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
const createActionDisabled = isNonExistent && !isActionEnabled(schemaType!, 'create')
const reconnecting = connectionState === 'reconnecting'
const isLocked = editState.transactionSyncLock?.enabled
// in cases where the document has drafts but the schema is live edit,
// there is a risk of data loss, so we disable editing in this case
const isLiveEditAndDraft = Boolean(liveEdit && editState.draft)

return (
!ready ||
Expand All @@ -527,19 +530,22 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => {
reconnecting ||
isLocked ||
isDeleting ||
isDeleted
isDeleted ||
isLiveEditAndDraft
)
}, [
connectionState,
editState.transactionSyncLock,
isNonExistent,
isDeleted,
isDeleting,
isPermissionsLoading,
permissions?.granted,
schemaType,
isNonExistent,
connectionState,
editState.transactionSyncLock?.enabled,
editState.draft,
liveEdit,
ready,
revTime,
schemaType,
isDeleting,
isDeleted,
])

const formState = useFormState({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {ScrollContainer, useTimelineSelector, VirtualizerScrollInstanceProvider}
import {css, styled} from 'styled-components'

import {PaneContent, usePane, usePaneLayout} from '../../../components'
import {isLiveEditEnabled} from '../../../components/paneItem/helpers'
import {useStructureTool} from '../../../useStructureTool'
import {DocumentInspectorPanel} from '../documentInspector'
import {InspectDialog} from '../inspectDialog'
Expand All @@ -14,6 +15,7 @@ import {
PermissionCheckBanner,
ReferenceChangedBanner,
} from './banners'
import {DraftLiveEditBanner} from './banners/DraftLiveEditBanner'
import {FormView} from './documentViews'

interface DocumentPanelProps {
Expand Down Expand Up @@ -117,6 +119,8 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
(state) => state.lastNonDeletedRevId,
)

const isLiveEdit = isLiveEditEnabled(schemaType)

// Scroll to top as `documentId` changes
useEffect(() => {
if (!documentScrollElement?.scrollTo) return
Expand Down Expand Up @@ -150,6 +154,14 @@ export const DocumentPanel = function DocumentPanel(props: DocumentPanelProps) {
scrollElement={documentScrollElement}
containerElement={formContainerElement}
>
{activeView.type === 'form' && isLiveEdit && ready && (
<DraftLiveEditBanner
displayed={displayed}
documentId={documentId}
schemaType={schemaType}
/>
)}

{activeView.type === 'form' && !isPermissionsLoading && ready && (
<>
<PermissionCheckBanner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {type SanityDocument} from '@sanity/client'
import {ErrorOutlineIcon} from '@sanity/icons'
import {useTelemetry} from '@sanity/telemetry/react'
import {Flex, Text} from '@sanity/ui'
import {useCallback, useEffect, useState} from 'react'
import {
isDraftId,
type ObjectSchemaType,
Translate,
useDocumentOperation,
useTranslation,
} from 'sanity'

import {Button} from '../../../../../ui-components'
import {structureLocaleNamespace} from '../../../../i18n'
import {ResolvedLiveEdit} from './__telemetry__/DraftLiveEditBanner.telemetry'
import {Banner} from './Banner'

interface DraftLiveEditBannerProps {
displayed: Partial<SanityDocument> | null
documentId: string
schemaType: ObjectSchemaType
}

export function DraftLiveEditBanner({
displayed,
documentId,
schemaType,
}: DraftLiveEditBannerProps): JSX.Element | null {
const {t} = useTranslation(structureLocaleNamespace)
const [isPublishing, setPublishing] = useState(false)
const [isDiscarding, setDiscarding] = useState(false)
const telemetry = useTelemetry()

const {publish, discardChanges} = useDocumentOperation(documentId, displayed?._type || '')

const handlePublish = useCallback(() => {
publish.execute()
setPublishing(true)
telemetry.log(ResolvedLiveEdit, {liveEditResolveType: 'publish'})
}, [publish, telemetry])

const handleDiscard = useCallback(() => {
discardChanges.execute()
setDiscarding(true)
telemetry.log(ResolvedLiveEdit, {liveEditResolveType: 'discard'})
}, [discardChanges, telemetry])

useEffect(() => {
return () => {
setPublishing(false)
setDiscarding(false)
}
})

if (displayed && displayed._id && !isDraftId(displayed._id)) {
return null
}

return (
<Banner
content={
<Flex align="center" justify="space-between" gap={1}>
<Text size={1} weight="medium">
<Translate
t={t}
i18nKey={'banners.live-edit-draft-banner.text'}
values={{schemaType: schemaType.title}}
/>
</Text>
<Button
onClick={handlePublish}
text={t('action.publish.live-edit.label')}
tooltipProps={{content: t('banners.live-edit-draft-banner.publish.tooltip')}}
loading={isPublishing}
/>

<Button
onClick={handleDiscard}
text={t('banners.live-edit-draft-banner.discard.tooltip')}
tooltipProps={{content: t('banners.live-edit-draft-banner.discard.tooltip')}}
loading={isDiscarding}
/>
</Flex>
}
data-testid="live-edit-type-banner"
icon={ErrorOutlineIcon}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {defineEvent} from '@sanity/telemetry'

interface TypeInfo {
liveEditResolveType: 'publish' | 'discard'
}

/**
* When a draft in a live edit document is published
* @internal
*/
export const ResolvedLiveEdit = defineEvent<TypeInfo>({
name: 'Resolved LiveEdit Draft',
version: 1,
description: 'User resolved a draft of a live edit document to continue editing',
})
31 changes: 31 additions & 0 deletions test/e2e/tests/desk/liveEditDraft.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable max-nested-callbacks */
import {expect, test} from '@playwright/test'

import {createUniqueDocument, withDefaultClient} from '../../helpers'

withDefaultClient((context) => {
test.describe('sanity/structure: document pane', () => {
test('on live edit document with a draft, a banner should appear', async ({page}) => {
// create published document
const uniqueDoc = await createUniqueDocument(context.client, {_type: 'playlist'})
const id = uniqueDoc._id!

// create draft document
await createUniqueDocument(context.client, {
_type: 'playlist',
_id: `drafts.${id}`,
name: 'Edited by e2e test runner',
})

await page.goto(`/test/content/playlist;${id}`)

await expect(page.getByTestId('document-panel-scroller')).toBeAttached()
await expect(page.getByTestId('string-input')).toBeAttached()

// checks that inputs are set to read only
await expect(await page.getByTestId('string-input')).toHaveAttribute('readonly', '')
// checks that the banner is visible
await expect(page.getByTestId('live-edit-type-banner')).toBeVisible()
})
})
})

0 comments on commit 7bf9995

Please sign in to comment.