@sanity/hierarchical-document-list 0.1.0-next.1

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/lib/TreeDeskStructure.d.ts +7 -0
  4. package/lib/TreeDeskStructure.js +43 -0
  5. package/lib/TreeInputComponent.d.ts +19 -0
  6. package/lib/TreeInputComponent.js +10 -0
  7. package/lib/components/DocumentInNode.d.ts +11 -0
  8. package/lib/components/DocumentInNode.js +46 -0
  9. package/lib/components/DocumentPreviewStatus.d.ts +7 -0
  10. package/lib/components/DocumentPreviewStatus.js +16 -0
  11. package/lib/components/NodeActions.d.ts +10 -0
  12. package/lib/components/NodeActions.js +23 -0
  13. package/lib/components/NodeContentRenderer.d.ts +8 -0
  14. package/lib/components/NodeContentRenderer.js +79 -0
  15. package/lib/components/PlaceholderDropzone.d.ts +9 -0
  16. package/lib/components/PlaceholderDropzone.js +17 -0
  17. package/lib/components/TreeEditor.d.ts +12 -0
  18. package/lib/components/TreeEditor.js +41 -0
  19. package/lib/components/TreeEditorErrorBoundary.d.ts +17 -0
  20. package/lib/components/TreeEditorErrorBoundary.js +40 -0
  21. package/lib/components/TreeNodeRenderer.d.ts +3 -0
  22. package/lib/components/TreeNodeRenderer.js +22 -0
  23. package/lib/components/TreeNodeRendererScaffold.d.ts +4 -0
  24. package/lib/components/TreeNodeRendererScaffold.js +164 -0
  25. package/lib/createDeskHierarchy.d.ts +10 -0
  26. package/lib/createDeskHierarchy.js +52 -0
  27. package/lib/createHierarchicalField.d.ts +8 -0
  28. package/lib/createHierarchicalField.js +30 -0
  29. package/lib/hiearchy.tree.d.ts +23 -0
  30. package/lib/hiearchy.tree.js +28 -0
  31. package/lib/index.d.ts +3 -0
  32. package/lib/index.js +3 -0
  33. package/lib/utils/flatDataToTree.d.ts +6 -0
  34. package/lib/utils/flatDataToTree.js +14 -0
  35. package/lib/utils/getAdjescentNodes.d.ts +12 -0
  36. package/lib/utils/getAdjescentNodes.js +15 -0
  37. package/lib/utils/getCommonTreeProps.d.ts +7 -0
  38. package/lib/utils/getCommonTreeProps.js +15 -0
  39. package/lib/utils/getTreeHeight.d.ts +3 -0
  40. package/lib/utils/getTreeHeight.js +7 -0
  41. package/lib/utils/gradientPatchAdapter.d.ts +4 -0
  42. package/lib/utils/gradientPatchAdapter.js +34 -0
  43. package/lib/utils/idUtils.d.ts +2 -0
  44. package/lib/utils/idUtils.js +6 -0
  45. package/lib/utils/moveItemInArray.d.ts +5 -0
  46. package/lib/utils/moveItemInArray.js +13 -0
  47. package/lib/utils/treeData.d.ts +18 -0
  48. package/lib/utils/treeData.js +77 -0
  49. package/lib/utils/treePatches.d.ts +13 -0
  50. package/lib/utils/treePatches.js +133 -0
  51. package/lib/utils/useAllItems.d.ts +7 -0
  52. package/lib/utils/useAllItems.js +92 -0
  53. package/lib/utils/useLocalTree.d.ts +17 -0
  54. package/lib/utils/useLocalTree.js +27 -0
  55. package/lib/utils/useTreeOperations.d.ts +9 -0
  56. package/lib/utils/useTreeOperations.js +16 -0
  57. package/lib/utils/useTreeOperationsProvider.d.ts +15 -0
  58. package/lib/utils/useTreeOperationsProvider.js +52 -0
  59. package/package.json +54 -0
  60. package/sanity.json +12 -0
  61. package/screenshot-1.jpg +0 -0
  62. package/tsconfig.json +20 -0
@@ -0,0 +1,13 @@
1
+ import { FullTree, NodeData, NodeRendererProps, OnMovePreviousAndNextLocation, TreeItem } from 'react-sortable-tree';
2
+ import { SanityTreeItem } from '../types';
3
+ export declare type HandleMovedNodeData = Omit<NodeData & FullTree & OnMovePreviousAndNextLocation, 'prevPath' | 'prevTreeIndex' | 'path' | 'treeIndex'>;
4
+ export declare type HandleMovedNode = (moveData: HandleMovedNodeData) => void;
5
+ export declare function getAddItemPatch(item: SanityTreeItem): unknown[];
6
+ export declare function getDuplicateItemPatch(nodeProps: NodeRendererProps): unknown[];
7
+ export declare function getRemoveItemPatch({ node }: Pick<NodeRendererProps, 'node'>): unknown[];
8
+ export declare function getMovedNodePatch(data: HandleMovedNodeData): unknown[];
9
+ export declare function getMoveItemPatch({ nodeProps: { node, treeIndex, parentNode }, localTree, direction }: {
10
+ nodeProps: NodeRendererProps;
11
+ localTree: TreeItem[];
12
+ direction: 'up' | 'down';
13
+ }): unknown[];
@@ -0,0 +1,133 @@
1
+ import * as Patch from '@sanity/form-builder/lib/patch/patches';
2
+ import { randomKey } from '@sanity/util/content';
3
+ import { getFlatDataFromTree } from 'react-sortable-tree';
4
+ import getAdjescentNodes from './getAdjescentNodes';
5
+ import moveItemInArray from './moveItemInArray';
6
+ import { normalizeNodeForStorage } from './treeData';
7
+ export function getAddItemPatch(item) {
8
+ const normalizedNode = normalizeNodeForStorage(item);
9
+ return [
10
+ // Add the node to the end of the tree
11
+ Patch.insert([normalizedNode], 'after', [-1])
12
+ ];
13
+ }
14
+ export function getDuplicateItemPatch(nodeProps) {
15
+ const newItem = {
16
+ ...nodeProps.node,
17
+ _key: randomKey(12)
18
+ };
19
+ const normalizedNode = normalizeNodeForStorage(newItem);
20
+ return [
21
+ // Add duplicated node before the existing one
22
+ Patch.insert([normalizedNode], 'before', [{ _key: nodeProps.node._key }])
23
+ ];
24
+ }
25
+ export function getRemoveItemPatch({ node }) {
26
+ const keyPath = { _key: node._key };
27
+ const children = getChildrenPaths(node);
28
+ return [
29
+ // 1. Unset the removed node
30
+ Patch.unset([keyPath]),
31
+ // 2. Unset its children
32
+ ...children.map((path) => Patch.unset([{ _key: path }]))
33
+ ];
34
+ }
35
+ export function getMovedNodePatch(data) {
36
+ const { nextParentNode } = data;
37
+ const keyPath = { _key: data.node._key };
38
+ // === REMOVING NODE FROM TREE ===
39
+ // `nextPath` will be null if the item is removed from tree
40
+ if (!Array.isArray(data.nextPath)) {
41
+ return getRemoveItemPatch({ node: data.node });
42
+ }
43
+ const nextFlatTree = getFlatDataFromTree({
44
+ treeData: data.treeData,
45
+ getNodeKey: (t) => t.node._key
46
+ });
47
+ const normalizedNode = normalizeNodeForStorage(data.node);
48
+ const { leadingNode, followingNode } = getAdjescentNodes({
49
+ flatTree: nextFlatTree,
50
+ node: data.node,
51
+ treeIndex: data.nextTreeIndex
52
+ });
53
+ return [
54
+ // 1. Unset the moved node
55
+ // (will be ignored by Content Lake on new nodes with _key not yet in tree)
56
+ Patch.unset([keyPath]),
57
+ // 2. SIBLING-BASED PLACEMENT
58
+ // If we were to place solely based on nextTreeIndex, concurrent changes from other editors could put the new node in an unexpected position.
59
+ // Let's instead anchor it to the _key of the sibling coming before or after it.
60
+ leadingNode?.node?._key
61
+ ? // After the sibling before it
62
+ Patch.insert([normalizedNode], 'after', [{ _key: leadingNode.node._key }])
63
+ : // Or before the sibling right after it, in case there's no leading sibling node
64
+ Patch.insert([normalizedNode], 'before', [
65
+ followingNode?.node?._key ? { _key: followingNode.node._key } : data.nextTreeIndex
66
+ ]),
67
+ // 3. Patch the new node with its new `parent`
68
+ nextParentNode
69
+ ? // If it has a parent node, set that parent's _key
70
+ Patch.set(nextParentNode._key, [keyPath, 'parent'])
71
+ : // Else remove the parent key entirely
72
+ Patch.unset([keyPath, 'parent'])
73
+ ];
74
+ }
75
+ function getChildrenPaths(node) {
76
+ if (!Array.isArray(node.children)) {
77
+ return [];
78
+ }
79
+ return node.children
80
+ .reduce((keyPaths, child) => [...keyPaths, child._key, ...getChildrenPaths(child)], [])
81
+ .filter(Boolean);
82
+ }
83
+ export function getMoveItemPatch({ nodeProps: { node, treeIndex, parentNode }, localTree, direction = 'up' }) {
84
+ const keyPath = { _key: node._key };
85
+ const nextTreeIndex = treeIndex + (direction === 'up' ? -1 : 1);
86
+ const flatTree = getFlatDataFromTree({
87
+ treeData: localTree,
88
+ getNodeKey: (t) => t.node._key
89
+ });
90
+ const nextFlatTree = moveItemInArray({
91
+ array: flatTree,
92
+ fromIndex: treeIndex,
93
+ toIndex: nextTreeIndex
94
+ });
95
+ const { leadingNode, followingNode } = getAdjescentNodes({
96
+ flatTree: nextFlatTree,
97
+ node,
98
+ treeIndex: nextTreeIndex
99
+ });
100
+ const normalizedNode = normalizeNodeForStorage(node);
101
+ console.log(`Move ${direction}`, {
102
+ node,
103
+ treeIndex,
104
+ parentNode,
105
+ nextFlatTree,
106
+ flatTree,
107
+ localTree,
108
+ leadingSibling: leadingNode,
109
+ followingSibling: followingNode
110
+ });
111
+ // When moving up, look at following node to figure out what is the next parent.
112
+ const nodeToInheritParent = direction === 'up' ? followingNode : leadingNode;
113
+ const nextParentNode = nodeToInheritParent?.parentNode;
114
+ return [
115
+ // 1. Unset the moved node
116
+ // (will be ignored by Content Lake on new nodes with _key not yet in tree)
117
+ Patch.unset([keyPath]),
118
+ // 2. SIBLING-BASED PLACEMENT
119
+ leadingNode?.node?._key
120
+ ? // After the sibling before it
121
+ Patch.insert([normalizedNode], 'after', [{ _key: leadingNode.node._key }])
122
+ : // Or before the sibling right after it, in case there's no leading sibling node
123
+ Patch.insert([normalizedNode], 'before', [
124
+ followingNode?.node?._key ? { _key: followingNode.node._key } : nextTreeIndex
125
+ ]),
126
+ // 3. Patch the new node with its new `parent`
127
+ nextParentNode
128
+ ? // If it has a parent node, set that parent's _key
129
+ Patch.set(nextParentNode._key, [keyPath, 'parent'])
130
+ : // Else remove the parent key entirely
131
+ Patch.unset([keyPath, 'parent'])
132
+ ];
133
+ }
@@ -0,0 +1,7 @@
1
+ import { AllItems, TreeInputOptions } from '../types';
2
+ declare type Status = 'loading' | 'success' | 'error';
3
+ export default function useAllItems(options: TreeInputOptions): {
4
+ status: Status;
5
+ allItems: AllItems;
6
+ };
7
+ export {};
@@ -0,0 +1,92 @@
1
+ import sanityClient from 'part:@sanity/base/client';
2
+ import React from 'react';
3
+ import { isDraft, unprefixId } from './idUtils';
4
+ const client = sanityClient.withConfig({
5
+ apiVersion: '2021-09-01'
6
+ });
7
+ function getDeskFilter({ referenceTo, referenceOptions }) {
8
+ const filterParts = ['_type in $docTypes'];
9
+ if (referenceOptions?.filter) {
10
+ filterParts.push(referenceOptions.filter);
11
+ }
12
+ return {
13
+ filter: filterParts.join(' && '),
14
+ params: {
15
+ ...(referenceOptions?.filterParams || {}),
16
+ docTypes: referenceTo.map((schemaType) => schemaType)
17
+ }
18
+ };
19
+ }
20
+ function updateItemInState(state, item) {
21
+ const newState = { ...state };
22
+ const publishedId = unprefixId(item._id);
23
+ newState[publishedId] = {
24
+ ...(newState[publishedId] || {}),
25
+ [isDraft(item._id) ? 'draft' : 'published']: item
26
+ };
27
+ return newState;
28
+ }
29
+ function allItemsReducer(state, action) {
30
+ if (action.type === 'addOrEditItem' && action.item?._id) {
31
+ return updateItemInState(state, action.item);
32
+ }
33
+ if (action.type === 'removeItem') {
34
+ const publishedId = unprefixId(action.itemId);
35
+ return {
36
+ ...state,
37
+ [publishedId]: isDraft(action.itemId)
38
+ ? // If a draft, keep only published
39
+ {
40
+ published: state[publishedId]?.published
41
+ }
42
+ : {
43
+ draft: state[publishedId]?.draft
44
+ }
45
+ };
46
+ }
47
+ if (action.type === 'setInitialData') {
48
+ return action.items.reduce(updateItemInState, {});
49
+ }
50
+ return state;
51
+ }
52
+ export default function useAllItems(options) {
53
+ const [status, setStatus] = React.useState('loading');
54
+ const [allItems, dispatch] = React.useReducer(allItemsReducer, {});
55
+ function handleListener(event) {
56
+ if (event.type !== 'mutation') {
57
+ return;
58
+ }
59
+ if (event.result) {
60
+ dispatch({ type: 'addOrEditItem', item: event.result });
61
+ }
62
+ else {
63
+ dispatch({ type: 'removeItem', itemId: event.documentId });
64
+ }
65
+ }
66
+ function handleFirstLoad(items) {
67
+ dispatch({ type: 'setInitialData', items });
68
+ setStatus('success');
69
+ }
70
+ React.useEffect(() => {
71
+ const { filter, params } = getDeskFilter(options);
72
+ const query = `*[${filter}] {
73
+ _id,
74
+ _type,
75
+ _updatedAt,
76
+ }`;
77
+ client
78
+ .fetch(query, params)
79
+ .then(handleFirstLoad)
80
+ .catch(() => {
81
+ setStatus('error');
82
+ });
83
+ const listener = client.listen(query, params).subscribe(handleListener);
84
+ return () => {
85
+ listener.unsubscribe();
86
+ };
87
+ }, []);
88
+ return {
89
+ status,
90
+ allItems
91
+ };
92
+ }
@@ -0,0 +1,17 @@
1
+ import { OnVisibilityToggleData, TreeItem } from 'react-sortable-tree';
2
+ import { AllItems, SanityTreeItem } from '../types';
3
+ /**
4
+ * Enhances tree data with information on:
5
+ * - `expanded` - native property of react-sortable-tree to determine collapsing & expanding of a node's children
6
+ * - `draftId` & `publishedId` - refer to SanityTreeItem's type annotations
7
+ *
8
+ * Doesn't modify the main tree or has side-effects on data.
9
+ * Has the added benefit of being local to the user, so external changes won't affect local visibility.
10
+ */
11
+ export default function useLocalTree({ tree, allItems }: {
12
+ tree: SanityTreeItem[];
13
+ allItems: AllItems;
14
+ }): {
15
+ handleVisibilityToggle: (data: OnVisibilityToggleData) => void;
16
+ localTree: TreeItem[];
17
+ };
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { dataToEditorTree } from './treeData';
3
+ /**
4
+ * Enhances tree data with information on:
5
+ * - `expanded` - native property of react-sortable-tree to determine collapsing & expanding of a node's children
6
+ * - `draftId` & `publishedId` - refer to SanityTreeItem's type annotations
7
+ *
8
+ * Doesn't modify the main tree or has side-effects on data.
9
+ * Has the added benefit of being local to the user, so external changes won't affect local visibility.
10
+ */
11
+ export default function useLocalTree({ tree, allItems }) {
12
+ const [visibilityMap, setVisibilityMap] = React.useState({});
13
+ function handleVisibilityToggle(data) {
14
+ setVisibilityMap({
15
+ ...visibilityMap,
16
+ [data.node._key]: data.expanded
17
+ });
18
+ }
19
+ return {
20
+ localTree: dataToEditorTree({
21
+ tree,
22
+ allItems,
23
+ visibilityMap
24
+ }),
25
+ handleVisibilityToggle
26
+ };
27
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import useAllItems from './useAllItems';
3
+ import useTreeOperationsProvider from './useTreeOperationsProvider';
4
+ declare type ContextValue = ReturnType<typeof useTreeOperationsProvider> & {
5
+ allItemsStatus: ReturnType<typeof useAllItems>['status'];
6
+ };
7
+ export declare const TreeOperationsContext: React.Context<ContextValue>;
8
+ export default function useTreeOperations(): ContextValue;
9
+ export {};
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ function placeholder() {
3
+ // no-op
4
+ }
5
+ export const TreeOperationsContext = React.createContext({
6
+ addItem: placeholder,
7
+ duplicateItem: placeholder,
8
+ removeItem: placeholder,
9
+ handleMovedNode: placeholder,
10
+ moveItemDown: placeholder,
11
+ moveItemUp: placeholder,
12
+ allItemsStatus: 'loading'
13
+ });
14
+ export default function useTreeOperations() {
15
+ return React.useContext(TreeOperationsContext);
16
+ }
@@ -0,0 +1,15 @@
1
+ import { NodeRendererProps, TreeItem } from 'react-sortable-tree';
2
+ import { SanityTreeItem } from '../types';
3
+ import { HandleMovedNode } from './treePatches';
4
+ export default function useTreeOperationsProvider(props: {
5
+ patchPrefix?: string;
6
+ onChange: (patch: unknown) => void;
7
+ localTree: TreeItem[];
8
+ }): {
9
+ handleMovedNode: HandleMovedNode;
10
+ addItem: (item: SanityTreeItem) => void;
11
+ duplicateItem: (nodeProps: NodeRendererProps) => void;
12
+ removeItem: (nodeProps: NodeRendererProps) => void;
13
+ moveItemUp: (nodeProps: NodeRendererProps) => void;
14
+ moveItemDown: (nodeProps: NodeRendererProps) => void;
15
+ };
@@ -0,0 +1,52 @@
1
+ import * as Patch from '@sanity/form-builder/lib/patch/patches';
2
+ import PatchEvent from '@sanity/form-builder/PatchEvent';
3
+ import { getAddItemPatch, getDuplicateItemPatch, getMovedNodePatch, getMoveItemPatch, getRemoveItemPatch } from './treePatches';
4
+ export default function useTreeOperationsProvider(props) {
5
+ const { localTree } = props;
6
+ function runPatches(patches) {
7
+ const finalPatches = [
8
+ // Ensure tree array exists before any operation
9
+ Patch.setIfMissing([]),
10
+ ...(patches || [])
11
+ ];
12
+ let patchEvent = PatchEvent.from(finalPatches);
13
+ if (props.patchPrefix) {
14
+ patchEvent = PatchEvent.from(finalPatches.map((patch) => Patch.prefixPath(patch, props.patchPrefix)));
15
+ }
16
+ props.onChange(patchEvent);
17
+ }
18
+ function handleMovedNode(data) {
19
+ runPatches(getMovedNodePatch(data));
20
+ }
21
+ function addItem(item) {
22
+ runPatches(getAddItemPatch(item));
23
+ }
24
+ function duplicateItem(nodeProps) {
25
+ runPatches(getDuplicateItemPatch(nodeProps));
26
+ }
27
+ function removeItem(nodeProps) {
28
+ runPatches(getRemoveItemPatch(nodeProps));
29
+ }
30
+ function moveItemUp(nodeProps) {
31
+ runPatches(getMoveItemPatch({
32
+ nodeProps,
33
+ localTree,
34
+ direction: 'up'
35
+ }));
36
+ }
37
+ function moveItemDown(nodeProps) {
38
+ runPatches(getMoveItemPatch({
39
+ nodeProps,
40
+ localTree,
41
+ direction: 'down'
42
+ }));
43
+ }
44
+ return {
45
+ handleMovedNode,
46
+ addItem,
47
+ removeItem,
48
+ moveItemUp,
49
+ moveItemDown,
50
+ duplicateItem
51
+ };
52
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sanity/hierarchical-document-list",
3
+ "version": "0.1.0-next.1",
4
+ "author": "Sanity <hello@sanity.io>",
5
+ "license": "MIT",
6
+ "main": "lib/index.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sanity-io/hierarchical-document-list.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/sanity-io/hierarchical-document-list/issues"
13
+ },
14
+ "homepage": "https://github.com/sanity-io/hierarchical-document-list#readme",
15
+ "scripts": {
16
+ "dev": "tsc -w -d",
17
+ "build": "tsc -d",
18
+ "format": "prettier src -w",
19
+ "lint": "eslint src"
20
+ },
21
+ "devDependencies": {
22
+ "@types/assert": "^1.5.6",
23
+ "@types/react": "^17.0.38",
24
+ "@types/react-dom": "^17.0.11",
25
+ "@types/react-sortable-tree": "^0.3.14",
26
+ "@types/styled-components": "^5.1.21",
27
+ "@typescript-eslint/eslint-plugin": "^5.10.1",
28
+ "@typescript-eslint/parser": "^5.10.1",
29
+ "eslint": "^8.7.0",
30
+ "eslint-config-prettier": "^8.3.0",
31
+ "eslint-config-sanity": "^5.1.0",
32
+ "prettier": "^2.5.1",
33
+ "styled-components": "^5.3.3",
34
+ "typescript": "^4.5.5"
35
+ },
36
+ "type": "module",
37
+ "dependencies": {
38
+ "@sanity/base": ">= 2.25.0",
39
+ "@sanity/color": "^2.1.6",
40
+ "@sanity/desk-tool": ">= 2.25.0",
41
+ "@sanity/form-builder": "^2.25.0",
42
+ "@sanity/icons": ">= 1.2.0",
43
+ "@sanity/ui": ">= 0.37.0",
44
+ "assert": "^2.0.0",
45
+ "react": "^17.0.2",
46
+ "react-dom": "^17.0.2",
47
+ "react-sortable-tree": "^2.8.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@sanity/base": ">= 2.25.0",
51
+ "@sanity/desk-tool": ">= 2.25.0",
52
+ "styled-components": ">= 5.2.0"
53
+ }
54
+ }
package/sanity.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "paths": {
3
+ "source": "./src",
4
+ "compiled": "./lib"
5
+ },
6
+ "parts": [
7
+ {
8
+ "implements": "part:@sanity/base/schema-type",
9
+ "path": "hierarchy.tree.js"
10
+ }
11
+ ]
12
+ }
Binary file
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "react-jsx",
17
+ "outDir": "lib"
18
+ },
19
+ "include": ["./src"]
20
+ }