Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(structure): live edit on draft documents #7526

Merged
merged 11 commits into from
Sep 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const myStringType = defineArrayMember({
})

export default defineType({
liveEdit: true,
Copy link
Contributor Author

@RitaDias RitaDias Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temp (commit will be removed before merging)

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