@portabletext/plugin-sdk-value 3.0.23 → 3.0.25
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 +4 -5
- package/src/index.ts +0 -1
- package/src/plugin.sdk-value.test.tsx +0 -183
- package/src/plugin.sdk-value.tsx +0 -193
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/plugin-sdk-value",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.25",
|
|
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,7 +34,6 @@
|
|
|
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": {
|
|
@@ -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.
|
|
67
|
-
"@portabletext/patches": "^2.0.
|
|
65
|
+
"@portabletext/editor": "^3.3.5",
|
|
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.
|
|
72
|
+
"@portabletext/editor": "^3.3.5"
|
|
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
|
-
})
|
package/src/plugin.sdk-value.tsx
DELETED
|
@@ -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
|
-
}
|