refactor: move getTreeData to utility file and add tests

This commit is contained in:
Daniel Lautzenheiser 2023-04-18 10:57:01 -04:00
parent 651bafe5e0
commit f3fccb1137
3 changed files with 381 additions and 103 deletions

View File

@ -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<string, string | undefined>);
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<string, string>),
};
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<string, BaseTreeNodeData[]>);
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<TreeNodeData[]>(getTreeData(collection, entries));
console.log('TREE DATA', treeData, collection, entries);
const [selected, setSelected] = useState<TreeNodeData | null>(null);
const [useFilter, setUseFilter] = useState(true);

View File

@ -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);
});
});
});

View File

@ -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<EF extends BaseField>(
collection: Collection<EF>,
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<string, string | undefined>);
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<string, string>),
};
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<string, BaseTreeNodeData[]>);
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;
}