diff --git a/package.json b/package.json index f91fc92..e82fcec 100644 --- a/package.json +++ b/package.json @@ -17,38 +17,32 @@ "singleQuote": true }, "devDependencies": { - "@types/react": "^18.0.20", - "@types/styled-components": "^5.1.26", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.38.0", - "contentful-management": "^10.6.3", - "contentful-ui-extensions-sdk": "^4.8.1", - "eslint": "^8.0.1", - "eslint-config-prettier": "^8.5.0", - "eslint-config-standard-with-typescript": "^23.0.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-json": "3.1.0", - "eslint-plugin-n": "^15.0.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-react": "^7.31.8", - "immer": "^9.0.15", - "install-peers": "^1.0.4", - "prettier": "2.7.1", - "react": "18.2.0", - "styled-components": "^5.3.5", - "typescript": "*", - "use-immer": "^0.7.0" - }, - "peerDependencies": { - "install-peers": "^1.0.4" - }, - "dependencies": { - "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" - } + "@types/react": "^18.0.20", + "@types/styled-components": "^5.1.26", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.38.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.5.0", + "eslint-config-standard-with-typescript": "^23.0.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-json": "3.1.0", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.31.8", + "install-peers": "^1.0.4", + "prettier": "2.7.1", + "typescript": "*" + }, + "peerDependencies": { + "install-peers": "^1.0.4" + }, + "dependencies": { + "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" + } } diff --git a/src/ContentTree.styled.tsx b/src/ContentTree.styled.tsx index d210a29..b0a97a0 100644 --- a/src/ContentTree.styled.tsx +++ b/src/ContentTree.styled.tsx @@ -6,8 +6,10 @@ export interface UiPalette { } export const StyledContentTreeTable = styled.table` + width: 100%; color: black; border: 0; + padding: 0 60px; margin: 0 auto; td { padding: 0.2em 1em 0.2em 0.2em; diff --git a/src/ContentTree.tsx b/src/ContentTree.tsx index e79afa6..ca14d08 100644 --- a/src/ContentTree.tsx +++ b/src/ContentTree.tsx @@ -1,15 +1,9 @@ -import { - EntryProps, - KeyValueMap, - Link, - PlainClientAPI, -} from 'contentful-management'; +import { 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 { ContentTreeRoot } from './ContentTreeRoot'; +import { emptyNodeProps, cfEntriesToNodes } from './ContentTreeUtils'; export interface ContentTreeProps { sdkInstance: PageExtensionSDK; @@ -21,250 +15,47 @@ export interface ContentTreeProps { iconRegistry?: { [index: string]: string }; } -const emptyNodeProps = (): ContentTreeNodeProps => { - return { id: '', name: '', expand: false, parentId: '' }; -}; - export const ContentTree = (props: ContentTreeProps): ReactElement => { const [stLocale] = useState(props.locales[0]); - const [stRoot, setStRoot] = useImmer(emptyNodeProps()); - - // EFFECTS + const [rootNodes, setRootNodes] = useImmer([emptyNodeProps()]); useEffect(() => { if (props.sdkInstance) { - loadRootData().catch((err) => { + loadData().catch((err) => { throw new Error('loadRootData', err); }); } }, [props.sdkInstance]); - // FUNCTIONS - - const addChildNodes = async (node: ContentTreeNodeProps): Promise => { - 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 => { - await props.sdkInstance.navigator.openEntry(entryId, { slideIn: true }); - }; - - const getContentfulChildEntries = async ( - parentId: string - ): Promise>> => { - 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 - | Array>; - if (Array.isArray(childNodeRefs)) { - for (const childNodeRef of childNodeRefs) { - allChildIds.push(childNodeRef.sys.id); - } - } else { - allChildIds.push(childNodeRefs.sys.id); - } - } - } - const allItems: Array> = []; - 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> = []; - 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>, - 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): 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): string => { - if (!entry.sys.publishedVersion) { - return 'draft'; - } - if (entry.sys.version - entry.sys.publishedVersion === 1) { - return 'published'; - } - return 'changed'; - }; - - const loadRootData = async (): Promise => { + const loadData = async (): Promise => { const CfRootData = await props.cma.entry.getMany({ query: { content_type: props.rootType }, }); - const rootNodes = cfEntriesToNodes(CfRootData.items); - for (const rootNode of rootNodes) { - const childEntries = await getContentfulChildEntries(rootNode.id); - const childNodes = cfEntriesToNodes(childEntries, 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 = true; - } - }); - console.log('🌴 tree', tree); - setStRoot(tree); - } - } + const nodes = cfEntriesToNodes( + CfRootData.items, + props.titleFields, + stLocale, + props.locales, + props.nodeContentTypes, + props.iconRegistry + ); + setRootNodes(nodes); }; - 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 ( <> - - - - Nodes - Content Type - Status - Last Modified - Last Published - - - - + {rootNodes.map((node, i) => ( + + ))} ); }; diff --git a/src/ContentTreeNode.tsx b/src/ContentTreeNode.tsx index 492fa02..e5d1b1e 100644 --- a/src/ContentTreeNode.tsx +++ b/src/ContentTreeNode.tsx @@ -31,11 +31,8 @@ const ContentTreeNode = (props: { editEntry: (nodeId: string) => Promise; }): ReactElement => { const [loading, setLoading] = useState(false); - const [node] = useState(props.node); - // React.useEffect(()=>{setLoading(false)},[props.node.childNodes]) - - const addChildren = async (): Promise => { + const addChildren = async (node: ContentTreeNodeProps): Promise => { setLoading(true); await props.addChildNodes(node); setLoading(false); @@ -47,8 +44,8 @@ const ContentTreeNode = (props: { }); }; - const handleAddChildren = (): void => { - addChildren().catch((err) => { + const handleAddChildren = (node: ContentTreeNodeProps): void => { + addChildren(node).catch((err) => { throw new Error('handleAddChildren', err); }); }; @@ -62,9 +59,9 @@ const ContentTreeNode = (props: { - ) : props.node.hasChildNodes ? ( props.node.expand ? ( - props.removeChildNodes(props.node)}>- + handleAddChildren(props.node)}>+ ) : ( - handleAddChildren()}>+ + props.removeChildNodes(props.node)}>- ) ) : null} @@ -89,7 +86,7 @@ const ContentTreeNode = (props: { {props.node.childNodes?.map((node, i) => { return ( { + 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 => { + 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 => { + 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 => { + await props.sdkInstance.navigator.openEntry(entryId, { slideIn: true }); + }; + + const getContentfulChildEntries = async ( + parentId: string + ): Promise>> => { + 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 + | Array>; + if (Array.isArray(childNodeRefs)) { + for (const childNodeRef of childNodeRefs) { + allChildIds.push(childNodeRef.sys.id); + } + } else { + allChildIds.push(childNodeRefs.sys.id); + } + } + } + const allItems: Array> = []; + 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> = []; + 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 ( + <> + + + + Nodes + Content Type + Status + Last Modified + Last Published + + + + + + ); +}; diff --git a/src/ContentTreeUtils.tsx b/src/ContentTreeUtils.tsx new file mode 100644 index 0000000..01d79de --- /dev/null +++ b/src/ContentTreeUtils.tsx @@ -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, + 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): string => { + if (!entry.sys.publishedVersion) { + return 'draft'; + } + if (entry.sys.version - entry.sys.publishedVersion === 1) { + return 'published'; + } + return 'changed'; +}; + +export const cfEntriesToNodes = ( + entries: Array>, + 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; +};