mirror of
https://github.com/foomo/contentfultree.git
synced 2025-10-16 12:25:41 +00:00
feat: enable multiple trees
This commit is contained in:
parent
0fba987343
commit
7db48d4325
62
package.json
62
package.json
@ -17,38 +17,32 @@
|
|||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
"@types/styled-components": "^5.1.26",
|
"@types/styled-components": "^5.1.26",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.38.0",
|
"@typescript-eslint/parser": "^5.38.0",
|
||||||
"contentful-management": "^10.6.3",
|
"eslint": "^8.0.1",
|
||||||
"contentful-ui-extensions-sdk": "^4.8.1",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint": "^8.0.1",
|
"eslint-config-standard-with-typescript": "^23.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-plugin-import": "^2.25.2",
|
||||||
"eslint-config-standard-with-typescript": "^23.0.0",
|
"eslint-plugin-json": "3.1.0",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-plugin-n": "^15.0.0",
|
||||||
"eslint-plugin-json": "3.1.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-n": "^15.0.0",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"install-peers": "^1.0.4",
|
||||||
"eslint-plugin-react": "^7.31.8",
|
"prettier": "2.7.1",
|
||||||
"immer": "^9.0.15",
|
"typescript": "*"
|
||||||
"install-peers": "^1.0.4",
|
},
|
||||||
"prettier": "2.7.1",
|
"peerDependencies": {
|
||||||
"react": "18.2.0",
|
"install-peers": "^1.0.4"
|
||||||
"styled-components": "^5.3.5",
|
},
|
||||||
"typescript": "*",
|
"dependencies": {
|
||||||
"use-immer": "^0.7.0"
|
"contentful-management": "10.15.0",
|
||||||
},
|
"contentful-ui-extensions-sdk": "4.12.1",
|
||||||
"peerDependencies": {
|
"immer": "9.0.15",
|
||||||
"install-peers": "^1.0.4"
|
"react": "18.2.0",
|
||||||
},
|
"styled-components": "5.3.5",
|
||||||
"dependencies": {
|
"use-immer": "0.7.0"
|
||||||
"contentful-management": "^10.15.0",
|
}
|
||||||
"contentful-ui-extensions-sdk": "^4.12.1",
|
|
||||||
"immer": "^9.0.15",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"styled-components": "^5.3.5",
|
|
||||||
"use-immer": "^0.7.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,10 @@ export interface UiPalette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const StyledContentTreeTable = styled.table`
|
export const StyledContentTreeTable = styled.table`
|
||||||
|
width: 100%;
|
||||||
color: black;
|
color: black;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
padding: 0 60px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
td {
|
td {
|
||||||
padding: 0.2em 1em 0.2em 0.2em;
|
padding: 0.2em 1em 0.2em 0.2em;
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
import {
|
import { PlainClientAPI } from 'contentful-management';
|
||||||
EntryProps,
|
|
||||||
KeyValueMap,
|
|
||||||
Link,
|
|
||||||
PlainClientAPI,
|
|
||||||
} from 'contentful-management';
|
|
||||||
import { PageExtensionSDK } from 'contentful-ui-extensions-sdk';
|
import { PageExtensionSDK } from 'contentful-ui-extensions-sdk';
|
||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import { useImmer } from 'use-immer';
|
import { useImmer } from 'use-immer';
|
||||||
|
import { ContentTreeRoot } from './ContentTreeRoot';
|
||||||
import { StyledContentTreeTable } from './ContentTree.styled';
|
import { emptyNodeProps, cfEntriesToNodes } from './ContentTreeUtils';
|
||||||
import ContentTreeNode, { ContentTreeNodeProps } from './ContentTreeNode';
|
|
||||||
|
|
||||||
export interface ContentTreeProps {
|
export interface ContentTreeProps {
|
||||||
sdkInstance: PageExtensionSDK;
|
sdkInstance: PageExtensionSDK;
|
||||||
@ -21,250 +15,47 @@ export interface ContentTreeProps {
|
|||||||
iconRegistry?: { [index: string]: string };
|
iconRegistry?: { [index: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyNodeProps = (): ContentTreeNodeProps => {
|
|
||||||
return { id: '', name: '', expand: false, parentId: '' };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ContentTree = (props: ContentTreeProps): ReactElement => {
|
export const ContentTree = (props: ContentTreeProps): ReactElement => {
|
||||||
const [stLocale] = useState(props.locales[0]);
|
const [stLocale] = useState(props.locales[0]);
|
||||||
const [stRoot, setStRoot] = useImmer(emptyNodeProps());
|
const [rootNodes, setRootNodes] = useImmer([emptyNodeProps()]);
|
||||||
|
|
||||||
// EFFECTS
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.sdkInstance) {
|
if (props.sdkInstance) {
|
||||||
loadRootData().catch((err) => {
|
loadData().catch((err) => {
|
||||||
throw new Error('loadRootData', err);
|
throw new Error('loadRootData', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [props.sdkInstance]);
|
}, [props.sdkInstance]);
|
||||||
|
|
||||||
// FUNCTIONS
|
const loadData = async (): Promise<void> => {
|
||||||
|
|
||||||
const addChildNodes = async (node: ContentTreeNodeProps): Promise<void> => {
|
|
||||||
let childNodes: ContentTreeNodeProps[] = [];
|
|
||||||
const cfChildren = await getContentfulChildEntries(node.id);
|
|
||||||
childNodes = cfEntriesToNodes(cfChildren, node.id);
|
|
||||||
setStRoot((draft) => {
|
|
||||||
recursiveProcessNodes(
|
|
||||||
node.id,
|
|
||||||
(targetNode) => {
|
|
||||||
targetNode.childNodes = childNodes;
|
|
||||||
targetNode.expand = true;
|
|
||||||
},
|
|
||||||
draft
|
|
||||||
);
|
|
||||||
console.log('🎈draft', draft);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const recursiveProcessNodes = (
|
|
||||||
targetNodeId: string,
|
|
||||||
processNode: (node: ContentTreeNodeProps) => void,
|
|
||||||
node: ContentTreeNodeProps
|
|
||||||
): void => {
|
|
||||||
if (node.id === targetNodeId) {
|
|
||||||
processNode(node);
|
|
||||||
}
|
|
||||||
if (node.childNodes != null) {
|
|
||||||
for (const targetNode of node.childNodes) {
|
|
||||||
recursiveProcessNodes(targetNodeId, processNode, targetNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const editEntry = async (entryId: string): Promise<void> => {
|
|
||||||
await props.sdkInstance.navigator.openEntry(entryId, { slideIn: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContentfulChildEntries = async (
|
|
||||||
parentId: string
|
|
||||||
): Promise<Array<EntryProps<KeyValueMap>>> => {
|
|
||||||
const parentItem = await props.cma.entry.get({ entryId: parentId });
|
|
||||||
const allChildIds: string[] = [];
|
|
||||||
for (const key of Object.keys(parentItem.fields)) {
|
|
||||||
if (props.nodeContentTypes.includes(key)) {
|
|
||||||
const childNodeRefs = parentItem.fields[key][stLocale] as
|
|
||||||
| Link<string>
|
|
||||||
| Array<Link<string>>;
|
|
||||||
if (Array.isArray(childNodeRefs)) {
|
|
||||||
for (const childNodeRef of childNodeRefs) {
|
|
||||||
allChildIds.push(childNodeRef.sys.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allChildIds.push(childNodeRefs.sys.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const allItems: Array<EntryProps<KeyValueMap>> = [];
|
|
||||||
let done = false;
|
|
||||||
let skip = 0;
|
|
||||||
while (!done) {
|
|
||||||
const col = await props.cma.entry.getMany({
|
|
||||||
query: {
|
|
||||||
'sys.id[in]': allChildIds.join(','),
|
|
||||||
skip,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
allItems.push(...col.items);
|
|
||||||
if (allItems.length < col.total) {
|
|
||||||
skip += 100;
|
|
||||||
} else {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cfChildren: Array<EntryProps<KeyValueMap>> = [];
|
|
||||||
const idPositionMap: { [index: string]: number } = allItems.reduce(
|
|
||||||
(acc: any, el, i) => {
|
|
||||||
acc[el.sys.id] = i;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
for (const childId of allChildIds) {
|
|
||||||
if (allItems[idPositionMap[childId]]) {
|
|
||||||
cfChildren.push(allItems[idPositionMap[childId]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cfChildren;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfEntriesToNodes = (
|
|
||||||
entries: Array<EntryProps<KeyValueMap>>,
|
|
||||||
parentId?: string
|
|
||||||
): ContentTreeNodeProps[] => {
|
|
||||||
if (entries.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const nodeArray: ContentTreeNodeProps[] = [];
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (!entry) {
|
|
||||||
console.log('this entry is nil');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let name = '';
|
|
||||||
for (const titleField of props.titleFields) {
|
|
||||||
if (entry.fields[titleField]?.[stLocale]) {
|
|
||||||
name = entry.fields[titleField][stLocale];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (name === '') {
|
|
||||||
name = entry.sys.id;
|
|
||||||
}
|
|
||||||
const node: ContentTreeNodeProps = {
|
|
||||||
id: entry.sys.id,
|
|
||||||
name,
|
|
||||||
contentType: entry.sys.contentType.sys.id,
|
|
||||||
icon:
|
|
||||||
props.iconRegistry != null
|
|
||||||
? props.iconRegistry[entry.sys.contentType.sys.id]
|
|
||||||
: '',
|
|
||||||
expand: !!parentId,
|
|
||||||
parentId,
|
|
||||||
hasChildNodes: cfEntryHasChildren(entry),
|
|
||||||
publishingStatus: cfEntryPublishingStatus(entry),
|
|
||||||
updatedAt: entry.sys.updatedAt,
|
|
||||||
publishedAt: entry.sys.publishedAt,
|
|
||||||
};
|
|
||||||
nodeArray.push(node);
|
|
||||||
});
|
|
||||||
return nodeArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfEntryHasChildren = (entry: EntryProps<KeyValueMap>): boolean => {
|
|
||||||
for (const nodeContentType of props.nodeContentTypes) {
|
|
||||||
for (const locale of props.locales) {
|
|
||||||
if (entry.fields[nodeContentType]?.[locale]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfEntryPublishingStatus = (entry: EntryProps<KeyValueMap>): string => {
|
|
||||||
if (!entry.sys.publishedVersion) {
|
|
||||||
return 'draft';
|
|
||||||
}
|
|
||||||
if (entry.sys.version - entry.sys.publishedVersion === 1) {
|
|
||||||
return 'published';
|
|
||||||
}
|
|
||||||
return 'changed';
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadRootData = async (): Promise<void> => {
|
|
||||||
const CfRootData = await props.cma.entry.getMany({
|
const CfRootData = await props.cma.entry.getMany({
|
||||||
query: { content_type: props.rootType },
|
query: { content_type: props.rootType },
|
||||||
});
|
});
|
||||||
const rootNodes = cfEntriesToNodes(CfRootData.items);
|
const nodes = cfEntriesToNodes(
|
||||||
for (const rootNode of rootNodes) {
|
CfRootData.items,
|
||||||
const childEntries = await getContentfulChildEntries(rootNode.id);
|
props.titleFields,
|
||||||
const childNodes = cfEntriesToNodes(childEntries, rootNode.id);
|
stLocale,
|
||||||
const nodes = [rootNode, ...childNodes];
|
props.locales,
|
||||||
if (nodes.length > 0) {
|
props.nodeContentTypes,
|
||||||
const newIdPositionMap = nodes.reduce((acc: any, el, i) => {
|
props.iconRegistry
|
||||||
acc[el.id] = i;
|
);
|
||||||
return acc;
|
setRootNodes(nodes);
|
||||||
}, {});
|
|
||||||
let tree: ContentTreeNodeProps = emptyNodeProps();
|
|
||||||
nodes.forEach((node: ContentTreeNodeProps) => {
|
|
||||||
node.childNodes = [];
|
|
||||||
if (!node.parentId) {
|
|
||||||
tree = node;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parentEl = nodes[newIdPositionMap[node.parentId]];
|
|
||||||
if (parentEl) {
|
|
||||||
parentEl.childNodes = [...(parentEl.childNodes ?? []), node];
|
|
||||||
parentEl.expand = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('🌴 tree', tree);
|
|
||||||
setStRoot(tree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeChildNodes = (node: ContentTreeNodeProps): void => {
|
|
||||||
setStRoot((draft) => {
|
|
||||||
recursiveProcessNodes(
|
|
||||||
node.id,
|
|
||||||
(targetNode) => {
|
|
||||||
targetNode.childNodes = [];
|
|
||||||
targetNode.expand = false;
|
|
||||||
},
|
|
||||||
draft
|
|
||||||
);
|
|
||||||
console.log('🎈draft', draft);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'=============================== RENDER ====================================',
|
|
||||||
stRoot
|
|
||||||
);
|
|
||||||
// create ID mapping
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledContentTreeTable>
|
{rootNodes.map((node, i) => (
|
||||||
<tbody>
|
<ContentTreeRoot
|
||||||
<tr>
|
key={i.toString()}
|
||||||
<th>Nodes</th>
|
node={node}
|
||||||
<th>Content Type</th>
|
locales={props.locales}
|
||||||
<th>Status</th>
|
nodeContentTypes={props.nodeContentTypes}
|
||||||
<th>Last Modified</th>
|
iconRegistry={props.iconRegistry}
|
||||||
<th>Last Published</th>
|
cma={props.cma}
|
||||||
</tr>
|
titleFields={props.titleFields}
|
||||||
<ContentTreeNode
|
sdkInstance={props.sdkInstance}
|
||||||
node={stRoot}
|
/>
|
||||||
depth={0}
|
))}
|
||||||
addChildNodes={addChildNodes}
|
|
||||||
removeChildNodes={removeChildNodes}
|
|
||||||
editEntry={editEntry}
|
|
||||||
/>
|
|
||||||
</tbody>
|
|
||||||
</StyledContentTreeTable>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,11 +31,8 @@ const ContentTreeNode = (props: {
|
|||||||
editEntry: (nodeId: string) => Promise<void>;
|
editEntry: (nodeId: string) => Promise<void>;
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [node] = useState(props.node);
|
|
||||||
|
|
||||||
// React.useEffect(()=>{setLoading(false)},[props.node.childNodes])
|
const addChildren = async (node: ContentTreeNodeProps): Promise<void> => {
|
||||||
|
|
||||||
const addChildren = async (): Promise<void> => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await props.addChildNodes(node);
|
await props.addChildNodes(node);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -47,8 +44,8 @@ const ContentTreeNode = (props: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddChildren = (): void => {
|
const handleAddChildren = (node: ContentTreeNodeProps): void => {
|
||||||
addChildren().catch((err) => {
|
addChildren(node).catch((err) => {
|
||||||
throw new Error('handleAddChildren', err);
|
throw new Error('handleAddChildren', err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -62,9 +59,9 @@ const ContentTreeNode = (props: {
|
|||||||
<StyledSpinner>-</StyledSpinner>
|
<StyledSpinner>-</StyledSpinner>
|
||||||
) : props.node.hasChildNodes ? (
|
) : props.node.hasChildNodes ? (
|
||||||
props.node.expand ? (
|
props.node.expand ? (
|
||||||
<a onClick={() => props.removeChildNodes(props.node)}>-</a>
|
<a onClick={() => handleAddChildren(props.node)}>+</a>
|
||||||
) : (
|
) : (
|
||||||
<a onClick={() => handleAddChildren()}>+</a>
|
<a onClick={() => props.removeChildNodes(props.node)}>-</a>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</StyledContentTreeNodeWedge>
|
</StyledContentTreeNodeWedge>
|
||||||
@ -89,7 +86,7 @@ const ContentTreeNode = (props: {
|
|||||||
{props.node.childNodes?.map((node, i) => {
|
{props.node.childNodes?.map((node, i) => {
|
||||||
return (
|
return (
|
||||||
<ContentTreeNode
|
<ContentTreeNode
|
||||||
key={i}
|
key={i.toString()}
|
||||||
node={node}
|
node={node}
|
||||||
depth={props.depth && props.depth + 1}
|
depth={props.depth && props.depth + 1}
|
||||||
addChildNodes={props.addChildNodes}
|
addChildNodes={props.addChildNodes}
|
||||||
|
|||||||
204
src/ContentTreeRoot.tsx
Normal file
204
src/ContentTreeRoot.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
EntryProps,
|
||||||
|
KeyValueMap,
|
||||||
|
Link,
|
||||||
|
PlainClientAPI,
|
||||||
|
} from 'contentful-management';
|
||||||
|
import { PageExtensionSDK } from 'contentful-ui-extensions-sdk';
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import { useImmer } from 'use-immer';
|
||||||
|
|
||||||
|
import { StyledContentTreeTable } from './ContentTree.styled';
|
||||||
|
import ContentTreeNode, { ContentTreeNodeProps } from './ContentTreeNode';
|
||||||
|
import { emptyNodeProps, cfEntriesToNodes } from './ContentTreeUtils';
|
||||||
|
|
||||||
|
export interface ContentTreeRootProps {
|
||||||
|
node: ContentTreeNodeProps;
|
||||||
|
sdkInstance: PageExtensionSDK;
|
||||||
|
cma: PlainClientAPI;
|
||||||
|
nodeContentTypes: string[];
|
||||||
|
titleFields: string[];
|
||||||
|
locales: string[]; // the first is the default locale
|
||||||
|
iconRegistry?: { [index: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContentTreeRoot = (props: ContentTreeRootProps): ReactElement => {
|
||||||
|
const [stLocale] = useState(props.locales[0]);
|
||||||
|
const [stRoot, setStRoot] = useImmer(emptyNodeProps());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.node.id) {
|
||||||
|
loadRootData(props.node).catch((err) => {
|
||||||
|
throw new Error('loadRootData', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.node]);
|
||||||
|
|
||||||
|
const loadRootData = async (
|
||||||
|
rootNode: ContentTreeNodeProps
|
||||||
|
): Promise<void> => {
|
||||||
|
const childEntries = await getContentfulChildEntries(rootNode.id);
|
||||||
|
const childNodes = cfEntriesToNodes(
|
||||||
|
childEntries,
|
||||||
|
props.titleFields,
|
||||||
|
stLocale,
|
||||||
|
props.locales,
|
||||||
|
props.nodeContentTypes,
|
||||||
|
props.iconRegistry,
|
||||||
|
rootNode.id
|
||||||
|
);
|
||||||
|
const nodes = [rootNode, ...childNodes];
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
const newIdPositionMap = nodes.reduce((acc: any, el, i) => {
|
||||||
|
acc[el.id] = i;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
let tree: ContentTreeNodeProps = emptyNodeProps();
|
||||||
|
nodes.forEach((node: ContentTreeNodeProps) => {
|
||||||
|
node.childNodes = [];
|
||||||
|
if (!node.parentId) {
|
||||||
|
tree = node;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentEl = nodes[newIdPositionMap[node.parentId]];
|
||||||
|
if (parentEl) {
|
||||||
|
parentEl.childNodes = [...(parentEl.childNodes ?? []), node];
|
||||||
|
parentEl.expand = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('🌴 tree', tree);
|
||||||
|
setStRoot(tree);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addChildNodes = async (node: ContentTreeNodeProps): Promise<void> => {
|
||||||
|
let childNodes: ContentTreeNodeProps[] = [];
|
||||||
|
const cfChildren = await getContentfulChildEntries(node.id);
|
||||||
|
childNodes = cfEntriesToNodes(
|
||||||
|
cfChildren,
|
||||||
|
props.titleFields,
|
||||||
|
stLocale,
|
||||||
|
props.locales,
|
||||||
|
props.nodeContentTypes,
|
||||||
|
props.iconRegistry,
|
||||||
|
node.id
|
||||||
|
);
|
||||||
|
setStRoot((draft) => {
|
||||||
|
recursiveProcessNodes(
|
||||||
|
node.id,
|
||||||
|
(targetNode) => {
|
||||||
|
targetNode.childNodes = childNodes;
|
||||||
|
targetNode.expand = false;
|
||||||
|
},
|
||||||
|
draft
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const recursiveProcessNodes = (
|
||||||
|
targetNodeId: string,
|
||||||
|
processNode: (node: ContentTreeNodeProps) => void,
|
||||||
|
node: ContentTreeNodeProps
|
||||||
|
): void => {
|
||||||
|
if (node.id === targetNodeId) {
|
||||||
|
processNode(node);
|
||||||
|
}
|
||||||
|
if (node.childNodes != null) {
|
||||||
|
for (const targetNode of node.childNodes) {
|
||||||
|
recursiveProcessNodes(targetNodeId, processNode, targetNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editEntry = async (entryId: string): Promise<void> => {
|
||||||
|
await props.sdkInstance.navigator.openEntry(entryId, { slideIn: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentfulChildEntries = async (
|
||||||
|
parentId: string
|
||||||
|
): Promise<Array<EntryProps<KeyValueMap>>> => {
|
||||||
|
const parentItem = await props.cma.entry.get({ entryId: parentId });
|
||||||
|
const allChildIds: string[] = [];
|
||||||
|
for (const key of Object.keys(parentItem.fields)) {
|
||||||
|
if (props.nodeContentTypes.includes(key)) {
|
||||||
|
const childNodeRefs = parentItem.fields[key][stLocale] as
|
||||||
|
| Link<string>
|
||||||
|
| Array<Link<string>>;
|
||||||
|
if (Array.isArray(childNodeRefs)) {
|
||||||
|
for (const childNodeRef of childNodeRefs) {
|
||||||
|
allChildIds.push(childNodeRef.sys.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allChildIds.push(childNodeRefs.sys.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const allItems: Array<EntryProps<KeyValueMap>> = [];
|
||||||
|
let done = false;
|
||||||
|
let skip = 0;
|
||||||
|
while (!done) {
|
||||||
|
const col = await props.cma.entry.getMany({
|
||||||
|
query: {
|
||||||
|
'sys.id[in]': allChildIds.join(','),
|
||||||
|
skip,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
allItems.push(...col.items);
|
||||||
|
if (allItems.length < col.total) {
|
||||||
|
skip += 100;
|
||||||
|
} else {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cfChildren: Array<EntryProps<KeyValueMap>> = [];
|
||||||
|
const idPositionMap: { [index: string]: number } = allItems.reduce(
|
||||||
|
(acc: any, el, i) => {
|
||||||
|
acc[el.sys.id] = i;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
for (const childId of allChildIds) {
|
||||||
|
if (allItems[idPositionMap[childId]]) {
|
||||||
|
cfChildren.push(allItems[idPositionMap[childId]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfChildren;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeChildNodes = (node: ContentTreeNodeProps): void => {
|
||||||
|
setStRoot((draft) => {
|
||||||
|
recursiveProcessNodes(
|
||||||
|
node.id,
|
||||||
|
(targetNode) => {
|
||||||
|
targetNode.childNodes = [];
|
||||||
|
targetNode.expand = true;
|
||||||
|
},
|
||||||
|
draft
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledContentTreeTable>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Nodes</th>
|
||||||
|
<th>Content Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Last Modified</th>
|
||||||
|
<th>Last Published</th>
|
||||||
|
</tr>
|
||||||
|
<ContentTreeNode
|
||||||
|
node={stRoot}
|
||||||
|
depth={1}
|
||||||
|
addChildNodes={addChildNodes}
|
||||||
|
removeChildNodes={removeChildNodes}
|
||||||
|
editEntry={editEntry}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</StyledContentTreeTable>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
76
src/ContentTreeUtils.tsx
Normal file
76
src/ContentTreeUtils.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { EntryProps, KeyValueMap } from 'contentful-management';
|
||||||
|
import { ContentTreeNodeProps } from './ContentTreeNode';
|
||||||
|
|
||||||
|
export const emptyNodeProps = (): ContentTreeNodeProps => {
|
||||||
|
return { id: '', name: '', expand: false, parentId: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cfEntryHasChildren = (
|
||||||
|
entry: EntryProps<KeyValueMap>,
|
||||||
|
nodeContentTypes: string[],
|
||||||
|
locales: string[]
|
||||||
|
): boolean => {
|
||||||
|
for (const nodeContentType of nodeContentTypes) {
|
||||||
|
for (const locale of locales) {
|
||||||
|
if (entry.fields[nodeContentType]?.[locale]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cfEntryPublishingStatus = (entry: EntryProps<KeyValueMap>): string => {
|
||||||
|
if (!entry.sys.publishedVersion) {
|
||||||
|
return 'draft';
|
||||||
|
}
|
||||||
|
if (entry.sys.version - entry.sys.publishedVersion === 1) {
|
||||||
|
return 'published';
|
||||||
|
}
|
||||||
|
return 'changed';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cfEntriesToNodes = (
|
||||||
|
entries: Array<EntryProps<KeyValueMap>>,
|
||||||
|
titleFields: string[],
|
||||||
|
stLocale: string,
|
||||||
|
locales: string[],
|
||||||
|
nodeContentTypes: string[],
|
||||||
|
iconRegistry?: { [index: string]: string },
|
||||||
|
parentId?: string
|
||||||
|
): ContentTreeNodeProps[] => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const nodeArray: ContentTreeNodeProps[] = [];
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let name = '';
|
||||||
|
for (const titleField of titleFields) {
|
||||||
|
if (entry.fields[titleField]?.[stLocale]) {
|
||||||
|
name = entry.fields[titleField][stLocale];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name === '') {
|
||||||
|
name = entry.sys.id;
|
||||||
|
}
|
||||||
|
const node: ContentTreeNodeProps = {
|
||||||
|
id: entry.sys.id,
|
||||||
|
name,
|
||||||
|
contentType: entry.sys.contentType.sys.id,
|
||||||
|
icon:
|
||||||
|
iconRegistry != null ? iconRegistry[entry.sys.contentType.sys.id] : '',
|
||||||
|
expand: !!parentId,
|
||||||
|
parentId,
|
||||||
|
hasChildNodes: cfEntryHasChildren(entry, nodeContentTypes, locales),
|
||||||
|
publishingStatus: cfEntryPublishingStatus(entry),
|
||||||
|
updatedAt: entry.sys.updatedAt,
|
||||||
|
publishedAt: entry.sys.publishedAt,
|
||||||
|
};
|
||||||
|
nodeArray.push(node);
|
||||||
|
});
|
||||||
|
return nodeArray;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user