From 73c7247176b32e6b8e39e270808fafb5a8f71121 Mon Sep 17 00:00:00 2001 From: Rupert Dunk Date: Thu, 19 Dec 2024 11:08:26 +0000 Subject: [PATCH] feat(visual-editing): add svelte optimistic state support (#2307) * feat(visual-editing): add optimistic export path * feat(visual-editing): add svelte optimistic state support * chore(apps): use svelte optimistic state * chore: migrate useDatasetMutator hook * chore: fix types and merge conflict regressions * chore: handle deprecated exports --------- Co-authored-by: Cody Olsen --- apps/svelte/src/components/Shoe.svelte | 16 ++- packages/visual-editing/package.json | 9 ++ packages/visual-editing/src/index.ts | 100 +++++++++++++++--- .../context.ts | 2 +- .../visual-editing/src/optimistic/index.ts | 54 ++++++++++ .../state}/createSharedListener.ts | 2 +- .../state/datasetMutator.ts} | 12 ++- .../state/documentMutator.ts} | 10 -- .../visual-editing/src/optimistic/types.ts | 77 ++++++++++++++ .../components/UnionInsertMenuOverlay.tsx | 2 +- packages/visual-editing/src/react/index.ts | 40 ++++++- .../useDocuments.ts | 79 ++------------ .../useOptimistic.ts | 14 +-- .../useOptimisticActor.ts | 2 +- packages/visual-editing/src/ui/Overlays.tsx | 8 +- .../src/ui/context-menu/ContextMenu.tsx | 2 +- .../src/ui/context-menu/contextMenuItems.tsx | 2 +- .../src/ui/optimistic-state/index.ts | 11 -- .../visual-editing/src/ui/useController.tsx | 2 +- .../src/ui/useDatasetMutator.ts | 6 +- packages/visual-editing/src/util/mutations.ts | 2 +- .../visual-editing/src/util/useDragEvents.ts | 2 +- packages/visual-editing/svelte/index.ts | 7 +- .../svelte/optimistic/optimisticActor.ts | 15 +++ .../svelte/optimistic/useOptimistic.ts | 95 +++++++++++++++++ 25 files changed, 432 insertions(+), 139 deletions(-) rename packages/visual-editing/src/{ui/optimistic-state => optimistic}/context.ts (90%) create mode 100644 packages/visual-editing/src/optimistic/index.ts rename packages/visual-editing/src/{ui/optimistic-state/machines => optimistic/state}/createSharedListener.ts (96%) rename packages/visual-editing/src/{ui/optimistic-state/machines/datasetMutatorMachine.ts => optimistic/state/datasetMutator.ts} (92%) rename packages/visual-editing/src/{ui/comlink/index.ts => optimistic/state/documentMutator.ts} (83%) create mode 100644 packages/visual-editing/src/optimistic/types.ts rename packages/visual-editing/src/{ui/optimistic-state => react}/useDocuments.ts (76%) rename packages/visual-editing/src/{ui/optimistic-state => react}/useOptimistic.ts (92%) rename packages/visual-editing/src/{ui/optimistic-state => react}/useOptimisticActor.ts (95%) delete mode 100644 packages/visual-editing/src/ui/optimistic-state/index.ts create mode 100644 packages/visual-editing/svelte/optimistic/optimisticActor.ts create mode 100644 packages/visual-editing/svelte/optimistic/useOptimistic.ts diff --git a/apps/svelte/src/components/Shoe.svelte b/apps/svelte/src/components/Shoe.svelte index 130fed875..061091789 100644 --- a/apps/svelte/src/components/Shoe.svelte +++ b/apps/svelte/src/components/Shoe.svelte @@ -1,7 +1,9 @@ diff --git a/packages/visual-editing/package.json b/packages/visual-editing/package.json index b633e0993..a8af1853d 100644 --- a/packages/visual-editing/package.json +++ b/packages/visual-editing/package.json @@ -40,6 +40,12 @@ "require": "./dist/next-pages-router/index.cjs", "default": "./dist/next-pages-router/index.js" }, + "./optimistic": { + "source": "./src/optimistic/index.ts", + "import": "./dist/optimistic/index.js", + "require": "./dist/optimistic/index.cjs", + "default": "./dist/optimistic/index.js" + }, "./react": { "source": "./src/react/index.ts", "import": "./dist/react/index.js", @@ -82,6 +88,9 @@ "next-pages-router": [ "./dist/next-pages-router/index.d.ts" ], + "optimistic": [ + "./dist/optimistic/index.d.ts" + ], "react": [ "./dist/react/index.d.ts" ], diff --git a/packages/visual-editing/src/index.ts b/packages/visual-editing/src/index.ts index ad332cd26..7986c3699 100644 --- a/packages/visual-editing/src/index.ts +++ b/packages/visual-editing/src/index.ts @@ -1,3 +1,17 @@ +import type {DatasetMutatorMachineInput as DatasetMutatorMachineInputDeprecated} from './optimistic/state/datasetMutator' +import type { + DocumentsGet as DocumentsGetDeprecated, + DocumentsMutate as DocumentsMutateDeprecated, + OptimisticDocument as OptimisticDocumentDeprecated, + OptimisticDocumentPatches as OptimisticDocumentPatchesDeprecated, + OptimisticReducerAction as OptimisticReducerActionDeprecated, + OptimisticReducer as OptimisticReducerDeprecated, + Path as PathDeprecated, + PathValue as PathValueDeprecated, +} from './optimistic/types' +import {useDocuments as useDocumentsDeprecated} from './react/useDocuments' +import {useOptimistic as useOptimisticDeprecated} from './react/useOptimistic' + export {createOverlayController} from './controller' export type { DisableVisualEditing, @@ -53,19 +67,6 @@ export type { VisualEditingOptions, } from './types' export {enableVisualEditing} from './ui/enableVisualEditing' -export type {DatasetMutatorMachineInput} from './ui/optimistic-state/machines/datasetMutatorMachine' -export { - type DocumentsGet, - type DocumentsMutate, - type OptimisticDocument, - type OptimisticDocumentPatches, - type OptimisticReducer, - type OptimisticReducerAction, - type Path, - type PathValue, - useDocuments, - useOptimistic, -} from './ui/optimistic-state' export {useSharedState} from './ui/shared-state/useSharedState' export { type CreateDataAttribute, @@ -89,3 +90,76 @@ export { createDataAttribute, } from '@repo/visual-editing-helpers' export {getArrayItemKeyAndParentPath} from './util/mutations' + +/** + * @public + * @deprecated Use `import {useDocuments} from '@sanity/visual-editing/react'` instead + */ +export const useDocuments = useDocumentsDeprecated +/** + * @public + * @deprecated Use `import {useOptimistic} from '@sanity/visual-editing/react'` instead + */ +export const useOptimistic = useOptimisticDeprecated +/** + * @public + * @deprecated Use `import type {DatasetMutatorMachineInput} from '@sanity/visual-editing/optimistic'` instead + */ +export type DatasetMutatorMachineInput = DatasetMutatorMachineInputDeprecated +/** + * @public + * @deprecated Use `import type {DocumentsGet} from '@sanity/visual-editing/optimistic'` instead + */ +export type DocumentsGet = DocumentsGetDeprecated +/** + * @public + * @deprecated Use `import type {DocumentsMutate} from '@sanity/visual-editing/optimistic'` instead + */ +export type DocumentsMutate = DocumentsMutateDeprecated +/** + * @public + * @deprecated Use `import type {OptimisticDocument} from '@sanity/visual-editing/optimistic'` instead + */ +export type OptimisticDocument = OptimisticDocumentDeprecated +/** + * @public + * @deprecated Use `import type {OptimisticDocumentPatches} from '@sanity/visual-editing/optimistic'` instead + */ +export type OptimisticDocumentPatches = OptimisticDocumentPatchesDeprecated +/** + * @public + * @deprecated Use `import type {OptimisticReducer} from '@sanity/visual-editing/optimistic'` instead + */ +export type OptimisticReducer = OptimisticReducerDeprecated +/** + * @public + * @deprecated Use `import type {OptimisticReducerAction} from '@sanity/visual-editing/optimistic'` instead + */ +export type OptimisticReducerAction = OptimisticReducerActionDeprecated +/** + * @public + * @deprecated Use `import type {Path} from '@sanity/visual-editing/optimistic'` instead + */ +export type Path = PathDeprecated +/** + * @public + * @deprecated Use `import type {PathValue} from '@sanity/visual-editing/optimistic'` instead + */ +export type PathValue = PathValueDeprecated +/** + * @internal + * @deprecated - do not use + */ +export type { + useDocumentsDeprecated, + useOptimisticDeprecated, + DatasetMutatorMachineInputDeprecated, + DocumentsGetDeprecated, + DocumentsMutateDeprecated, + OptimisticDocumentDeprecated, + OptimisticDocumentPatchesDeprecated, + OptimisticReducerDeprecated, + OptimisticReducerActionDeprecated, + PathDeprecated, + PathValueDeprecated, +} diff --git a/packages/visual-editing/src/ui/optimistic-state/context.ts b/packages/visual-editing/src/optimistic/context.ts similarity index 90% rename from packages/visual-editing/src/ui/optimistic-state/context.ts rename to packages/visual-editing/src/optimistic/context.ts index 0dc4c09e0..c6b09c627 100644 --- a/packages/visual-editing/src/ui/optimistic-state/context.ts +++ b/packages/visual-editing/src/optimistic/context.ts @@ -1,5 +1,5 @@ import {createEmptyActor, type ActorRefFrom} from 'xstate' -import {createDatasetMutator} from '../comlink' +import {createDatasetMutator} from './state/datasetMutator' export type MutatorActor = ActorRefFrom> export type EmptyActor = typeof emptyActor diff --git a/packages/visual-editing/src/optimistic/index.ts b/packages/visual-editing/src/optimistic/index.ts new file mode 100644 index 000000000..f207226bd --- /dev/null +++ b/packages/visual-editing/src/optimistic/index.ts @@ -0,0 +1,54 @@ +export type {VisualEditingNode} from '../types' +export type { + DocumentSchema, + HistoryRefresh, + HistoryUpdate, + PreviewSnapshot, + ResolvedSchemaTypeMap, + SanityNode, + SanityStegaNode, + SchemaArrayItem, + SchemaArrayNode, + SchemaBooleanNode, + SchemaInlineNode, + SchemaNode, + SchemaNullNode, + SchemaNumberNode, + SchemaObjectField, + SchemaObjectNode, + SchemaStringNode, + SchemaType, + SchemaUnionNode, + SchemaUnionNodeOptions, + SchemaUnionOption, + SchemaUnknownNode, + Serializable, + SerializableArray, + SerializableObject, + SerializablePrimitive, + TypeSchema, + UnresolvedPath, + VisualEditingControllerMsg, + VisualEditingNodeMsg, +} from '@repo/visual-editing-helpers' +export type { + DocumentsGet, + DocumentsMutate, + OptimisticDocument, + OptimisticDocumentPatches, + OptimisticReducerAction, + Path, + PathValue, + OptimisticReducer, +} from './types' +export { + type MutatorActor, + type EmptyActor, + emptyActor, + actor, + listeners, + isEmptyActor, + setActor, +} from './context' +export {createDatasetMutator, type DatasetMutatorMachineInput} from './state/datasetMutator' +export {createDocumentMutator} from './state/documentMutator' diff --git a/packages/visual-editing/src/ui/optimistic-state/machines/createSharedListener.ts b/packages/visual-editing/src/optimistic/state/createSharedListener.ts similarity index 96% rename from packages/visual-editing/src/ui/optimistic-state/machines/createSharedListener.ts rename to packages/visual-editing/src/optimistic/state/createSharedListener.ts index aa396d14e..5efecfdfd 100644 --- a/packages/visual-editing/src/ui/optimistic-state/machines/createSharedListener.ts +++ b/packages/visual-editing/src/optimistic/state/createSharedListener.ts @@ -1,6 +1,6 @@ import {type ListenEvent} from '@sanity/client' import {merge, ReplaySubject, Subject, type Observable, type ObservedValueOf} from 'rxjs' -import type {VisualEditingNode} from '../../../types' +import type {VisualEditingNode} from '../../types' // eslint-disable-next-line @typescript-eslint/no-explicit-any type SharedListenEvent = ListenEvent> diff --git a/packages/visual-editing/src/ui/optimistic-state/machines/datasetMutatorMachine.ts b/packages/visual-editing/src/optimistic/state/datasetMutator.ts similarity index 92% rename from packages/visual-editing/src/ui/optimistic-state/machines/datasetMutatorMachine.ts rename to packages/visual-editing/src/optimistic/state/datasetMutator.ts index 8721874de..a05253586 100644 --- a/packages/visual-editing/src/ui/optimistic-state/machines/datasetMutatorMachine.ts +++ b/packages/visual-editing/src/optimistic/state/datasetMutator.ts @@ -11,7 +11,8 @@ import { type DocumentMutatorMachineParentEvent, } from '@sanity/mutate/_unstable_machine' import {assertEvent, assign, emit, setup, stopChild, type ActorRefFrom} from 'xstate' -import type {createDocumentMutator} from '../../comlink' +import type {VisualEditingNode} from '../../types' +import {createDocumentMutator} from './documentMutator' export interface DatasetMutatorMachineInput extends Omit { client: SanityClient @@ -118,3 +119,12 @@ export const datasetMutatorMachine = setup({ pristine: {}, }, }) + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const createDatasetMutator = (comlink: VisualEditingNode) => { + return datasetMutatorMachine.provide({ + actors: { + documentMutatorMachine: createDocumentMutator(comlink), + }, + }) +} diff --git a/packages/visual-editing/src/ui/comlink/index.ts b/packages/visual-editing/src/optimistic/state/documentMutator.ts similarity index 83% rename from packages/visual-editing/src/ui/comlink/index.ts rename to packages/visual-editing/src/optimistic/state/documentMutator.ts index 818216e44..998e799d7 100644 --- a/packages/visual-editing/src/ui/comlink/index.ts +++ b/packages/visual-editing/src/optimistic/state/documentMutator.ts @@ -6,7 +6,6 @@ import { } from '@sanity/mutate/_unstable_machine' import {enqueueActions, fromPromise} from 'xstate' import type {VisualEditingNode} from '../../types' -import {datasetMutatorMachine} from '../optimistic-state/machines/datasetMutatorMachine' // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const createDocumentMutator = (comlink: VisualEditingNode) => { @@ -56,12 +55,3 @@ export const createDocumentMutator = (comlink: VisualEditingNode) => { }, }) } - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const createDatasetMutator = (comlink: VisualEditingNode) => { - return datasetMutatorMachine.provide({ - actors: { - documentMutatorMachine: createDocumentMutator(comlink), - }, - }) -} diff --git a/packages/visual-editing/src/optimistic/types.ts b/packages/visual-editing/src/optimistic/types.ts new file mode 100644 index 000000000..47d21df0f --- /dev/null +++ b/packages/visual-editing/src/optimistic/types.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type {SanityDocument} from '@sanity/client' +import {type Mutation, type NodePatchList} from '@sanity/mutate' + +export type Path = K extends string + ? T[K] extends Record + ? `${K}.${Path}` | K + : K + : never + +export type PathValue = P extends `${infer K}.${infer Rest}` + ? K extends keyof T + ? PathValue + : never + : P extends keyof T + ? T[P] + : never + +export type DocumentsMutate = ( + documentId: string, + mutations: Mutation[], + options?: {commit?: boolean | {debounce: number}}, +) => void + +export type DocumentsGet = >( + documentId: string, +) => OptimisticDocument + +export type OptimisticDocumentPatches = Record> = + | ((context: { + draftId: string + publishedId: string + /** + * @deprecated - use `getSnapshot` instead + */ + snapshot: SanityDocument | undefined + getSnapshot: () => Promise | null> + }) => Promise | NodePatchList) + | NodePatchList + +export type OptimisticDocument = Record> = { + /** + * The document ID + */ + id: string + /** + * Commits any locally applied mutations to the remote document + */ + commit: () => void + /** + * @deprecated - use `getSnapshot` instead + */ + get: { + (): SanityDocument | undefined +

>(path: P): PathValue | undefined + } + /** + * Returns a promise that resolves to the current document snapshot + */ + getSnapshot: () => Promise | null> + /** + * Applies the given patches to the document + */ + patch: ( + patches: OptimisticDocumentPatches, + options?: {commit?: boolean | {debounce: number}}, + ) => void +} + +export type OptimisticReducerAction = { + document: T + id: string + originalId: string + type: 'appear' | 'mutate' | 'disappear' +} + +export type OptimisticReducer = (state: T, action: OptimisticReducerAction) => T diff --git a/packages/visual-editing/src/overlay-components/components/UnionInsertMenuOverlay.tsx b/packages/visual-editing/src/overlay-components/components/UnionInsertMenuOverlay.tsx index 4c157612d..73bde431a 100644 --- a/packages/visual-editing/src/overlay-components/components/UnionInsertMenuOverlay.tsx +++ b/packages/visual-editing/src/overlay-components/components/UnionInsertMenuOverlay.tsx @@ -11,8 +11,8 @@ import { type MouseEvent, } from 'react' import {styled} from 'styled-components' +import {useDocuments} from '../../react/useDocuments' import type {ElementNode, OverlayComponent} from '../../types' -import {useDocuments} from '../../ui/optimistic-state/useDocuments' import {getArrayInsertPatches} from '../../util/mutations' import {InsertMenuPopover} from './InsertMenu' diff --git a/packages/visual-editing/src/react/index.ts b/packages/visual-editing/src/react/index.ts index d6cf10b86..702a09edf 100644 --- a/packages/visual-editing/src/react/index.ts +++ b/packages/visual-editing/src/react/index.ts @@ -12,26 +12,58 @@ export type { OverlayElementParent, SanityNode, VisualEditingOptions, -} from '../types.ts' + VisualEditingNode, +} from '../types' export {VisualEditing} from '../ui/VisualEditing' export { createDataAttribute, type CreateDataAttribute, type CreateDataAttributeProps, type DocumentSchema, + type PreviewSnapshot, + type ResolvedSchemaTypeMap, + type SanityStegaNode, type SchemaArrayItem, - type SchemaNode, - type SchemaUnionOption, - type WithRequired, type SchemaArrayNode, type SchemaBooleanNode, type SchemaInlineNode, + type SchemaNode, type SchemaNullNode, type SchemaNumberNode, type SchemaObjectField, type SchemaObjectNode, type SchemaStringNode, + type SchemaType, type SchemaUnionNode, type SchemaUnionNodeOptions, + type SchemaUnionOption, type SchemaUnknownNode, + type Serializable, + type SerializableArray, + type SerializableObject, + type SerializablePrimitive, + type TypeSchema, + type UnresolvedPath, + type VisualEditingControllerMsg, + type VisualEditingNodeMsg, + type WithRequired, } from '@repo/visual-editing-helpers' +export {createDocumentMutator} from '../optimistic/state/documentMutator' +export { + createDatasetMutator, + type DatasetMutatorMachineInput, +} from '../optimistic/state/datasetMutator' +export {emptyActor, type MutatorActor, type EmptyActor} from '../optimistic/context' +export type { + DocumentsGet, + DocumentsMutate, + OptimisticDocument, + OptimisticDocumentPatches, + OptimisticReducer, + OptimisticReducerAction, + Path, + PathValue, +} from '../optimistic/types' +export {useOptimistic} from './useOptimistic' +export {useOptimisticActor} from './useOptimisticActor' +export {useDocuments} from './useDocuments' diff --git a/packages/visual-editing/src/ui/optimistic-state/useDocuments.ts b/packages/visual-editing/src/react/useDocuments.ts similarity index 76% rename from packages/visual-editing/src/ui/optimistic-state/useDocuments.ts rename to packages/visual-editing/src/react/useDocuments.ts index c12145218..485277684 100644 --- a/packages/visual-editing/src/ui/optimistic-state/useDocuments.ts +++ b/packages/visual-editing/src/react/useDocuments.ts @@ -1,78 +1,19 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type {SanityDocument} from '@sanity/client' -import {createIfNotExists, patch, type Mutation, type NodePatchList} from '@sanity/mutate' +import {createIfNotExists, patch} from '@sanity/mutate' import {get as getAtPath} from '@sanity/util/paths' import {useCallback} from 'react' -import {getDraftId, getPublishedId} from '../../util/documents' -import type {MutatorActor} from './context' -import {isEmptyActor} from './context' +import {isEmptyActor, type MutatorActor} from '../optimistic/context' +import type { + DocumentsGet, + DocumentsMutate, + OptimisticDocumentPatches, + Path, + PathValue, +} from '../optimistic/types' +import {getDraftId, getPublishedId} from '../util/documents' import {useOptimisticActor} from './useOptimisticActor' -export type Path = K extends string - ? T[K] extends Record - ? `${K}.${Path}` | K - : K - : never - -export type PathValue = P extends `${infer K}.${infer Rest}` - ? K extends keyof T - ? PathValue - : never - : P extends keyof T - ? T[P] - : never - -export type DocumentsMutate = ( - documentId: string, - mutations: Mutation[], - options?: {commit?: boolean | {debounce: number}}, -) => void - -export type DocumentsGet = >( - documentId: string, -) => OptimisticDocument - -export type OptimisticDocumentPatches = Record> = - | ((context: { - draftId: string - publishedId: string - /** - * @deprecated - use `getSnapshot` instead - */ - snapshot: SanityDocument | undefined - getSnapshot: () => Promise | null> - }) => Promise | NodePatchList) - | NodePatchList - -export type OptimisticDocument = Record> = { - /** - * The document ID - */ - id: string - /** - * Commits any locally applied mutations to the remote document - */ - commit: () => void - /** - * @deprecated - use `getSnapshot` instead - */ - get: { - (): SanityDocument | undefined -

>(path: P): PathValue | undefined - } - /** - * Returns a promise that resolves to the current document snapshot - */ - getSnapshot: () => Promise | null> - /** - * Applies the given patches to the document - */ - patch: ( - patches: OptimisticDocumentPatches, - options?: {commit?: boolean | {debounce: number}}, - ) => void -} - function debounce) => ReturnType>(fn: F, timeout: number): F { let timer: ReturnType return ((...args: Parameters) => { diff --git a/packages/visual-editing/src/ui/optimistic-state/useOptimistic.ts b/packages/visual-editing/src/react/useOptimistic.ts similarity index 92% rename from packages/visual-editing/src/ui/optimistic-state/useOptimistic.ts rename to packages/visual-editing/src/react/useOptimistic.ts index b863eba13..c51c48958 100644 --- a/packages/visual-editing/src/ui/optimistic-state/useOptimistic.ts +++ b/packages/visual-editing/src/react/useOptimistic.ts @@ -1,19 +1,11 @@ import type {SanityDocument} from '@sanity/types' import {startTransition, useEffect, useState} from 'react' import {useEffectEvent} from 'use-effect-event' -import {getPublishedId} from '../../util/documents' -import {isEmptyActor} from './context' +import {isEmptyActor} from '../optimistic/context' +import type {OptimisticReducer, OptimisticReducerAction} from '../optimistic/types' +import {getPublishedId} from '../util/documents' import {useOptimisticActor} from './useOptimisticActor' -export type OptimisticReducerAction = { - document: T - id: string - originalId: string - type: 'appear' | 'mutate' | 'disappear' -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type OptimisticReducer = (state: T, action: OptimisticReducerAction) => T - export function useOptimistic( passthrough: T, reducer: OptimisticReducer | Array>, diff --git a/packages/visual-editing/src/ui/optimistic-state/useOptimisticActor.ts b/packages/visual-editing/src/react/useOptimisticActor.ts similarity index 95% rename from packages/visual-editing/src/ui/optimistic-state/useOptimisticActor.ts rename to packages/visual-editing/src/react/useOptimisticActor.ts index 235d53b99..52112b41e 100644 --- a/packages/visual-editing/src/ui/optimistic-state/useOptimisticActor.ts +++ b/packages/visual-editing/src/react/useOptimisticActor.ts @@ -6,7 +6,7 @@ import { listeners, type EmptyActor, type MutatorActor, -} from './context' +} from '../optimistic/context' export function useOptimisticActor(): MutatorActor | EmptyActor { const subscribe = useCallback((listener: () => void) => { diff --git a/packages/visual-editing/src/ui/Overlays.tsx b/packages/visual-editing/src/ui/Overlays.tsx index 9b163b947..ae1062ea0 100644 --- a/packages/visual-editing/src/ui/Overlays.tsx +++ b/packages/visual-editing/src/ui/Overlays.tsx @@ -20,6 +20,7 @@ import { type FunctionComponent, } from 'react' import {styled} from 'styled-components' +import {useOptimisticActor, useOptimisticActorReady} from '../react/useOptimisticActor' import type { OverlayComponentResolver, OverlayEventHandler, @@ -27,11 +28,10 @@ import type { VisualEditingNode, } from '../types' import {getDraftId, getPublishedId} from '../util/documents' -import {sanityNodesExistInSameArray} from '../util/findSanityNodes.ts' +import {sanityNodesExistInSameArray} from '../util/findSanityNodes' import {useDragEndEvents} from '../util/useDragEvents' import {ContextMenu} from './context-menu/ContextMenu' import {ElementOverlay} from './ElementOverlay' -import {useOptimisticActor, useOptimisticActorReady} from './optimistic-state/useOptimisticActor' import {OverlayDragGroupRect} from './OverlayDragGroupRect' import {OverlayDragInsertMarker} from './OverlayDragInsertMarker' import {OverlayDragPreview} from './OverlayDragPreview' @@ -39,8 +39,8 @@ import {OverlayMinimapPrompt} from './OverlayMinimapPrompt' import {overlayStateReducer} from './overlayStateReducer' import {PreviewSnapshotsProvider} from './preview/PreviewSnapshotsProvider' import {SchemaProvider} from './schema/SchemaProvider' -import {SharedStateProvider} from './shared-state/SharedStateProvider.tsx' -import {sendTelemetry} from './telemetry/sendTelemetry.ts' +import {SharedStateProvider} from './shared-state/SharedStateProvider' +import {sendTelemetry} from './telemetry/sendTelemetry' import {useController} from './useController' import {usePerspectiveSync} from './usePerspectiveSync' import {useReportDocuments} from './useReportDocuments' diff --git a/packages/visual-editing/src/ui/context-menu/ContextMenu.tsx b/packages/visual-editing/src/ui/context-menu/ContextMenu.tsx index 62c1f3eab..b6106b97f 100644 --- a/packages/visual-editing/src/ui/context-menu/ContextMenu.tsx +++ b/packages/visual-editing/src/ui/context-menu/ContextMenu.tsx @@ -12,9 +12,9 @@ import { type PopoverMargins, } from '@sanity/ui' import {useCallback, useEffect, useMemo, useState, type FunctionComponent} from 'react' +import {useDocuments} from '../../react/useDocuments' import type {ContextMenuNode, ContextMenuProps} from '../../types' import {getNodeIcon} from '../../util/getNodeIcon' -import {useDocuments} from '../optimistic-state/useDocuments' import {PopoverPortal} from '../PopoverPortal' import {useSchema} from '../schema/useSchema' import {getContextMenuItems} from './contextMenuItems' diff --git a/packages/visual-editing/src/ui/context-menu/contextMenuItems.tsx b/packages/visual-editing/src/ui/context-menu/contextMenuItems.tsx index 76003de94..68f3ad4c0 100644 --- a/packages/visual-editing/src/ui/context-menu/contextMenuItems.tsx +++ b/packages/visual-editing/src/ui/context-menu/contextMenuItems.tsx @@ -19,6 +19,7 @@ import { import type {SchemaType} from '@sanity/types' import {MenuGroup} from '@sanity/ui' import {type FunctionComponent} from 'react' +import type {OptimisticDocument} from '../../optimistic' import {InsertMenu} from '../../overlay-components/components/InsertMenu' import type {ContextMenuNode, OverlayElementField, OverlayElementParent} from '../../types' import {getNodeIcon} from '../../util/getNodeIcon' @@ -28,7 +29,6 @@ import { getArrayMovePatches, getArrayRemovePatches, } from '../../util/mutations' -import type {OptimisticDocument} from '../optimistic-state/useDocuments' export function getArrayRemoveAction(node: SanityNode, doc: OptimisticDocument): () => void { if (!node.type) throw new Error('Node type is missing') diff --git a/packages/visual-editing/src/ui/optimistic-state/index.ts b/packages/visual-editing/src/ui/optimistic-state/index.ts deleted file mode 100644 index 223cc74c2..000000000 --- a/packages/visual-editing/src/ui/optimistic-state/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - useDocuments, - type DocumentsGet, - type DocumentsMutate, - type OptimisticDocument, - type OptimisticDocumentPatches, - type Path, - type PathValue, -} from './useDocuments' -export {useOptimistic, type OptimisticReducer, type OptimisticReducerAction} from './useOptimistic' -export {useOptimisticActor} from './useOptimisticActor' diff --git a/packages/visual-editing/src/ui/useController.tsx b/packages/visual-editing/src/ui/useController.tsx index 708628c2a..f35cd61a6 100644 --- a/packages/visual-editing/src/ui/useController.tsx +++ b/packages/visual-editing/src/ui/useController.tsx @@ -1,7 +1,7 @@ import {useEffect, useRef, type MutableRefObject} from 'react' import {createOverlayController} from '../controller' +import {useOptimisticActorReady} from '../react/useOptimisticActor' import type {OverlayController, OverlayEventHandler} from '../types' -import {useOptimisticActorReady} from './optimistic-state/useOptimisticActor' /** * Hook for using an overlay controller diff --git a/packages/visual-editing/src/ui/useDatasetMutator.ts b/packages/visual-editing/src/ui/useDatasetMutator.ts index 4bfce629b..b7945f4a1 100644 --- a/packages/visual-editing/src/ui/useDatasetMutator.ts +++ b/packages/visual-editing/src/ui/useDatasetMutator.ts @@ -1,9 +1,9 @@ import {useEffect, useState} from 'react' import {createActor} from 'xstate' +import {setActor, type MutatorActor} from '../optimistic/context' +import {createSharedListener} from '../optimistic/state/createSharedListener' +import {createDatasetMutator} from '../optimistic/state/datasetMutator' import type {VisualEditingNode} from '../types' -import {createDatasetMutator} from './comlink' -import {setActor, type MutatorActor} from './optimistic-state/context' -import {createSharedListener} from './optimistic-state/machines/createSharedListener' /** * Hook for maintaining a channel between overlays and the presentation tool diff --git a/packages/visual-editing/src/util/mutations.ts b/packages/visual-editing/src/util/mutations.ts index 1f1d182b7..d1e32bbe9 100644 --- a/packages/visual-editing/src/util/mutations.ts +++ b/packages/visual-editing/src/util/mutations.ts @@ -2,7 +2,7 @@ import type {SanityNode} from '@repo/visual-editing-helpers' import type {SanityDocument} from '@sanity/client' import {at, insert, truncate, type NodePatchList} from '@sanity/mutate' import {get} from '@sanity/util/paths' -import type {OptimisticDocument} from '../ui/optimistic-state' +import type {OptimisticDocument} from '../optimistic/types' import {randomKey} from './randomKey' export function getArrayItemKeyAndParentPath(pathOrNode: string | SanityNode): { diff --git a/packages/visual-editing/src/util/useDragEvents.ts b/packages/visual-editing/src/util/useDragEvents.ts index 007ad5036..8d81ee504 100644 --- a/packages/visual-editing/src/util/useDragEvents.ts +++ b/packages/visual-editing/src/util/useDragEvents.ts @@ -1,8 +1,8 @@ import {at, insert, remove} from '@sanity/mutate' import {get as getFromPath} from '@sanity/util/paths' import {useCallback, useEffect} from 'react' +import {useDocuments} from '../react/useDocuments' import type {DragEndEvent, DragInsertPosition} from '../types' -import {useDocuments} from '../ui/optimistic-state/useDocuments' import {getArrayItemKeyAndParentPath} from './mutations' // Finds the node that the drag end event was relative to, and the relative diff --git a/packages/visual-editing/svelte/index.ts b/packages/visual-editing/svelte/index.ts index 69e94dfa9..b686ee182 100644 --- a/packages/visual-editing/svelte/index.ts +++ b/packages/visual-editing/svelte/index.ts @@ -1,4 +1,5 @@ -export * from './hooks' -export * from './previewStore' -export * from './types' +export {handlePreview} from './hooks' +export {isPreviewing, setPreviewing} from './previewStore' +export type {VisualEditingProps, HandlePreviewOptions, VisualEditingLocals} from './types' export {default as VisualEditing} from './VisualEditing.svelte' +export {useOptimistic} from './optimistic/useOptimistic' diff --git a/packages/visual-editing/svelte/optimistic/optimisticActor.ts b/packages/visual-editing/svelte/optimistic/optimisticActor.ts new file mode 100644 index 000000000..12e3440de --- /dev/null +++ b/packages/visual-editing/svelte/optimistic/optimisticActor.ts @@ -0,0 +1,15 @@ +import {actor, listeners} from '@sanity/visual-editing/optimistic' +import {readable} from 'svelte/store' + +export const optimisticActor = readable(actor, (set) => { + const listener = () => { + set(actor) + } + + listeners.add(listener) + + return () => { + actor.stop() + listeners.delete(listener) + } +}) diff --git a/packages/visual-editing/svelte/optimistic/useOptimistic.ts b/packages/visual-editing/svelte/optimistic/useOptimistic.ts new file mode 100644 index 000000000..09c05014e --- /dev/null +++ b/packages/visual-editing/svelte/optimistic/useOptimistic.ts @@ -0,0 +1,95 @@ +import type {SanityDocument} from '@sanity/types' +import { + isEmptyActor, + type OptimisticReducer, + type OptimisticReducerAction, +} from '@sanity/visual-editing/optimistic' +import {onMount} from 'svelte' +import {derived, get, writable, type Readable} from 'svelte/store' +import {getPublishedId} from '../../src/util/documents' +import {optimisticActor} from './optimisticActor' + +export function useOptimistic( + initial: T, + reducer: OptimisticReducer | Array>, +): {value: Readable; update: (newPassthrough: T) => void} { + // The current passthrough state, either the initial value passed to the + // function call or set via update + const passthrough = writable(initial) + // The last action event that was received, if this is defined, we are in a + // "dirty" state + const lastEvent = writable | null>(null) + // The optimistic state that is returned if we are in a "dirty" state + const optimistic = writable(initial) + + const reduceStateFromAction = (action: OptimisticReducerAction, prevState: T) => { + const reducers = Array.isArray(reducer) ? reducer : [reducer] + return reducers.reduce( + (acc, reducer) => + reducer(acc, { + document: action.document, + id: getPublishedId(action.id), + originalId: action.id, + type: action.type, + }), + prevState, + ) + } + + let pristineTimeout: ReturnType + + onMount(() => + optimisticActor.subscribe((actor) => { + // If the actor hasn't been set yet, we don't need to subscribe to mutations + if (isEmptyActor(actor)) { + return + } + // When a rebased event is received, apply it to the current optimistic + // state, and update the last action to signal we are in a "dirty" state + actor.on('rebased.local', (event) => { + const action = { + document: event.document as U, + id: event.id, + originalId: getPublishedId(event.id), + type: 'mutate' as const, + } + optimistic.update((prev) => reduceStateFromAction(action, prev)) + lastEvent.set(action) + clearTimeout(pristineTimeout) + }) + + // If no rebased events were received in the 15 seconds after a pristine + // event, reset to a "pristine" state by removing the last event, and + // align the optimistic state with the passthrough state + actor.on('pristine', () => { + pristineTimeout = setTimeout(() => { + lastEvent.set(null) + optimistic.set(get(passthrough)) + }, 15000) + }) + }), + ) + + const updatePassthrough = (newPassthrough: T) => { + // If we are in a dirty state (i.e. have a last event to apply) when the + // passthrough is updated, apply it to the passthrough as optimistic state + const $lastEvent = get(lastEvent) + if ($lastEvent) { + optimistic.set(reduceStateFromAction($lastEvent, newPassthrough)) + } + // Also always update the passthrough state + passthrough.set(newPassthrough) + } + + // If we are in a "dirty" state (have an event to apply), return the + // optimistic state, otherwise we are in the "pristine" state, so return the + // passthrough state + const optimisticValue = derived( + [passthrough, optimistic, lastEvent], + ([$passthrough, $optimistic, $lastEvent]) => { + return $lastEvent ? $optimistic : $passthrough + }, + ) + + return {update: updatePassthrough, value: optimisticValue} +}