From b0fed8a831f2de751de2e579adccee2cc3ad3b30 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Thu, 22 Feb 2024 08:50:26 -0800 Subject: [PATCH] feat: add reference field (#253) --- .changeset/tiny-scissors-destroy.md | 5 + examples/blog/collections/BlogPosts.schema.ts | 12 + examples/blog/root-cms.d.ts | 19 +- packages/create-root/src/root-version.ts | 2 +- .../root-cms/cli/commands/generate-types.ts | 15 +- packages/root-cms/core/schema.ts | 24 +- .../ui/components/DocEditor/DocEditor.tsx | 687 +----------------- .../DocEditor/fields/BooleanField.tsx | 39 + .../DocEditor/fields/DateTimeField.tsx | 59 ++ .../components/DocEditor/fields/FieldProps.ts | 13 + .../components/DocEditor/fields/FileField.tsx | 167 +++++ .../DocEditor/fields/ImageField.tsx | 210 ++++++ .../DocEditor/fields/MultiSelectField.tsx | 53 ++ .../DocEditor/fields/ReferenceField.css | 63 ++ .../DocEditor/fields/ReferenceField.tsx | 171 +++++ .../DocEditor/fields/RichTextField.tsx | 37 + .../DocEditor/fields/SelectField.tsx | 50 ++ .../DocEditor/fields/StringField.tsx | 51 ++ .../DocPickerModal/DocPickerModal.css | 58 ++ .../DocPickerModal/DocPickerModal.tsx | 187 +++++ .../LocalizationModal/LocalizationModal.tsx | 4 + packages/root-cms/ui/hooks/useDocsList.ts | 57 ++ .../pages/CollectionPage/CollectionPage.tsx | 62 +- packages/root-cms/ui/ui.tsx | 26 +- 24 files changed, 1331 insertions(+), 740 deletions(-) create mode 100644 .changeset/tiny-scissors-destroy.md create mode 100644 packages/root-cms/ui/components/DocEditor/fields/BooleanField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/DateTimeField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/FieldProps.ts create mode 100644 packages/root-cms/ui/components/DocEditor/fields/FileField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/ImageField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/MultiSelectField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/ReferenceField.css create mode 100644 packages/root-cms/ui/components/DocEditor/fields/ReferenceField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/RichTextField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/SelectField.tsx create mode 100644 packages/root-cms/ui/components/DocEditor/fields/StringField.tsx create mode 100644 packages/root-cms/ui/components/DocPickerModal/DocPickerModal.css create mode 100644 packages/root-cms/ui/components/DocPickerModal/DocPickerModal.tsx create mode 100644 packages/root-cms/ui/hooks/useDocsList.ts diff --git a/.changeset/tiny-scissors-destroy.md b/.changeset/tiny-scissors-destroy.md new file mode 100644 index 00000000..f469c2d1 --- /dev/null +++ b/.changeset/tiny-scissors-destroy.md @@ -0,0 +1,5 @@ +--- +'@blinkk/root-cms': minor +--- + +feat: add reference field diff --git a/examples/blog/collections/BlogPosts.schema.ts b/examples/blog/collections/BlogPosts.schema.ts index a0748709..daedaaeb 100644 --- a/examples/blog/collections/BlogPosts.schema.ts +++ b/examples/blog/collections/BlogPosts.schema.ts @@ -95,6 +95,18 @@ export default schema.collection({ label: 'Published Date Override', help: 'Override for the "Published" date.' }), + schema.reference({ + id: 'parentPost', + label: 'Parent Post', + help: 'Optional parent post for breadcrumbs.', + collections: ['BlogPosts'], + }), + schema.array({ + id: 'relatedPosts', + label: 'Related Posts', + help: 'Suggest related blog posts to read.', + of: schema.reference({collections: ['BlogPosts', 'BlogPostsSandbox']}), + }), ], }), ], diff --git a/examples/blog/root-cms.d.ts b/examples/blog/root-cms.d.ts index 3bfe8c86..dd4c7d89 100644 --- a/examples/blog/root-cms.d.ts +++ b/examples/blog/root-cms.d.ts @@ -20,11 +20,20 @@ export interface RootCMSRichText { blocks: RootCMSRichTextBlock[]; } +export interface RootCMSReference { + /** The id of the doc, e.g. "Pages/foo-bar". */ + id: string; + /** The collection id of the doc, e.g. "Pages". */ + collection: string; + /** The slug of the doc, e.g. "foo-bar". */ + slug: string; +} + export interface RootCMSDoc { /** The id of the doc, e.g. "Pages/foo-bar". */ id: string; /** The collection id of the doc, e.g. "Pages". */ - collectionId: string; + collection: string; /** The slug of the doc, e.g. "foo-bar". */ slug: string; /** System-level metadata. */ @@ -80,6 +89,10 @@ export interface BlogPostsFields { }; /** Published Date Override. Override for the "Published" date. */ publishedAtOverride?: number; + /** Parent Post. Optional parent post for breadcrumbs. */ + parentPost?: RootCMSReference; + /** Related Posts. Suggest related blog posts to read. */ + relatedPosts?: RootCMSReference[]; }; } @@ -123,6 +136,10 @@ export interface BlogPostsSandboxFields { }; /** Published Date Override. Override for the "Published" date. */ publishedAtOverride?: number; + /** Parent Post. Optional parent post for breadcrumbs. */ + parentPost?: RootCMSReference; + /** Related Posts. Suggest related blog posts to read. */ + relatedPosts?: RootCMSReference[]; }; } diff --git a/packages/create-root/src/root-version.ts b/packages/create-root/src/root-version.ts index d474a53e..41e1ef20 100644 --- a/packages/create-root/src/root-version.ts +++ b/packages/create-root/src/root-version.ts @@ -1,2 +1,2 @@ // This file is autogenerated by update-root-version.mjs. -export const ROOT_VERSION = '1.0.0-rc.24'; +export const ROOT_VERSION = '1.0.0-rc.25'; diff --git a/packages/root-cms/cli/commands/generate-types.ts b/packages/root-cms/cli/commands/generate-types.ts index 37f8dd14..2ff988ee 100644 --- a/packages/root-cms/cli/commands/generate-types.ts +++ b/packages/root-cms/cli/commands/generate-types.ts @@ -49,11 +49,20 @@ export interface RootCMSRichText { blocks: RootCMSRichTextBlock[]; } +export interface RootCMSReference { + /** The id of the doc, e.g. "Pages/foo-bar". */ + id: string; + /** The collection id of the doc, e.g. "Pages". */ + collection: string; + /** The slug of the doc, e.g. "foo-bar". */ + slug: string; +} + export interface RootCMSDoc { /** The id of the doc, e.g. "Pages/foo-bar". */ id: string; /** The collection id of the doc, e.g. "Pages". */ - collectionId: string; + collection: string; /** The slug of the doc, e.g. "foo-bar". */ slug: string; /** System-level metadata. */ @@ -142,6 +151,10 @@ function fieldType(field: Field): dom.Type { const oneofType = dom.create.namedTypeReference('RootCMSOneOf'); return oneofType; } + if (field.type === 'reference') { + const richtextType = dom.create.namedTypeReference('RootCMSReference'); + return richtextType; + } if (field.type === 'richtext') { const richtextType = dom.create.namedTypeReference('RootCMSRichText'); return richtextType; diff --git a/packages/root-cms/core/schema.ts b/packages/root-cms/core/schema.ts index cfa99944..c2b8c983 100644 --- a/packages/root-cms/core/schema.ts +++ b/packages/root-cms/core/schema.ts @@ -176,6 +176,20 @@ export function richtext(field: Omit): RichTextField { return {...field, type: 'richtext'}; } +export type ReferenceField = CommonFieldProps & { + type: 'reference'; + /** List of collection ids the reference can be chosen from. */ + collections?: string[]; + /** Initial collection to show when picking a reference. */ + initialCollection?: string; + /** Label for the button. Defaults to "Select". */ + buttonLabel?: string; +}; + +export function reference(field: Omit): ReferenceField { + return {...field, type: 'reference'}; +} + export type Field = | StringField | NumberField @@ -189,7 +203,8 @@ export type Field = | ObjectField | ArrayField | OneOfField - | RichTextField; + | RichTextField + | ReferenceField; /** * Similar to {@link Field} but with a required `id`. @@ -197,7 +212,12 @@ export type Field = */ export type FieldWithId = Field; -export type ObjectLikeField = ImageField | FileField | ObjectField | OneOfField; +export type ObjectLikeField = + | ImageField + | FileField + | ObjectField + | OneOfField + | ReferenceField; export interface Schema { name: string; diff --git a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx index d65d8ca1..661364e0 100644 --- a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx +++ b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx @@ -1,16 +1,4 @@ -import { - ActionIcon, - Button, - Checkbox, - LoadingOverlay, - Menu, - MultiSelect, - Select, - Textarea, - TextInput, - Tooltip, -} from '@mantine/core'; -import {showNotification} from '@mantine/notifications'; +import {ActionIcon, Button, LoadingOverlay, Menu, Select} from '@mantine/core'; import { IconBraces, IconCircleArrowDown, @@ -18,8 +6,6 @@ import { IconCirclePlus, IconCopy, IconDotsVertical, - IconFileUpload, - IconPhotoUp, IconPlanet, IconRocket, IconRowInsertBottom, @@ -27,9 +13,7 @@ import { IconTrash, IconTriangleFilled, } from '@tabler/icons-preact'; -import {Timestamp} from 'firebase/firestore'; -import {ChangeEvent} from 'preact/compat'; -import {useEffect, useReducer, useRef, useState} from 'preact/hooks'; +import {useEffect, useReducer, useState} from 'preact/hooks'; import {route} from 'preact-router'; import * as schema from '../../../core/schema.js'; @@ -38,8 +22,6 @@ import { SaveState, UseDraftHook, } from '../../hooks/useDraft.js'; -import {joinClassNames} from '../../utils/classes.js'; -import {uploadFileToGCS} from '../../utils/gcs.js'; import {flattenNestedKeys} from '../../utils/objects.js'; import {getPlaceholderKeys, strFormat} from '../../utils/str-format.js'; import { @@ -51,10 +33,16 @@ import {useEditJsonModal} from '../EditJsonModal/EditJsonModal.js'; import {useLocalizationModal} from '../LocalizationModal/LocalizationModal.js'; import {usePublishDocModal} from '../PublishDocModal/PublishDocModal.js'; import './DocEditor.css'; -import { - RichTextData, - RichTextEditor, -} from '../RichTextEditor/RichTextEditor.js'; +import {BooleanField} from './fields/BooleanField.js'; +import {DateTimeField} from './fields/DateTimeField.js'; +import {FieldProps} from './fields/FieldProps.js'; +import {FileField} from './fields/FileField.js'; +import {ImageField} from './fields/ImageField.js'; +import {MultiSelectField} from './fields/MultiSelectField.js'; +import {ReferenceField} from './fields/ReferenceField.js'; +import {RichTextField} from './fields/RichTextField.js'; +import {SelectField} from './fields/SelectField.js'; +import {StringField} from './fields/StringField.js'; interface DocEditorProps { docId: string; @@ -149,17 +137,6 @@ export function DocEditor(props: DocEditorProps) { ); } -interface FieldProps { - collection: schema.Collection; - field: schema.Field; - level?: number; - hideHeader?: boolean; - onChange?: (newValue: any) => void; - shallowKey: string; - deepKey: string; - draft: DraftController; -} - DocEditor.Field = (props: FieldProps) => { const field = props.field; const level = props.level ?? 0; @@ -184,25 +161,27 @@ DocEditor.Field = (props: FieldProps) => { {field.type === 'array' ? ( ) : field.type === 'boolean' ? ( - + ) : field.type === 'datetime' ? ( - + ) : field.type === 'file' ? ( - + ) : field.type === 'image' ? ( - + ) : field.type === 'multiselect' ? ( - + ) : field.type === 'object' ? ( ) : field.type === 'oneof' ? ( + ) : field.type === 'reference' ? ( + ) : field.type === 'richtext' ? ( - + ) : field.type === 'select' ? ( - + ) : field.type === 'string' ? ( - + ) : (
Unknown field type: {field.type} @@ -213,442 +192,6 @@ DocEditor.Field = (props: FieldProps) => { ); }; -DocEditor.StringField = (props: FieldProps) => { - const field = props.field as schema.StringField; - const [value, setValue] = useState(''); - - function onChange(newValue: string) { - setValue(newValue); - props.draft.updateKey(props.deepKey, newValue); - } - - useEffect(() => { - const unsubscribe = props.draft.subscribe( - props.deepKey, - (newValue: string) => { - setValue(newValue); - } - ); - return unsubscribe; - }, []); - - if (field.variant === 'textarea') { - return ( -