@portabletext/plugin-sdk-value 3.0.22 → 3.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-sdk-value",
3
- "version": "3.0.22",
3
+ "version": "3.0.24",
4
4
  "description": "Synchronizes the Portable Text Editor value with the Sanity SDK, allowing for two-way editing.",
5
5
  "keywords": [
6
6
  "portabletext",
@@ -34,13 +34,12 @@
34
34
  "main": "./dist/index.js",
35
35
  "types": "./dist/index.d.ts",
36
36
  "files": [
37
- "src",
38
37
  "dist"
39
38
  ],
40
39
  "dependencies": {
41
40
  "@sanity/diff-patch": "^6.0.0",
42
41
  "@sanity/json-match": "^1.0.5",
43
- "react-compiler-runtime": "1.0.0"
42
+ "react-compiler-runtime": "^1.0.0"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@sanity/schema": "^4.20.3",
@@ -53,9 +52,9 @@
53
52
  "@vitejs/plugin-react": "^5.0.4",
54
53
  "@vitest/browser": "^4.0.14",
55
54
  "@vitest/browser-playwright": "^4.0.14",
56
- "babel-plugin-react-compiler": "1.0.0",
55
+ "babel-plugin-react-compiler": "^1.0.0",
57
56
  "eslint": "^9.39.1",
58
- "eslint-plugin-react-hooks": "7.0.1",
57
+ "eslint-plugin-react-hooks": "^7.0.1",
59
58
  "playwright": "^1.56.1",
60
59
  "react": "^19.2.1",
61
60
  "react-dom": "^19.2.1",
@@ -63,14 +62,14 @@
63
62
  "typescript": "5.9.3",
64
63
  "typescript-eslint": "^8.48.0",
65
64
  "vitest": "^4.0.14",
66
- "@portabletext/editor": "^3.3.2",
67
- "@portabletext/patches": "^2.0.0"
65
+ "@portabletext/editor": "^3.3.4",
66
+ "@portabletext/patches": "^2.0.1"
68
67
  },
69
68
  "peerDependencies": {
70
69
  "@sanity/sdk-react": "^2.1.2",
71
70
  "react": "^18.3 || ^19",
72
71
  "react-dom": "^18.3 || ^19",
73
- "@portabletext/editor": "^3.3.2"
72
+ "@portabletext/editor": "^3.3.4"
74
73
  },
75
74
  "engines": {
76
75
  "node": ">=20.19 <22 || >=22.12"
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './plugin.sdk-value'
@@ -1,183 +0,0 @@
1
- import {
2
- defineSchema,
3
- EditorProvider,
4
- PortableTextEditable,
5
- useEditor,
6
- type Editor,
7
- type PortableTextBlock,
8
- } from '@portabletext/editor'
9
- import {
10
- createDocumentHandle,
11
- getDocumentState,
12
- ResourceProvider,
13
- useEditDocument,
14
- type ResourceProviderProps,
15
- type SanityConfig,
16
- type SanityInstance,
17
- type StateSource,
18
- } from '@sanity/sdk-react'
19
- import {render, waitFor} from '@testing-library/react'
20
- import userEvent, {type UserEvent} from '@testing-library/user-event'
21
- import {useEffect} from 'react'
22
- import {ErrorBoundary} from 'react-error-boundary'
23
- import {
24
- afterEach,
25
- beforeEach,
26
- describe,
27
- expect,
28
- it,
29
- vi,
30
- type Mock,
31
- } from 'vitest'
32
- import {SDKValuePlugin} from './plugin.sdk-value'
33
-
34
- vi.mock('@sanity/sdk-react', async () => {
35
- const {createContext, createRef, Suspense, useContext} = await import('react')
36
- const Context = createContext<SanityConfig | null>(null)
37
- const eventTarget = new EventTarget()
38
- const valueRef = createRef<PortableTextBlock[]>()
39
-
40
- return {
41
- createDocumentHandle: (value: unknown) => value,
42
- useEditDocument: () => {
43
- if (!valueRef.current) {
44
- // suspend at least once
45
- throw new Promise((resolve) => {
46
- valueRef.current = []
47
- setTimeout(resolve, 0)
48
- })
49
- }
50
-
51
- return (value: PortableTextBlock[]) => {
52
- valueRef.current = value
53
- eventTarget.dispatchEvent(new CustomEvent('change'))
54
- }
55
- },
56
- getDocumentState: (): StateSource<PortableTextBlock[] | null> => {
57
- return {
58
- getCurrent: () => valueRef.current,
59
- subscribe: (fn) => {
60
- const listener = () => fn?.()
61
- eventTarget.addEventListener('change', listener)
62
- return () => eventTarget.removeEventListener('change', listener)
63
- },
64
- get observable(): StateSource<
65
- PortableTextBlock[] | null
66
- >['observable'] {
67
- throw new Error('Not implemented')
68
- },
69
- }
70
- },
71
- useSanityInstance: () => useContext(Context)!,
72
- ResourceProvider: ({
73
- children,
74
- fallback,
75
- ...config
76
- }: ResourceProviderProps) => (
77
- <Suspense fallback={fallback}>
78
- <Context.Provider value={config}>{children}</Context.Provider>
79
- </Suspense>
80
- ),
81
- }
82
- })
83
-
84
- const schemaDefinition = defineSchema({})
85
-
86
- describe(SDKValuePlugin.name, () => {
87
- let user: UserEvent
88
- let setSdkValue: (value: PortableTextBlock[] | null | undefined) => void
89
- let getSdkValue: () => PortableTextBlock[] | null | undefined
90
- let getEditorValue: () => PortableTextBlock[] | null | undefined
91
- let portableTextEditable: HTMLElement
92
- let unmount: () => void
93
- let errorHandler: Mock
94
-
95
- beforeEach(async () => {
96
- user = userEvent.setup()
97
- errorHandler = vi.fn()
98
-
99
- const testId = 'portable-text-editable'
100
- const {resolve: resolveEditor, promise: editorPromise} =
101
- Promise.withResolvers<Editor>()
102
-
103
- const doc = createDocumentHandle({
104
- documentId: 'example-document-id',
105
- documentType: 'example-document-type',
106
- })
107
- const path = 'example-portable-text-field'
108
-
109
- function CaptureEditorPlugin() {
110
- const editor = useEditor()
111
-
112
- useEffect(() => {
113
- resolveEditor(editor)
114
- }, [editor])
115
-
116
- return null
117
- }
118
-
119
- const result = render(
120
- <ErrorBoundary fallback={null} onError={errorHandler}>
121
- <ResourceProvider
122
- projectId="example-project"
123
- dataset="example-dataset"
124
- fallback={<>Loading…</>}
125
- >
126
- <EditorProvider initialConfig={{schemaDefinition}}>
127
- <SDKValuePlugin {...doc} path={path} />
128
- <CaptureEditorPlugin />
129
- <PortableTextEditable data-testid={testId} />
130
- </EditorProvider>
131
- </ResourceProvider>
132
- </ErrorBoundary>,
133
- )
134
- portableTextEditable = await waitFor(() => result.getByTestId(testId))
135
- unmount = result.unmount
136
-
137
- const editor = await editorPromise
138
- setSdkValue = useEditDocument<PortableTextBlock[] | null | undefined>({
139
- ...doc,
140
- path,
141
- })
142
-
143
- getSdkValue = getDocumentState(null as unknown as SanityInstance, {
144
- ...doc,
145
- path,
146
- }).getCurrent
147
-
148
- getEditorValue = () => editor.getSnapshot().context.value
149
- })
150
-
151
- afterEach(async () => {
152
- // wait one frame before unmounting in the event of errors
153
- await new Promise((resolve) => setTimeout(resolve, 0))
154
- expect(errorHandler).not.toHaveBeenCalled()
155
- unmount?.()
156
- })
157
-
158
- it('syncs editor changes to the SDK', async () => {
159
- await user.click(portableTextEditable)
160
- await user.type(portableTextEditable, 'Hello world!')
161
-
162
- expect(getSdkValue()).toEqual(getEditorValue())
163
- expect(getSdkValue()).toMatchObject([{children: [{text: 'Hello world!'}]}])
164
- })
165
-
166
- it('syncs SDK changes to the editor', () => {
167
- const testValue: PortableTextBlock[] = [
168
- {
169
- _type: 'block',
170
- _key: 'test-key',
171
- children: [
172
- {_type: 'span', _key: 'span-key', text: 'SDK content', marks: []},
173
- ],
174
- markDefs: [],
175
- style: 'normal',
176
- },
177
- ]
178
-
179
- setSdkValue(testValue)
180
- expect(getEditorValue()).toEqual(getSdkValue())
181
- expect(getEditorValue()).toEqual(testValue)
182
- })
183
- })
@@ -1,193 +0,0 @@
1
- import {
2
- useEditor,
3
- type PortableTextBlock,
4
- type Patch as PtePatch,
5
- } from '@portabletext/editor'
6
- import type {
7
- JSONValue,
8
- Path,
9
- PathSegment,
10
- InsertPatch as PteInsertPatch,
11
- } from '@portabletext/patches'
12
- import {diffValue, type SanityPatchOperations} from '@sanity/diff-patch'
13
- import {
14
- parsePath,
15
- type ExprNode,
16
- type PathNode,
17
- type SegmentNode,
18
- type ThisNode,
19
- } from '@sanity/json-match'
20
- import {
21
- getDocumentState,
22
- useEditDocument,
23
- useSanityInstance,
24
- type DocumentHandle,
25
- } from '@sanity/sdk-react'
26
- import {useEffect} from 'react'
27
-
28
- interface SDKValuePluginProps extends DocumentHandle {
29
- path: string
30
- }
31
-
32
- type InsertPatch = Required<Pick<SanityPatchOperations, 'insert'>>
33
-
34
- const ARRAYIFY_ERROR_MESSAGE =
35
- 'Unexpected path format from diffValue output. Please report this issue.'
36
-
37
- function* getSegments(
38
- node: PathNode,
39
- ): Generator<Exclude<SegmentNode, ThisNode>> {
40
- if (node.base) {
41
- yield* getSegments(node.base)
42
- }
43
- if (node.segment.type !== 'This') {
44
- yield node.segment
45
- }
46
- }
47
-
48
- function isKeyPath(node: ExprNode): node is PathNode {
49
- if (node.type !== 'Path') {
50
- return false
51
- }
52
- if (node.base) {
53
- return false
54
- }
55
- if (node.recursive) {
56
- return false
57
- }
58
- if (node.segment.type !== 'Identifier') {
59
- return false
60
- }
61
- return node.segment.name === '_key'
62
- }
63
-
64
- function arrayifyPath(pathExpr: string): Path {
65
- const node = parsePath(pathExpr)
66
- if (!node) {
67
- return []
68
- }
69
- if (node.type !== 'Path') {
70
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
71
- }
72
-
73
- return Array.from(getSegments(node)).map((segment): PathSegment => {
74
- if (segment.type === 'Identifier') {
75
- return segment.name
76
- }
77
- if (segment.type !== 'Subscript') {
78
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
79
- }
80
- if (segment.elements.length !== 1) {
81
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
82
- }
83
-
84
- const [element] = segment.elements
85
- if (element.type === 'Number') {
86
- return element.value
87
- }
88
-
89
- if (element.type !== 'Comparison') {
90
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
91
- }
92
- if (element.operator !== '==') {
93
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
94
- }
95
- const keyPathNode = [element.left, element.right].find(isKeyPath)
96
- if (!keyPathNode) {
97
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
98
- }
99
- const other = element.left === keyPathNode ? element.right : element.left
100
- if (other.type !== 'String') {
101
- throw new Error(ARRAYIFY_ERROR_MESSAGE)
102
- }
103
- return {_key: other.value}
104
- })
105
- }
106
-
107
- function convertPatches(patches: SanityPatchOperations[]): PtePatch[] {
108
- return patches.flatMap((p) => {
109
- return Object.entries(p).flatMap(([type, values]): PtePatch[] => {
110
- const origin = 'remote'
111
-
112
- switch (type) {
113
- case 'set':
114
- case 'setIfMissing':
115
- case 'diffMatchPatch':
116
- case 'inc':
117
- case 'dec': {
118
- return Object.entries(values).map(
119
- ([pathExpr, value]) =>
120
- ({type, value, origin, path: arrayifyPath(pathExpr)}) as PtePatch,
121
- )
122
- }
123
- case 'unset': {
124
- if (!Array.isArray(values)) {
125
- return []
126
- }
127
- return values.map(arrayifyPath).map((path) => ({type, origin, path}))
128
- }
129
- case 'insert': {
130
- const {items, ...rest} = values as InsertPatch['insert']
131
- type InsertPosition = PteInsertPatch['position']
132
- const position = Object.keys(rest).at(0) as InsertPosition | undefined
133
-
134
- if (!position) {
135
- return []
136
- }
137
- const pathExpr = (rest as {[K in InsertPosition]: string})[position]
138
- const insertPatch: PteInsertPatch = {
139
- type,
140
- origin,
141
- position,
142
- path: arrayifyPath(pathExpr),
143
- items: items as JSONValue[],
144
- }
145
-
146
- return [insertPatch]
147
- }
148
-
149
- default: {
150
- return []
151
- }
152
- }
153
- })
154
- })
155
- }
156
- /**
157
- * @public
158
- */
159
- export function SDKValuePlugin(props: SDKValuePluginProps) {
160
- // NOTE: the real `useEditDocument` suspends until the document is loaded into the SDK store
161
- const setSdkValue = useEditDocument(props)
162
- const instance = useSanityInstance(props)
163
- const editor = useEditor()
164
-
165
- useEffect(() => {
166
- const getEditorValue = () => editor.getSnapshot().context.value
167
- const {getCurrent: getSdkValue, subscribe: onSdkValueChange} =
168
- getDocumentState<PortableTextBlock[]>(instance, props)
169
-
170
- const editorSubscription = editor.on('patch', () =>
171
- setSdkValue(getEditorValue()),
172
- )
173
- const unsubscribeToEditorChanges = () => editorSubscription.unsubscribe()
174
- const unsubscribeToSdkChanges = onSdkValueChange(() => {
175
- const snapshot = getEditorValue()
176
- const patches = convertPatches(diffValue(snapshot, getSdkValue()))
177
-
178
- if (patches.length) {
179
- editor.send({type: 'patches', patches, snapshot})
180
- }
181
- })
182
-
183
- // update initial value
184
- editor.send({type: 'update value', value: getSdkValue() ?? []})
185
-
186
- return () => {
187
- unsubscribeToEditorChanges()
188
- unsubscribeToSdkChanges()
189
- }
190
- }, [editor, instance, props, setSdkValue])
191
-
192
- return null
193
- }