diff --git a/packages/core/src/components/collections/NestedCollection.tsx b/packages/core/src/components/collections/NestedCollection.tsx index c6dfce0a..d0744ea6 100644 --- a/packages/core/src/components/collections/NestedCollection.tsx +++ b/packages/core/src/components/collections/NestedCollection.tsx @@ -1,33 +1,16 @@ import { Article as ArticleIcon } from '@styled-icons/material/Article'; import { ChevronRight as ChevronRightIcon } from '@styled-icons/material/ChevronRight'; import sortBy from 'lodash/sortBy'; -import { dirname, sep } from 'path'; import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import useEntries from '@staticcms/core/lib/hooks/useEntries'; import classNames from '@staticcms/core/lib/util/classNames.util'; -import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util'; -import { stringTemplate } from '@staticcms/core/lib/widgets'; +import { getTreeData } from '@staticcms/core/lib/util/nested.util'; import NavLink from '../navbar/NavLink'; import type { Collection, Entry } from '@staticcms/core/interface'; - -const { addFileTemplateFields } = stringTemplate; - -interface BaseTreeNodeData { - title: string | undefined; - path: string; - isDir: boolean; - isRoot: boolean; - expanded?: boolean; -} - -type SingleTreeNodeData = BaseTreeNodeData | (Entry & BaseTreeNodeData); - -type TreeNodeData = SingleTreeNodeData & { - children: TreeNodeData[]; -}; +import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util'; function getNodeTitle(node: TreeNodeData) { const title = node.isRoot @@ -118,88 +101,6 @@ export function walk(treeData: TreeNodeData[], callback: (node: TreeNodeData) => return traverse(treeData); } -export function getTreeData(collection: Collection, entries: Entry[]): TreeNodeData[] { - const collectionFolder = 'folder' in collection ? collection.folder : ''; - const rootFolder = '/'; - const entriesObj = entries.map(e => ({ ...e, path: e.path.slice(collectionFolder.length) })); - - const dirs = entriesObj.reduce((acc, entry) => { - let dir: string | undefined = dirname(entry.path); - while (dir && !acc[dir] && dir !== rootFolder) { - const parts: string[] = dir.split(sep); - acc[dir] = parts.pop(); - dir = parts.length ? parts.join(sep) : undefined; - } - return acc; - }, {} as Record); - - if ('nested' in collection && collection.nested?.summary) { - collection = { - ...collection, - summary: collection.nested.summary, - }; - } else { - collection = { - ...collection, - }; - delete collection.summary; - } - - const flatData = [ - { - title: collection.label, - path: rootFolder, - isDir: true, - isRoot: true, - }, - ...Object.entries(dirs).map(([key, value]) => ({ - title: value, - path: key, - isDir: true, - isRoot: false, - })), - ...entriesObj.map((e, index) => { - let entry = entries[index]; - entry = { - ...entry, - data: addFileTemplateFields(entry.path, entry.data as Record), - }; - const title = selectEntryCollectionTitle(collection, entry); - return { - ...e, - title, - isDir: false, - isRoot: false, - }; - }), - ]; - - const parentsToChildren = flatData.reduce((acc, node) => { - const parent = node.path === rootFolder ? '' : dirname(node.path); - if (acc[parent]) { - acc[parent].push(node); - } else { - acc[parent] = [node]; - } - return acc; - }, {} as Record); - - function reducer(acc: TreeNodeData[], value: BaseTreeNodeData) { - const node = value; - let children: TreeNodeData[] = []; - if (parentsToChildren[node.path]) { - children = parentsToChildren[node.path].reduce(reducer, []); - } - - acc.push({ ...node, children }); - return acc; - } - - const treeData = parentsToChildren[''].reduce(reducer, []); - - return treeData; -} - export function updateNode( treeData: TreeNodeData[], node: TreeNodeData, @@ -234,6 +135,7 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) => const entries = useEntries(collection); const [treeData, setTreeData] = useState(getTreeData(collection, entries)); + console.log('TREE DATA', treeData, collection, entries); const [selected, setSelected] = useState(null); const [useFilter, setUseFilter] = useState(true); diff --git a/packages/core/src/lib/util/__tests__/nested.util.spec.ts b/packages/core/src/lib/util/__tests__/nested.util.spec.ts new file mode 100644 index 00000000..16677ffb --- /dev/null +++ b/packages/core/src/lib/util/__tests__/nested.util.spec.ts @@ -0,0 +1,274 @@ +import { getTreeData } from '../nested.util'; + +import type { Collection, Entry } from '@staticcms/core/interface'; + +const collection: Collection = { + create: true, + fields: [ + { label: 'Title', name: 'title', widget: 'string' }, + { + label: 'Body', + media_library: { folder_support: true }, + name: 'body', + widget: 'markdown', + }, + ], + folder: '_nested_pages', + label: 'Nested Pages', + label_singular: 'Page', + media_library: { folder_support: true }, + name: 'pages', + nested: { depth: 100, summary: '{{title}}', path: { label: 'Path', index_file: 'index' } }, + sortable_fields: { fields: Array(1) }, + view_filters: [], + view_groups: [], +}; + +const entries: Entry[] = [ + { + author: '', + collection: 'pages', + data: { title: 'An Author', body: 'Author details go here!.\n' }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/authors/author-1/index.md', + raw: '---\ntitle: An Author\n---\nAuthor details go here!.\n', + slug: 'authors/author-1/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { title: 'Authors' }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/authors/index.md', + raw: '---\ntitle: Authors\n---\n', + slug: 'authors/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { + title: 'Hello World', + body: 'Coffee is a small tree or shrub that grows in the … great number of migratory and resident species.\n', + }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/posts/hello-world/index.md', + raw: '---\ntitle: Hello World\n---\nCoffee is a small tree or shrub that grows in the forest understory in its wild form, and traditionally was grown commercially under other trees that provided shade. The forest-like structure of shade coffee farms provides habitat for a great number of migratory and resident species.\n', + slug: 'posts/hello-world/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { title: 'Posts' }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/posts/index.md', + raw: '---\ntitle: Posts\n---\n', + slug: 'posts/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { + title: 'Hello World News', + body: 'Coffee is a small tree or shrub that grows in the … great number of migratory and resident species.\n', + }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/posts/news/hello-world-news/index.md', + raw: '---\ntitle: Hello World News\n---\nCoffee is a small tree or shrub that grows in the forest understory in its wild form, and traditionally was grown commercially under other trees that provided shade. The forest-like structure of shade coffee farms provides habitat for a great number of migratory and resident species.\n', + slug: 'posts/news/hello-world-news/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { title: 'News Articles' }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/posts/news/index.md', + raw: '---\ntitle: News Articles\n---\n', + slug: 'posts/news/index', + status: '', + updatedOn: '', + }, + { + author: '', + collection: 'pages', + data: { title: 'Pages' }, + i18n: {}, + isFetching: false, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '_nested_pages/index.md', + raw: '---\ntitle: Pages\n---\n', + slug: 'index', + status: '', + updatedOn: '', + }, +]; + +describe('nested.util', () => { + describe('getTreeData', () => { + it('returns root if entries are empty', () => { + expect(getTreeData(collection, [])).toEqual([ + { children: [], isDir: true, isRoot: true, path: '/', title: 'Nested Pages' }, + ]); + }); + + it('returns nested structure', () => { + const treeData = getTreeData(collection, entries); + + // ROOT + expect(treeData.length).toBe(1); + + const rootNode = treeData[0]; + expect(rootNode.isDir).toBe(true); + expect(rootNode.isRoot).toBe(true); + expect(rootNode.path).toBe('/'); + expect(rootNode.title).toBe('Nested Pages'); + expect(rootNode.children.length).toBe(3); + + // ROOT > index.md + const rootIndexNode = rootNode.children[2]; + expect(rootIndexNode.isDir).toBe(false); + expect(rootIndexNode.isRoot).toBe(false); + expect(rootIndexNode.path).toBe('/index.md'); + expect(rootIndexNode.title).toBe('Pages'); + expect(rootIndexNode.children.length).toBe(0); + + // ROOT > Authors Node + const authorsNode = rootNode.children[0]; + expect(authorsNode.isDir).toBe(true); + expect(authorsNode.isRoot).toBe(false); + expect(authorsNode.path).toBe('/authors'); + expect(authorsNode.title).toBe('/authors'); + expect(authorsNode.children.length).toBe(2); + + // ROOT > Authors Node > index.md + const authorsIndexNode = authorsNode.children[1]; + expect(authorsIndexNode.isDir).toBe(false); + expect(authorsIndexNode.isRoot).toBe(false); + expect(authorsIndexNode.path).toBe('/authors/index.md'); + expect(authorsIndexNode.title).toBe('Authors'); + expect(authorsIndexNode.children.length).toBe(0); + + // ROOT > Authors Node > An Author Node + const anAuthorNode = authorsNode.children[0]; + expect(anAuthorNode.isDir).toBe(true); + expect(anAuthorNode.isRoot).toBe(false); + expect(anAuthorNode.path).toBe('/authors/author-1'); + expect(anAuthorNode.title).toBe('/authors/author-1'); + expect(anAuthorNode.children.length).toBe(1); + + // ROOT > Authors Node > An Author Node > index.md + const anAuthorIndexNode = anAuthorNode.children[0]; + expect(anAuthorIndexNode.isDir).toBe(false); + expect(anAuthorIndexNode.isRoot).toBe(false); + expect(anAuthorIndexNode.path).toBe('/authors/author-1/index.md'); + expect(anAuthorIndexNode.title).toBe('An Author'); + expect(anAuthorIndexNode.children.length).toBe(0); + + // ROOT > Posts Node + const postsNode = rootNode.children[1]; + expect(postsNode.isDir).toBe(true); + expect(postsNode.isRoot).toBe(false); + expect(postsNode.path).toBe('/posts'); + expect(postsNode.title).toBe('/posts'); + expect(postsNode.children.length).toBe(3); + + // ROOT > Posts Node > index.md + const postsIndexNode = postsNode.children[2]; + expect(postsIndexNode.isDir).toBe(false); + expect(postsIndexNode.isRoot).toBe(false); + expect(postsIndexNode.path).toBe('/posts/index.md'); + expect(postsIndexNode.title).toBe('Posts'); + expect(postsIndexNode.children.length).toBe(0); + + // ROOT > Posts Node > Hello World Node + const helloWorldNode = postsNode.children[0]; + expect(helloWorldNode.isDir).toBe(true); + expect(helloWorldNode.isRoot).toBe(false); + expect(helloWorldNode.path).toBe('/posts/hello-world'); + expect(helloWorldNode.title).toBe('/posts/hello-world'); + expect(helloWorldNode.children.length).toBe(1); + + // ROOT > Posts Node > Hello World Node > index.md + const helloWorldIndexNode = helloWorldNode.children[0]; + expect(helloWorldIndexNode.isDir).toBe(false); + expect(helloWorldIndexNode.isRoot).toBe(false); + expect(helloWorldIndexNode.path).toBe('/posts/hello-world/index.md'); + expect(helloWorldIndexNode.title).toBe('Hello World'); + expect(helloWorldIndexNode.children.length).toBe(0); + + // ROOT > Posts Node > News Articles Node + const newsArticlesNode = postsNode.children[1]; + expect(newsArticlesNode.isDir).toBe(true); + expect(newsArticlesNode.isRoot).toBe(false); + expect(newsArticlesNode.path).toBe('/posts/news'); + expect(newsArticlesNode.title).toBe('/posts/news'); + expect(newsArticlesNode.children.length).toBe(2); + + // ROOT > Posts Node > Hello World Node > index.md + const newsArticlesIndexNode = newsArticlesNode.children[1]; + expect(newsArticlesIndexNode.isDir).toBe(false); + expect(newsArticlesIndexNode.isRoot).toBe(false); + expect(newsArticlesIndexNode.path).toBe('/posts/news/index.md'); + expect(newsArticlesIndexNode.title).toBe('News Articles'); + expect(newsArticlesIndexNode.children.length).toBe(0); + + // ROOT > Posts Node > News Articles Node > Hello World News Node + const helloWorldNewsNode = newsArticlesNode.children[0]; + expect(helloWorldNewsNode.isDir).toBe(true); + expect(helloWorldNewsNode.isRoot).toBe(false); + expect(helloWorldNewsNode.path).toBe('/posts/news/hello-world-news'); + expect(helloWorldNewsNode.title).toBe('/posts/news/hello-world-news'); + expect(helloWorldNewsNode.children.length).toBe(1); + + // ROOT > Posts Node > Hello World Node > Hello World News Node > index.md + const helloWorldNewsIndexNode = helloWorldNewsNode.children[0]; + expect(helloWorldNewsIndexNode.isDir).toBe(false); + expect(helloWorldNewsIndexNode.isRoot).toBe(false); + expect(helloWorldNewsIndexNode.path).toBe('/posts/news/hello-world-news/index.md'); + expect(helloWorldNewsIndexNode.title).toBe('Hello World News'); + expect(helloWorldNewsIndexNode.children.length).toBe(0); + }); + }); +}); diff --git a/packages/core/src/lib/util/nested.util.ts b/packages/core/src/lib/util/nested.util.ts index 2bcd6d58..adae1109 100644 --- a/packages/core/src/lib/util/nested.util.ts +++ b/packages/core/src/lib/util/nested.util.ts @@ -1,11 +1,28 @@ import trim from 'lodash/trim'; -import { basename, dirname, extname, join } from 'path'; +import { basename, dirname, extname, join, sep } from 'path'; import { sanitizeSlug } from '../urlHelper'; import { selectEntryCollectionTitle, selectFolderEntryExtension } from './collection.util'; import { isEmpty, isNotEmpty } from './string.util'; +import { stringTemplate } from '../widgets'; -import type { Collection, Config, Entry } from '@staticcms/core/interface'; +import type { BaseField, Collection, Config, Entry } from '@staticcms/core/interface'; + +const { addFileTemplateFields } = stringTemplate; + +interface BaseTreeNodeData { + title: string | undefined; + path: string; + isDir: boolean; + isRoot: boolean; + expanded?: boolean; +} + +export type SingleTreeNodeData = BaseTreeNodeData | (Entry & BaseTreeNodeData); + +export type TreeNodeData = SingleTreeNodeData & { + children: TreeNodeData[]; +}; export function selectCustomPath( entry: Entry, @@ -75,3 +92,88 @@ export function getNestedSlug( return ''; } + +export function getTreeData( + collection: Collection, + entries: Entry[], +): TreeNodeData[] { + const collectionFolder = 'folder' in collection ? collection.folder : ''; + const rootFolder = '/'; + const entriesObj = entries.map(e => ({ ...e, path: e.path.slice(collectionFolder.length) })); + + const dirs = entriesObj.reduce((acc, entry) => { + let dir: string | undefined = dirname(entry.path); + while (dir && !acc[dir] && dir !== rootFolder) { + const parts: string[] = dir.split(sep); + acc[dir] = parts.pop(); + dir = parts.length ? parts.join(sep) : undefined; + } + return acc; + }, {} as Record); + + if ('nested' in collection && collection.nested?.summary) { + collection = { + ...collection, + summary: collection.nested.summary, + }; + } else { + collection = { + ...collection, + }; + delete collection.summary; + } + + const flatData = [ + { + title: collection.label, + path: rootFolder, + isDir: true, + isRoot: true, + }, + ...Object.entries(dirs).map(([key, value]) => ({ + title: value, + path: key, + isDir: true, + isRoot: false, + })), + ...entriesObj.map((e, index) => { + let entry = entries[index]; + entry = { + ...entry, + data: addFileTemplateFields(entry.path, entry.data as Record), + }; + const title = selectEntryCollectionTitle(collection, entry); + return { + ...e, + title, + isDir: false, + isRoot: false, + }; + }), + ]; + + const parentsToChildren = flatData.reduce((acc, node) => { + const parent = node.path === rootFolder ? '' : dirname(node.path); + if (acc[parent]) { + acc[parent].push(node); + } else { + acc[parent] = [node]; + } + return acc; + }, {} as Record); + + function reducer(acc: TreeNodeData[], value: BaseTreeNodeData) { + const node = value; + let children: TreeNodeData[] = []; + if (parentsToChildren[node.path]) { + children = parentsToChildren[node.path].reduce(reducer, []); + } + + acc.push({ ...node, children }); + return acc; + } + + const treeData = parentsToChildren[''].reduce(reducer, []); + + return treeData; +}