@portabletext/plugin-sdk-value 1.0.0
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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
- package/src/index.ts +1 -0
- package/src/plugin.sdk-value.test.tsx +183 -0
- package/src/plugin.sdk-value.tsx +157 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 - 2025 Sanity.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SDK Value Plugin
|
|
2
|
+
|
|
3
|
+
> 🔗 Connects a Portable Text Editor with a Sanity document using the SDK
|
|
4
|
+
|
|
5
|
+
The SDK Value plugin provides seamless two-way synchronization between a Portable Text Editor instance and a specific field in a Sanity document. This enables real-time collaboration and ensures that changes made through the editor are immediately reflected in the document, and vice versa.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Two-way synchronization**: Changes in the editor update the document, and document changes update the editor
|
|
10
|
+
- **Real-time updates**: Automatically handles patches from external sources (other users, mutations, etc.)
|
|
11
|
+
- **Optimistic updates**: Provides smooth user experience with immediate local updates
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Import the `SDKValuePlugin` React component and place it inside the `EditorProvider`. The plugin requires document handle properties and a path to specify which field to synchronize:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
defineSchema,
|
|
20
|
+
EditorProvider,
|
|
21
|
+
PortableTextEditable,
|
|
22
|
+
} from '@portabletext/editor'
|
|
23
|
+
import {SDKValuePlugin} from '@portabletext/plugin-sdk-value'
|
|
24
|
+
|
|
25
|
+
function MyEditor() {
|
|
26
|
+
return (
|
|
27
|
+
<EditorProvider initialConfig={{schemaDefinition: defineSchema({})}}>
|
|
28
|
+
<PortableTextEditable />
|
|
29
|
+
<SDKValuePlugin
|
|
30
|
+
documentId="my-document-id"
|
|
31
|
+
documentType="myDocumentType"
|
|
32
|
+
path="content"
|
|
33
|
+
/>
|
|
34
|
+
</EditorProvider>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
The `SDKValuePlugin` component accepts a [Document Handle](https://www.sanity.io/docs/app-sdk/document-handles) plus an additional `path` parameter:
|
|
42
|
+
|
|
43
|
+
| Prop | Type | Description |
|
|
44
|
+
| -------------- | ------------------- | ------------------------------------------------------------------ |
|
|
45
|
+
| `documentId` | `string` | The document ID |
|
|
46
|
+
| `documentType` | `string` | The document type |
|
|
47
|
+
| `path` | `string` | [JSONMatch][json-match] path expression to the Portable Text field |
|
|
48
|
+
| `dataset` | `string` (optional) | Dataset name (if different from configured default) |
|
|
49
|
+
| `projectId` | `string` (optional) | Project ID (if different from configured default) |
|
|
50
|
+
|
|
51
|
+
[json-match]: https://www.sanity.io/docs/content-lake/json-match
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
This plugin requires:
|
|
56
|
+
|
|
57
|
+
- `@sanity/sdk-react` for document state management
|
|
58
|
+
- The document must exist in the Sanity dataset
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var compilerRuntime = require("react/compiler-runtime"), editor = require("@portabletext/editor"), diffPatch = require("@sanity/diff-patch"), jsonMatch = require("@sanity/json-match"), sdkReact = require("@sanity/sdk-react"), react = require("react");
|
|
4
|
+
const ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
|
|
5
|
+
function* getSegments(node) {
|
|
6
|
+
node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
|
|
7
|
+
}
|
|
8
|
+
function isKeyPath(node) {
|
|
9
|
+
return node.type !== "Path" || node.base || node.recursive || node.segment.type !== "Identifier" ? !1 : node.segment.name === "_key";
|
|
10
|
+
}
|
|
11
|
+
function arrayifyPath(pathExpr) {
|
|
12
|
+
const node = jsonMatch.parsePath(pathExpr);
|
|
13
|
+
if (!node) return [];
|
|
14
|
+
if (node.type !== "Path") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
15
|
+
return Array.from(getSegments(node)).map((segment) => {
|
|
16
|
+
if (segment.type === "Identifier") return segment.name;
|
|
17
|
+
if (segment.type !== "Subscript") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
18
|
+
if (segment.elements.length !== 1) throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
19
|
+
const [element] = segment.elements;
|
|
20
|
+
if (element.type === "Number") return element.value;
|
|
21
|
+
if (element.type !== "Comparison") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
22
|
+
if (element.operator !== "==") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
23
|
+
const keyPathNode = [element.left, element.right].find(isKeyPath);
|
|
24
|
+
if (!keyPathNode) throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
25
|
+
const other = element.left === keyPathNode ? element.right : element.left;
|
|
26
|
+
if (other.type !== "String") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
27
|
+
return {
|
|
28
|
+
_key: other.value
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function convertPatches(patches) {
|
|
33
|
+
return patches.flatMap((p) => Object.entries(p).flatMap(([type, values]) => {
|
|
34
|
+
const origin = "remote";
|
|
35
|
+
switch (type) {
|
|
36
|
+
case "set":
|
|
37
|
+
case "setIfMissing":
|
|
38
|
+
case "diffMatchPatch":
|
|
39
|
+
case "inc":
|
|
40
|
+
case "dec":
|
|
41
|
+
return Object.entries(values).map(([pathExpr, value]) => ({
|
|
42
|
+
type,
|
|
43
|
+
value,
|
|
44
|
+
origin,
|
|
45
|
+
path: arrayifyPath(pathExpr)
|
|
46
|
+
}));
|
|
47
|
+
case "unset":
|
|
48
|
+
return Array.isArray(values) ? values.map(arrayifyPath).map((path) => ({
|
|
49
|
+
type,
|
|
50
|
+
origin,
|
|
51
|
+
path
|
|
52
|
+
})) : [];
|
|
53
|
+
case "insert": {
|
|
54
|
+
const {
|
|
55
|
+
items,
|
|
56
|
+
...rest
|
|
57
|
+
} = values, position = Object.keys(rest).at(0);
|
|
58
|
+
if (!position) return [];
|
|
59
|
+
const pathExpr = rest[position];
|
|
60
|
+
return [{
|
|
61
|
+
type,
|
|
62
|
+
origin,
|
|
63
|
+
position,
|
|
64
|
+
path: arrayifyPath(pathExpr),
|
|
65
|
+
items
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
function SDKValuePlugin(props) {
|
|
74
|
+
const $ = compilerRuntime.c(6), setSdkValue = sdkReact.useEditDocument(props), instance = sdkReact.useSanityInstance(props), editor$1 = editor.useEditor();
|
|
75
|
+
let t0, t1;
|
|
76
|
+
return $[0] !== editor$1 || $[1] !== instance || $[2] !== props || $[3] !== setSdkValue ? (t0 = () => {
|
|
77
|
+
const getEditorValue = () => editor$1.getSnapshot().context.value, {
|
|
78
|
+
getCurrent: getSdkValue,
|
|
79
|
+
subscribe: onSdkValueChange
|
|
80
|
+
} = sdkReact.getDocumentState(instance, props), editorSubscription = editor$1.on("patch", () => setSdkValue(getEditorValue())), unsubscribeToEditorChanges = () => editorSubscription.unsubscribe(), unsubscribeToSdkChanges = onSdkValueChange(() => {
|
|
81
|
+
const snapshot = getEditorValue(), patches = convertPatches(diffPatch.diffValue(snapshot, getSdkValue()));
|
|
82
|
+
patches.length && editor$1.send({
|
|
83
|
+
type: "patches",
|
|
84
|
+
patches,
|
|
85
|
+
snapshot
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
return editor$1.send({
|
|
89
|
+
type: "update value",
|
|
90
|
+
value: getSdkValue() ?? []
|
|
91
|
+
}), () => {
|
|
92
|
+
unsubscribeToEditorChanges(), unsubscribeToSdkChanges();
|
|
93
|
+
};
|
|
94
|
+
}, t1 = [setSdkValue, editor$1, instance, props], $[0] = editor$1, $[1] = instance, $[2] = props, $[3] = setSdkValue, $[4] = t0, $[5] = t1) : (t0 = $[4], t1 = $[5]), react.useEffect(t0, t1), null;
|
|
95
|
+
}
|
|
96
|
+
exports.SDKValuePlugin = SDKValuePlugin;
|
|
97
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/plugin.sdk-value.tsx"],"sourcesContent":["import {\n useEditor,\n type PortableTextBlock,\n type Patch as PtePatch,\n} from '@portabletext/editor'\nimport type {\n JSONValue,\n Path,\n PathSegment,\n InsertPatch as PteInsertPatch,\n} from '@portabletext/patches'\nimport {diffValue, type SanityPatchOperations} from '@sanity/diff-patch'\nimport {\n parsePath,\n type ExprNode,\n type PathNode,\n type SegmentNode,\n type ThisNode,\n} from '@sanity/json-match'\nimport {\n getDocumentState,\n useEditDocument,\n useSanityInstance,\n type DocumentHandle,\n} from '@sanity/sdk-react'\nimport {useEffect} from 'react'\n\ninterface SDKValuePluginProps extends DocumentHandle {\n path: string\n}\n\ntype InsertPatch = Required<Pick<SanityPatchOperations, 'insert'>>\n\nconst ARRAYIFY_ERROR_MESSAGE =\n 'Unexpected path format from diffValue output. Please report this issue.'\n\nfunction* getSegments(\n node: PathNode,\n): Generator<Exclude<SegmentNode, ThisNode>> {\n if (node.base) yield* getSegments(node.base)\n if (node.segment.type !== 'This') yield node.segment\n}\n\nfunction isKeyPath(node: ExprNode): node is PathNode {\n if (node.type !== 'Path') return false\n if (node.base) return false\n if (node.recursive) return false\n if (node.segment.type !== 'Identifier') return false\n return node.segment.name === '_key'\n}\n\nfunction arrayifyPath(pathExpr: string): Path {\n const node = parsePath(pathExpr)\n if (!node) return []\n if (node.type !== 'Path') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n\n return Array.from(getSegments(node)).map((segment): PathSegment => {\n if (segment.type === 'Identifier') return segment.name\n if (segment.type !== 'Subscript') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n if (segment.elements.length !== 1) throw new Error(ARRAYIFY_ERROR_MESSAGE)\n\n const [element] = segment.elements\n if (element.type === 'Number') return element.value\n\n if (element.type !== 'Comparison') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n if (element.operator !== '==') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n const keyPathNode = [element.left, element.right].find(isKeyPath)\n if (!keyPathNode) throw new Error(ARRAYIFY_ERROR_MESSAGE)\n const other = element.left === keyPathNode ? element.right : element.left\n if (other.type !== 'String') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n return {_key: other.value}\n })\n}\n\nfunction convertPatches(patches: SanityPatchOperations[]): PtePatch[] {\n return patches.flatMap((p) => {\n return Object.entries(p).flatMap(([type, values]): PtePatch[] => {\n const origin = 'remote'\n\n switch (type) {\n case 'set':\n case 'setIfMissing':\n case 'diffMatchPatch':\n case 'inc':\n case 'dec': {\n return Object.entries(values).map(\n ([pathExpr, value]) =>\n ({type, value, origin, path: arrayifyPath(pathExpr)}) as PtePatch,\n )\n }\n case 'unset': {\n if (!Array.isArray(values)) return []\n return values.map(arrayifyPath).map((path) => ({type, origin, path}))\n }\n case 'insert': {\n const {items, ...rest} = values as InsertPatch['insert']\n type InsertPosition = PteInsertPatch['position']\n const position = Object.keys(rest).at(0) as InsertPosition | undefined\n\n if (!position) return []\n const pathExpr = (rest as {[K in InsertPosition]: string})[position]\n const insertPatch: PteInsertPatch = {\n type,\n origin,\n position,\n path: arrayifyPath(pathExpr),\n items: items as JSONValue[],\n }\n\n return [insertPatch]\n }\n\n default: {\n return []\n }\n }\n })\n })\n}\n/**\n * @public\n */\nexport function SDKValuePlugin(props: SDKValuePluginProps) {\n // NOTE: the real `useEditDocument` suspends until the document is loaded into the SDK store\n const setSdkValue = useEditDocument(props)\n const instance = useSanityInstance(props)\n const editor = useEditor()\n\n useEffect(() => {\n const getEditorValue = () => editor.getSnapshot().context.value\n const {getCurrent: getSdkValue, subscribe: onSdkValueChange} =\n getDocumentState<PortableTextBlock[]>(instance, props)\n\n const editorSubscription = editor.on('patch', () =>\n setSdkValue(getEditorValue()),\n )\n const unsubscribeToEditorChanges = () => editorSubscription.unsubscribe()\n const unsubscribeToSdkChanges = onSdkValueChange(() => {\n const snapshot = getEditorValue()\n const patches = convertPatches(diffValue(snapshot, getSdkValue()))\n\n if (patches.length) {\n editor.send({type: 'patches', patches, snapshot})\n }\n })\n\n // update initial value\n editor.send({type: 'update value', value: getSdkValue() ?? []})\n\n return () => {\n unsubscribeToEditorChanges()\n unsubscribeToSdkChanges()\n }\n }, [setSdkValue, editor, instance, props])\n\n return null\n}\n"],"names":["ARRAYIFY_ERROR_MESSAGE","getSegments","node","base","segment","type","isKeyPath","recursive","name","arrayifyPath","pathExpr","parsePath","Error","Array","from","map","elements","length","element","value","operator","keyPathNode","left","right","find","other","_key","convertPatches","patches","flatMap","p","Object","entries","values","origin","path","isArray","items","rest","position","keys","at","SDKValuePlugin","props","$","_c","setSdkValue","useEditDocument","instance","useSanityInstance","editor","useEditor","t0","t1","getEditorValue","getSnapshot","context","getCurrent","getSdkValue","subscribe","onSdkValueChange","getDocumentState","editorSubscription","on","unsubscribeToEditorChanges","unsubscribe","unsubscribeToSdkChanges","snapshot","diffValue","send","useEffect"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { c } from "react/compiler-runtime";
|
|
2
|
+
import { useEditor } from "@portabletext/editor";
|
|
3
|
+
import { diffValue } from "@sanity/diff-patch";
|
|
4
|
+
import { parsePath } from "@sanity/json-match";
|
|
5
|
+
import { useEditDocument, useSanityInstance, getDocumentState } from "@sanity/sdk-react";
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
const ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
|
|
8
|
+
function* getSegments(node) {
|
|
9
|
+
node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
|
|
10
|
+
}
|
|
11
|
+
function isKeyPath(node) {
|
|
12
|
+
return node.type !== "Path" || node.base || node.recursive || node.segment.type !== "Identifier" ? !1 : node.segment.name === "_key";
|
|
13
|
+
}
|
|
14
|
+
function arrayifyPath(pathExpr) {
|
|
15
|
+
const node = parsePath(pathExpr);
|
|
16
|
+
if (!node) return [];
|
|
17
|
+
if (node.type !== "Path") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
18
|
+
return Array.from(getSegments(node)).map((segment) => {
|
|
19
|
+
if (segment.type === "Identifier") return segment.name;
|
|
20
|
+
if (segment.type !== "Subscript") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
21
|
+
if (segment.elements.length !== 1) throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
22
|
+
const [element] = segment.elements;
|
|
23
|
+
if (element.type === "Number") return element.value;
|
|
24
|
+
if (element.type !== "Comparison") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
25
|
+
if (element.operator !== "==") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
26
|
+
const keyPathNode = [element.left, element.right].find(isKeyPath);
|
|
27
|
+
if (!keyPathNode) throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
28
|
+
const other = element.left === keyPathNode ? element.right : element.left;
|
|
29
|
+
if (other.type !== "String") throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
30
|
+
return {
|
|
31
|
+
_key: other.value
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function convertPatches(patches) {
|
|
36
|
+
return patches.flatMap((p) => Object.entries(p).flatMap(([type, values]) => {
|
|
37
|
+
const origin = "remote";
|
|
38
|
+
switch (type) {
|
|
39
|
+
case "set":
|
|
40
|
+
case "setIfMissing":
|
|
41
|
+
case "diffMatchPatch":
|
|
42
|
+
case "inc":
|
|
43
|
+
case "dec":
|
|
44
|
+
return Object.entries(values).map(([pathExpr, value]) => ({
|
|
45
|
+
type,
|
|
46
|
+
value,
|
|
47
|
+
origin,
|
|
48
|
+
path: arrayifyPath(pathExpr)
|
|
49
|
+
}));
|
|
50
|
+
case "unset":
|
|
51
|
+
return Array.isArray(values) ? values.map(arrayifyPath).map((path) => ({
|
|
52
|
+
type,
|
|
53
|
+
origin,
|
|
54
|
+
path
|
|
55
|
+
})) : [];
|
|
56
|
+
case "insert": {
|
|
57
|
+
const {
|
|
58
|
+
items,
|
|
59
|
+
...rest
|
|
60
|
+
} = values, position = Object.keys(rest).at(0);
|
|
61
|
+
if (!position) return [];
|
|
62
|
+
const pathExpr = rest[position];
|
|
63
|
+
return [{
|
|
64
|
+
type,
|
|
65
|
+
origin,
|
|
66
|
+
position,
|
|
67
|
+
path: arrayifyPath(pathExpr),
|
|
68
|
+
items
|
|
69
|
+
}];
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function SDKValuePlugin(props) {
|
|
77
|
+
const $ = c(6), setSdkValue = useEditDocument(props), instance = useSanityInstance(props), editor = useEditor();
|
|
78
|
+
let t0, t1;
|
|
79
|
+
return $[0] !== editor || $[1] !== instance || $[2] !== props || $[3] !== setSdkValue ? (t0 = () => {
|
|
80
|
+
const getEditorValue = () => editor.getSnapshot().context.value, {
|
|
81
|
+
getCurrent: getSdkValue,
|
|
82
|
+
subscribe: onSdkValueChange
|
|
83
|
+
} = getDocumentState(instance, props), editorSubscription = editor.on("patch", () => setSdkValue(getEditorValue())), unsubscribeToEditorChanges = () => editorSubscription.unsubscribe(), unsubscribeToSdkChanges = onSdkValueChange(() => {
|
|
84
|
+
const snapshot = getEditorValue(), patches = convertPatches(diffValue(snapshot, getSdkValue()));
|
|
85
|
+
patches.length && editor.send({
|
|
86
|
+
type: "patches",
|
|
87
|
+
patches,
|
|
88
|
+
snapshot
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return editor.send({
|
|
92
|
+
type: "update value",
|
|
93
|
+
value: getSdkValue() ?? []
|
|
94
|
+
}), () => {
|
|
95
|
+
unsubscribeToEditorChanges(), unsubscribeToSdkChanges();
|
|
96
|
+
};
|
|
97
|
+
}, t1 = [setSdkValue, editor, instance, props], $[0] = editor, $[1] = instance, $[2] = props, $[3] = setSdkValue, $[4] = t0, $[5] = t1) : (t0 = $[4], t1 = $[5]), useEffect(t0, t1), null;
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
SDKValuePlugin
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/plugin.sdk-value.tsx"],"sourcesContent":["import {\n useEditor,\n type PortableTextBlock,\n type Patch as PtePatch,\n} from '@portabletext/editor'\nimport type {\n JSONValue,\n Path,\n PathSegment,\n InsertPatch as PteInsertPatch,\n} from '@portabletext/patches'\nimport {diffValue, type SanityPatchOperations} from '@sanity/diff-patch'\nimport {\n parsePath,\n type ExprNode,\n type PathNode,\n type SegmentNode,\n type ThisNode,\n} from '@sanity/json-match'\nimport {\n getDocumentState,\n useEditDocument,\n useSanityInstance,\n type DocumentHandle,\n} from '@sanity/sdk-react'\nimport {useEffect} from 'react'\n\ninterface SDKValuePluginProps extends DocumentHandle {\n path: string\n}\n\ntype InsertPatch = Required<Pick<SanityPatchOperations, 'insert'>>\n\nconst ARRAYIFY_ERROR_MESSAGE =\n 'Unexpected path format from diffValue output. Please report this issue.'\n\nfunction* getSegments(\n node: PathNode,\n): Generator<Exclude<SegmentNode, ThisNode>> {\n if (node.base) yield* getSegments(node.base)\n if (node.segment.type !== 'This') yield node.segment\n}\n\nfunction isKeyPath(node: ExprNode): node is PathNode {\n if (node.type !== 'Path') return false\n if (node.base) return false\n if (node.recursive) return false\n if (node.segment.type !== 'Identifier') return false\n return node.segment.name === '_key'\n}\n\nfunction arrayifyPath(pathExpr: string): Path {\n const node = parsePath(pathExpr)\n if (!node) return []\n if (node.type !== 'Path') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n\n return Array.from(getSegments(node)).map((segment): PathSegment => {\n if (segment.type === 'Identifier') return segment.name\n if (segment.type !== 'Subscript') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n if (segment.elements.length !== 1) throw new Error(ARRAYIFY_ERROR_MESSAGE)\n\n const [element] = segment.elements\n if (element.type === 'Number') return element.value\n\n if (element.type !== 'Comparison') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n if (element.operator !== '==') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n const keyPathNode = [element.left, element.right].find(isKeyPath)\n if (!keyPathNode) throw new Error(ARRAYIFY_ERROR_MESSAGE)\n const other = element.left === keyPathNode ? element.right : element.left\n if (other.type !== 'String') throw new Error(ARRAYIFY_ERROR_MESSAGE)\n return {_key: other.value}\n })\n}\n\nfunction convertPatches(patches: SanityPatchOperations[]): PtePatch[] {\n return patches.flatMap((p) => {\n return Object.entries(p).flatMap(([type, values]): PtePatch[] => {\n const origin = 'remote'\n\n switch (type) {\n case 'set':\n case 'setIfMissing':\n case 'diffMatchPatch':\n case 'inc':\n case 'dec': {\n return Object.entries(values).map(\n ([pathExpr, value]) =>\n ({type, value, origin, path: arrayifyPath(pathExpr)}) as PtePatch,\n )\n }\n case 'unset': {\n if (!Array.isArray(values)) return []\n return values.map(arrayifyPath).map((path) => ({type, origin, path}))\n }\n case 'insert': {\n const {items, ...rest} = values as InsertPatch['insert']\n type InsertPosition = PteInsertPatch['position']\n const position = Object.keys(rest).at(0) as InsertPosition | undefined\n\n if (!position) return []\n const pathExpr = (rest as {[K in InsertPosition]: string})[position]\n const insertPatch: PteInsertPatch = {\n type,\n origin,\n position,\n path: arrayifyPath(pathExpr),\n items: items as JSONValue[],\n }\n\n return [insertPatch]\n }\n\n default: {\n return []\n }\n }\n })\n })\n}\n/**\n * @public\n */\nexport function SDKValuePlugin(props: SDKValuePluginProps) {\n // NOTE: the real `useEditDocument` suspends until the document is loaded into the SDK store\n const setSdkValue = useEditDocument(props)\n const instance = useSanityInstance(props)\n const editor = useEditor()\n\n useEffect(() => {\n const getEditorValue = () => editor.getSnapshot().context.value\n const {getCurrent: getSdkValue, subscribe: onSdkValueChange} =\n getDocumentState<PortableTextBlock[]>(instance, props)\n\n const editorSubscription = editor.on('patch', () =>\n setSdkValue(getEditorValue()),\n )\n const unsubscribeToEditorChanges = () => editorSubscription.unsubscribe()\n const unsubscribeToSdkChanges = onSdkValueChange(() => {\n const snapshot = getEditorValue()\n const patches = convertPatches(diffValue(snapshot, getSdkValue()))\n\n if (patches.length) {\n editor.send({type: 'patches', patches, snapshot})\n }\n })\n\n // update initial value\n editor.send({type: 'update value', value: getSdkValue() ?? []})\n\n return () => {\n unsubscribeToEditorChanges()\n unsubscribeToSdkChanges()\n }\n }, [setSdkValue, editor, instance, props])\n\n return null\n}\n"],"names":["ARRAYIFY_ERROR_MESSAGE","getSegments","node","base","segment","type","isKeyPath","recursive","name","arrayifyPath","pathExpr","parsePath","Error","Array","from","map","elements","length","element","value","operator","keyPathNode","left","right","find","other","_key","convertPatches","patches","flatMap","p","Object","entries","values","origin","path","isArray","items","rest","position","keys","at","SDKValuePlugin","props","$","_c","setSdkValue","useEditDocument","instance","useSanityInstance","editor","useEditor","t0","t1","getEditorValue","getSnapshot","context","getCurrent","getSdkValue","subscribe","onSdkValueChange","getDocumentState","editorSubscription","on","unsubscribeToEditorChanges","unsubscribe","unsubscribeToSdkChanges","snapshot","diffValue","send","useEffect"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portabletext/plugin-sdk-value",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Synchronizes the Portable Text Editor value with the Sanity SDK, allowing for two-way editing.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"portabletext",
|
|
7
|
+
"plugin",
|
|
8
|
+
"sdk",
|
|
9
|
+
"sync",
|
|
10
|
+
"value",
|
|
11
|
+
"@sanity/sdk",
|
|
12
|
+
"@sanity/sdk-react"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://portabletext.org",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/portabletext/plugins/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/portabletext/plugins.git",
|
|
21
|
+
"directory": "plugins/sdk-value"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"source": "./src/index.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/index.cjs",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.cjs",
|
|
37
|
+
"module": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@portabletext/editor": "^1.55.12",
|
|
45
|
+
"@portabletext/patches": "^1.1.0",
|
|
46
|
+
"@sanity/schema": "^3.96.0",
|
|
47
|
+
"@sanity/sdk-react": "^2.1.0",
|
|
48
|
+
"@sanity/types": "^3.96.0",
|
|
49
|
+
"@testing-library/react": "^16.3.0",
|
|
50
|
+
"@testing-library/user-event": "^14.6.1",
|
|
51
|
+
"@types/react": "^19.1.2",
|
|
52
|
+
"@vitejs/plugin-react": "^4.4.1",
|
|
53
|
+
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
|
|
54
|
+
"playwright": "^1.52.0",
|
|
55
|
+
"react": "^19.1.0",
|
|
56
|
+
"react-dom": "^19.1.0",
|
|
57
|
+
"react-error-boundary": "^6.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@portabletext/editor": "^1.55.12",
|
|
61
|
+
"@sanity/sdk-react": "^2.1.0",
|
|
62
|
+
"react": "^19.1.0",
|
|
63
|
+
"react-dom": "^19.1.0"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@sanity/diff-patch": "^6.0.0",
|
|
67
|
+
"@sanity/json-match": "^1.0.5",
|
|
68
|
+
"react-compiler-runtime": "19.1.0-rc.1"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "pkg-utils build --strict --check --clean",
|
|
72
|
+
"check:lint": "biome lint .",
|
|
73
|
+
"check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ignore-pattern '**/__tests__/**' --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --plugin react-hooks --rule 'react-compiler/react-compiler: [warn]' --rule 'react-hooks/rules-of-hooks: [error]' --rule 'react-hooks/exhaustive-deps: [error]' src",
|
|
74
|
+
"check:types": "tsc",
|
|
75
|
+
"check:types:watch": "tsc --watch",
|
|
76
|
+
"clean": "del .turbo && del dist && del node_modules",
|
|
77
|
+
"dev": "pkg-utils watch",
|
|
78
|
+
"lint:fix": "biome lint --write .",
|
|
79
|
+
"test": "vitest --run"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './plugin.sdk-value'
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
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) yield* getSegments(node.base)
|
|
41
|
+
if (node.segment.type !== 'This') yield node.segment
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isKeyPath(node: ExprNode): node is PathNode {
|
|
45
|
+
if (node.type !== 'Path') return false
|
|
46
|
+
if (node.base) return false
|
|
47
|
+
if (node.recursive) return false
|
|
48
|
+
if (node.segment.type !== 'Identifier') return false
|
|
49
|
+
return node.segment.name === '_key'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function arrayifyPath(pathExpr: string): Path {
|
|
53
|
+
const node = parsePath(pathExpr)
|
|
54
|
+
if (!node) return []
|
|
55
|
+
if (node.type !== 'Path') throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
56
|
+
|
|
57
|
+
return Array.from(getSegments(node)).map((segment): PathSegment => {
|
|
58
|
+
if (segment.type === 'Identifier') return segment.name
|
|
59
|
+
if (segment.type !== 'Subscript') throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
60
|
+
if (segment.elements.length !== 1) throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
61
|
+
|
|
62
|
+
const [element] = segment.elements
|
|
63
|
+
if (element.type === 'Number') return element.value
|
|
64
|
+
|
|
65
|
+
if (element.type !== 'Comparison') throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
66
|
+
if (element.operator !== '==') throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
67
|
+
const keyPathNode = [element.left, element.right].find(isKeyPath)
|
|
68
|
+
if (!keyPathNode) throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
69
|
+
const other = element.left === keyPathNode ? element.right : element.left
|
|
70
|
+
if (other.type !== 'String') throw new Error(ARRAYIFY_ERROR_MESSAGE)
|
|
71
|
+
return {_key: other.value}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function convertPatches(patches: SanityPatchOperations[]): PtePatch[] {
|
|
76
|
+
return patches.flatMap((p) => {
|
|
77
|
+
return Object.entries(p).flatMap(([type, values]): PtePatch[] => {
|
|
78
|
+
const origin = 'remote'
|
|
79
|
+
|
|
80
|
+
switch (type) {
|
|
81
|
+
case 'set':
|
|
82
|
+
case 'setIfMissing':
|
|
83
|
+
case 'diffMatchPatch':
|
|
84
|
+
case 'inc':
|
|
85
|
+
case 'dec': {
|
|
86
|
+
return Object.entries(values).map(
|
|
87
|
+
([pathExpr, value]) =>
|
|
88
|
+
({type, value, origin, path: arrayifyPath(pathExpr)}) as PtePatch,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
case 'unset': {
|
|
92
|
+
if (!Array.isArray(values)) return []
|
|
93
|
+
return values.map(arrayifyPath).map((path) => ({type, origin, path}))
|
|
94
|
+
}
|
|
95
|
+
case 'insert': {
|
|
96
|
+
const {items, ...rest} = values as InsertPatch['insert']
|
|
97
|
+
type InsertPosition = PteInsertPatch['position']
|
|
98
|
+
const position = Object.keys(rest).at(0) as InsertPosition | undefined
|
|
99
|
+
|
|
100
|
+
if (!position) return []
|
|
101
|
+
const pathExpr = (rest as {[K in InsertPosition]: string})[position]
|
|
102
|
+
const insertPatch: PteInsertPatch = {
|
|
103
|
+
type,
|
|
104
|
+
origin,
|
|
105
|
+
position,
|
|
106
|
+
path: arrayifyPath(pathExpr),
|
|
107
|
+
items: items as JSONValue[],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [insertPatch]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
default: {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
export function SDKValuePlugin(props: SDKValuePluginProps) {
|
|
124
|
+
// NOTE: the real `useEditDocument` suspends until the document is loaded into the SDK store
|
|
125
|
+
const setSdkValue = useEditDocument(props)
|
|
126
|
+
const instance = useSanityInstance(props)
|
|
127
|
+
const editor = useEditor()
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const getEditorValue = () => editor.getSnapshot().context.value
|
|
131
|
+
const {getCurrent: getSdkValue, subscribe: onSdkValueChange} =
|
|
132
|
+
getDocumentState<PortableTextBlock[]>(instance, props)
|
|
133
|
+
|
|
134
|
+
const editorSubscription = editor.on('patch', () =>
|
|
135
|
+
setSdkValue(getEditorValue()),
|
|
136
|
+
)
|
|
137
|
+
const unsubscribeToEditorChanges = () => editorSubscription.unsubscribe()
|
|
138
|
+
const unsubscribeToSdkChanges = onSdkValueChange(() => {
|
|
139
|
+
const snapshot = getEditorValue()
|
|
140
|
+
const patches = convertPatches(diffValue(snapshot, getSdkValue()))
|
|
141
|
+
|
|
142
|
+
if (patches.length) {
|
|
143
|
+
editor.send({type: 'patches', patches, snapshot})
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// update initial value
|
|
148
|
+
editor.send({type: 'update value', value: getSdkValue() ?? []})
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
unsubscribeToEditorChanges()
|
|
152
|
+
unsubscribeToSdkChanges()
|
|
153
|
+
}
|
|
154
|
+
}, [setSdkValue, editor, instance, props])
|
|
155
|
+
|
|
156
|
+
return null
|
|
157
|
+
}
|