Last active
January 29, 2024 04:25
-
-
Save DrMint/f6c9efb13292c56dfce2db545515536f to your computer and use it in GitHub Desktop.
Generating finite tree-like block structures for Payload CMS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { BlocksConfig, generateBlocks } from "./recursiveBlocks"; | |
// First, we define all your blocks name | |
const enum BlockName { | |
Text = "Text", | |
Section = "Section", | |
} | |
// Then, the configuration. | |
const blocksConfig: BlocksConfig<BlockName> = { | |
Text: { | |
root: true, | |
block: { | |
slug: "textBlock", | |
interfaceName: "TextBlock", | |
fields: [ | |
{ | |
name: "content", | |
type: "textarea", | |
}, | |
], | |
}, | |
}, | |
Section: { | |
root: true, | |
block: { | |
slug: "section", | |
labels: { singular: "Section", plural: "Sections" }, | |
fields: [{ name: "title", type: "text" }], | |
recursion: { | |
name: "content", | |
condition: (depth) => depth < 5, | |
newDepth: (depth) => depth + 1, | |
blocks: [BlockName.Section, BlockName.Text], | |
}, | |
}, | |
}, | |
}; | |
// Finally we generate the recursive (but with finite depth) block | |
export const contentBlocks = generateBlocks(blocksConfig); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Block, BlockField } from "payload/types"; | |
const isDefined = <T>(value: T | null | undefined): value is T => | |
value !== null && value !== undefined; | |
const capitalize = (string: string): string => { | |
const [firstLetter, ...otherLetters] = string; | |
if (!isDefined(firstLetter)) return ""; | |
return [firstLetter.toUpperCase(), ...otherLetters].join(""); | |
}; | |
const recursionFieldName = "recursion" as const; | |
type BlockConfig<T extends string> = { | |
root: boolean; | |
block: RecursiveBlock<T> | Block; | |
}; | |
type RecursiveBlock<T extends string> = Omit<Block, "fields" | "interfaceName"> & { | |
[recursionFieldName]: Omit<BlockField, "blocks" | "type"> & { | |
newDepth: (currentDepth: number) => number; | |
condition: (currentDepth: number, parents: T[]) => boolean; | |
blocks: T[]; | |
}; | |
fields?: Block["fields"]; | |
}; | |
export type BlocksConfig<T extends string> = Record<T, BlockConfig<T>>; | |
export const generateBlocks = <T extends string>(blocksConfig: BlocksConfig<T>): Block[] => { | |
const isRecursiveBlock = (block: RecursiveBlock<T> | Block): block is RecursiveBlock<T> => | |
recursionFieldName in block; | |
const getInterfaceName = (parents: T[], currentBlockName: T): string => { | |
return [...parents, currentBlockName] | |
.map((blockName) => blocksConfig[blockName].block.slug) | |
.map(capitalize) | |
.join("_"); | |
}; | |
const getCurrentDepth = (parents: T[]): number => | |
parents.reduce((acc, blockName) => { | |
const block = blocksConfig[blockName].block; | |
if (!isRecursiveBlock(block)) return acc; | |
return block[recursionFieldName].newDepth(acc); | |
}, 1); | |
const generateRecursiveBlocks = (parents: T[], blockName: T): Block | undefined => { | |
const currentDepth = getCurrentDepth(parents); | |
const block = blocksConfig[blockName].block; | |
if (!isRecursiveBlock(block)) return block; | |
const { | |
slug, | |
labels, | |
fields = [], | |
recursion: { newDepth, blocks, condition, ...fieldsProps }, | |
} = block; | |
const generatedBlocks = blocks | |
.filter((blockName) => { | |
const block = blocksConfig[blockName].block; | |
if (!isRecursiveBlock(block)) return true; | |
return block[recursionFieldName].condition(currentDepth, parents); | |
}) | |
.map((nextBlockName) => generateRecursiveBlocks([...parents, blockName], nextBlockName)) | |
.filter(isDefined); | |
// Cut dead branches (branches without leafs) | |
if (generatedBlocks.length === 0) { | |
return undefined; | |
} | |
return { | |
slug, | |
interfaceName: getInterfaceName(parents, blockName), | |
labels, | |
fields: [ | |
...fields, | |
{ | |
...fieldsProps, | |
type: "blocks", | |
blocks: generatedBlocks, | |
}, | |
], | |
}; | |
}; | |
const rootBlockNames = Object.entries<BlockConfig<T>>(blocksConfig) | |
.filter(([_, blockConfig]) => blockConfig.root) | |
.map(([blockName]) => blockName as T); | |
return rootBlockNames | |
.map((blockName) => generateRecursiveBlocks([], blockName)) | |
.filter(isDefined); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment