From e477f9d5fb17d894bf8660407d6ac028835f888a Mon Sep 17 00:00:00 2001 From: Steven Le <387282+stevenle@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:08:22 -0800 Subject: [PATCH 1/5] feat(cms): add reference field quick edit modal --- .../ui/components/DocEditor/DocEditor.tsx | 3 +- .../DocEditor/fields/ReferenceField.tsx | 12 ++- .../DocEditor/fields/ReferencesField.tsx | 13 +++- .../ReferenceFieldEditorModal.css | 8 ++ .../ReferenceFieldEditorModal.tsx | 76 +++++++++++++++++++ packages/root-cms/ui/hooks/useDraftDoc.tsx | 26 ++++++- packages/root-cms/ui/ui.tsx | 2 + 7 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.css create mode 100644 packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.tsx diff --git a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx index 92771bf3c..132e8fb7e 100644 --- a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx +++ b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx @@ -116,6 +116,7 @@ import {StringField} from './fields/StringField.js'; interface DocEditorProps { docId: string; + hideStatusBar?: boolean; } const COLLECTION_SCHEMA_TYPES_CONTEXT = createContext< @@ -147,7 +148,7 @@ export function DocEditor(props: DocEditorProps) { visible={loading} loaderProps={{color: 'gray', size: 'xl'}} /> - {!loading && ( + {!loading && !props.hideStatusBar && (
+ + referenceFieldEditorModal.open({docId: refId})} + > + + +
+ + + referenceFieldEditorModal.open({docId: refId}) + } + > + + + { + modals.openContextModal(MODAL_ID, { + ...modalTheme, + innerProps: props, + size: 'xl', + overflow: 'inside', + closeOnClickOutside: false, + closeOnEscape: false, + }); + }, + }; +} + +/** + * Modal for quickly editing a referenced doc and persisting changes on save. + */ +export function ReferenceFieldEditorModal( + modalProps: ContextModalProps +) { + const {id, context, innerProps} = modalProps; + return ( +
+ + + context.closeModal(id)} + /> + +
+ ); +} + +ReferenceFieldEditorModal.Footer = (props: {onCancel: () => void}) => { + const draft = useDraftDoc(); + + return ( +
+ + +
+ ); +}; + +ReferenceFieldEditorModal.id = MODAL_ID; diff --git a/packages/root-cms/ui/hooks/useDraftDoc.tsx b/packages/root-cms/ui/hooks/useDraftDoc.tsx index e9720c4f3..880f0d4e3 100644 --- a/packages/root-cms/ui/hooks/useDraftDoc.tsx +++ b/packages/root-cms/ui/hooks/useDraftDoc.tsx @@ -68,6 +68,10 @@ export class DraftDocController extends EventListener { private autolockApplied = false; /** When true, prevents any writes to the DB (e.g. user lacks edit access). */ readOnly = false; + /** When false, draft changes are only written by explicitly calling flush(). */ + autoSave = true; + /** When false, pending updates are discarded when stop()/dispose() is called. */ + flushOnStop = true; started = false; constructor(docId: string) { @@ -144,7 +148,9 @@ export class DraftDocController extends EventListener { if (this.dbUnsubscribe) { this.dbUnsubscribe(); } - this.flush(); + if (this.flushOnStop) { + this.flush(); + } this.started = false; } @@ -196,7 +202,9 @@ export class DraftDocController extends EventListener { this.pendingUpdates.set(key, value); this.store.set(key, value); this.setSaveState(SaveState.UPDATES_PENDING); - this.queueChanges(); + if (this.autoSave) { + this.queueChanges(); + } } /** @@ -219,7 +227,9 @@ export class DraftDocController extends EventListener { } this.store.update(updates); this.setSaveState(SaveState.UPDATES_PENDING); - this.queueChanges(); + if (this.autoSave) { + this.queueChanges(); + } } /** @@ -232,7 +242,9 @@ export class DraftDocController extends EventListener { this.pendingUpdates.set(key, deleteField()); this.store.set(key, undefined); this.setSaveState(SaveState.UPDATES_PENDING); - this.queueChanges(); + if (this.autoSave) { + this.queueChanges(); + } } /** @@ -392,6 +404,10 @@ export interface DraftDocProviderProps { docId: string; /** When true, prevents any writes to the DB (e.g. user lacks edit access). */ readOnly?: boolean; + /** When false, changes are only persisted when `flush()` is called. */ + autoSave?: boolean; + /** When false, pending updates are discarded when unmounting the provider. */ + flushOnStop?: boolean; children?: ComponentChildren; } @@ -415,6 +431,8 @@ export function DraftDocProvider(props: DraftDocProviderProps) { // Set readOnly mode on the controller based on the prop. controller.readOnly = props.readOnly ?? false; + controller.autoSave = props.autoSave ?? true; + controller.flushOnStop = props.flushOnStop ?? true; useEffect(() => { setLoading(true); diff --git a/packages/root-cms/ui/ui.tsx b/packages/root-cms/ui/ui.tsx index 8bc7b29a7..5c6dd820f 100644 --- a/packages/root-cms/ui/ui.tsx +++ b/packages/root-cms/ui/ui.tsx @@ -22,6 +22,7 @@ import {ExportSheetModal} from './components/ExportSheetModal/ExportSheetModal.j import {LocalizationModal} from './components/LocalizationModal/LocalizationModal.js'; import {LockPublishingModal} from './components/LockPublishingModal/LockPublishingModal.js'; import {PublishDocModal} from './components/PublishDocModal/PublishDocModal.js'; +import {ReferenceFieldEditorModal} from './components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.js'; import {ScheduleReleaseModal} from './components/ScheduleReleaseModal/ScheduleReleaseModal.js'; import {VersionHistoryModal} from './components/VersionHistoryModal/VersionHistoryModal.js'; import {FirebaseContext, FirebaseContextObject} from './hooks/useFirebase.js'; @@ -127,6 +128,7 @@ function App() { [LocalizationModal.id]: LocalizationModal, [LockPublishingModal.id]: LockPublishingModal, [PublishDocModal.id]: PublishDocModal, + [ReferenceFieldEditorModal.id]: ReferenceFieldEditorModal, [ScheduleReleaseModal.id]: ScheduleReleaseModal, [VersionHistoryModal.id]: VersionHistoryModal, }} From bd92e5bbbe6ad455865a20b1a93f4258fe86e483 Mon Sep 17 00:00:00 2001 From: Steven Le Date: Wed, 25 Feb 2026 15:58:18 -0800 Subject: [PATCH 2/5] chore: update modal styles --- .../ui/components/DocEditor/DocEditor.tsx | 3 +- .../ReferenceFieldEditorModal.css | 31 +++++++++++++++++-- .../ReferenceFieldEditorModal.tsx | 14 +++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx index 132e8fb7e..76815ac92 100644 --- a/packages/root-cms/ui/components/DocEditor/DocEditor.tsx +++ b/packages/root-cms/ui/components/DocEditor/DocEditor.tsx @@ -115,6 +115,7 @@ import {SelectField} from './fields/SelectField.js'; import {StringField} from './fields/StringField.js'; interface DocEditorProps { + className?: string; docId: string; hideStatusBar?: boolean; } @@ -143,7 +144,7 @@ export function DocEditor(props: DocEditorProps) { value={collection?.schema?.types || {}} > -
+
{ - modals.openContextModal(MODAL_ID, { + modals.openContextModal(MODAL_ID, { ...modalTheme, + className: 'ReferenceFieldEditorModalWrap', innerProps: props, + title: `Edit ${props.docId}`, size: 'xl', overflow: 'inside', closeOnClickOutside: false, @@ -43,7 +46,11 @@ export function ReferenceFieldEditorModal( autoSave={false} flushOnStop={false} > - + context.closeModal(id)} /> @@ -57,11 +64,12 @@ ReferenceFieldEditorModal.Footer = (props: {onCancel: () => void}) => { return (
- +
+ ); +}; + ReferenceFieldEditorModal.Footer = (props: {onCancel: () => void}) => { const draft = useDraftDoc(); From a97982a64303671f81ae5d4e9968f29935776c6f Mon Sep 17 00:00:00 2001 From: Steven Le Date: Wed, 25 Feb 2026 18:07:58 -0800 Subject: [PATCH 5/5] chore: move new tab button to bottom left --- .../ReferenceFieldEditorModal.css | 15 +----- .../ReferenceFieldEditorModal.tsx | 49 ++++++++----------- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.css b/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.css index 339a8d537..79836b889 100644 --- a/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.css +++ b/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.css @@ -32,17 +32,6 @@ padding: 12px 20px; } -.ReferenceFieldEditorModal__header { - display: flex; - align-items: center; - gap: 12px; -} - -.ReferenceFieldEditorModal__header__title a { - text-decoration: none; -} - -.ReferenceFieldEditorModal__header__title a:hover { - text-decoration: underline; - text-underline-offset: 4px; +.ReferenceFieldEditorModal__footer__start { + flex: 1; } diff --git a/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.tsx b/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.tsx index a8ca7dc81..3bf10b1c8 100644 --- a/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.tsx +++ b/packages/root-cms/ui/components/ReferenceFieldEditorModal/ReferenceFieldEditorModal.tsx @@ -23,7 +23,7 @@ export function useReferenceFieldEditorModal() { ...modalTheme, className: 'ReferenceFieldEditorModalWrap', innerProps: props, - title: , + title: `Edit ${props.docId}`, size: 'xl', overflow: 'inside', closeOnClickOutside: false, @@ -40,19 +40,17 @@ export function ReferenceFieldEditorModal( modalProps: ContextModalProps ) { const {id, context, innerProps} = modalProps; + const docId = innerProps.docId; return (
- + context.closeModal(id)} /> @@ -60,33 +58,26 @@ export function ReferenceFieldEditorModal( ); } -ReferenceFieldEditorModal.Header = (props: {docId: string}) => { - const docUrl = `/cms/content/${props.docId}`; - return ( -
-
- Edit {props.docId} -
- -
- ); -}; - -ReferenceFieldEditorModal.Footer = (props: {onCancel: () => void}) => { +ReferenceFieldEditorModal.Footer = (props: { + docId: string; + onCancel: () => void; +}) => { const draft = useDraftDoc(); return (
+
+ +