diff --git a/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.css b/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.css index e0749e57..e0d3e645 100644 --- a/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.css +++ b/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.css @@ -63,3 +63,4 @@ font-weight: 500; color: #ababab; } + diff --git a/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.tsx b/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.tsx index 333a5023..e086aae3 100644 --- a/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.tsx +++ b/packages/root-cms/ui/components/DocEditor/fields/ReferenceField.tsx @@ -11,6 +11,7 @@ import {notifyErrors} from '../../../utils/notifications.js'; import {getNestedValue} from '../../../utils/objects.js'; import {useDocPickerModal} from '../../DocPickerModal/DocPickerModal.js'; import {FieldProps} from './FieldProps.js'; +import {ReferenceFieldEditorModal} from './ReferenceFieldEditorModal.js'; export interface ReferenceFieldValue { id: string; @@ -34,6 +35,7 @@ export function ReferenceField(props: FieldProps) { } const docPickerModal = useDocPickerModal(); + const [quickEditDocId, setQuickEditDocId] = useState(null); function openDocPicker() { const initialCollection = refId @@ -53,7 +55,10 @@ export function ReferenceField(props: FieldProps) {
{refId ? (
- + setQuickEditDocId(docId)} + />
openDocPicker()}> {field.buttonLabel || 'Select'} + setQuickEditDocId(null)} + />
); } interface ReferencePreviewProps { id: string; + onOpenQuickEdit?: (docId: string) => void; } ReferenceField.Preview = (props: ReferencePreviewProps) => { @@ -103,7 +114,10 @@ ReferenceField.Preview = (props: ReferencePreviewProps) => {
) : previewDoc ? ( - + ) : (
Doc not found: {props.id} (was it deleted?). Select a new doc @@ -114,7 +128,10 @@ ReferenceField.Preview = (props: ReferencePreviewProps) => { ); }; -ReferenceField.DocCard = (props: {doc: any}) => { +ReferenceField.DocCard = (props: { + doc: any; + onOpenQuickEdit?: (docId: string) => void; +}) => { const doc = props.doc; // NOTE(stevenle): older db versions stored the doc id as doc.sys.id. const docId = doc.id || doc.sys?.id || ''; @@ -144,6 +161,19 @@ ReferenceField.DocCard = (props: {doc: any}) => { className="ReferenceField__DocCard" href={`/cms/content/${docId}`} target="_blank" + rel="noopener noreferrer" + onClick={(event) => { + if ( + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return; + } + event.preventDefault(); + props.onOpenQuickEdit?.(docId); + }} >
void; +} + +/** + * Modal for editing a referenced document and applying changes on save. + */ +export function ReferenceFieldEditorModal( + props: ReferenceFieldEditorModalProps +) { + const modalTheme = useModalTheme(); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [initialDoc, setInitialDoc] = useState(null); + const collectionId = props.docId?.split('/')[0] || ''; + const collection = useCollectionSchema(collectionId); + + const controller = useMemo(() => { + if (!initialDoc) { + return null; + } + return new InMemoryDraftDocController(initialDoc, null); + }, [initialDoc]); + + const draftContext: DraftDocContext | null = useMemo(() => { + if (!controller) { + return null; + } + return { + loading: false, + controller: controller as unknown as DraftDocContext['controller'], + }; + }, [controller]); + + const objectField = useMemo(() => { + if (!collection.schema) { + return null; + } + return { + type: 'object', + id: 'fields', + label: 'Fields', + variant: 'inline', + fields: collection.schema.fields, + }; + }, [collection.schema]); + + useEffect(() => { + if (!props.opened || !props.docId) { + return; + } + setLoading(true); + setInitialDoc(null); + void (async () => { + try { + await notifyErrors(async () => { + const loadedDoc = await getDocFromCacheOrFetch(props.docId!); + setInitialDoc(cloneData(loadedDoc || null)); + }); + } finally { + setLoading(false); + } + })(); + }, [props.opened, props.docId]); + + async function onSave() { + if (!props.docId || !controller) { + return; + } + setSaving(true); + await notifyErrors(async () => { + const [collectionId, slug] = props.docId!.split('/'); + const projectId = window.__ROOT_CTX.rootConfig.projectId; + const docRef = doc( + window.firebase.db, + 'Projects', + projectId, + 'Collections', + collectionId, + 'Drafts', + slug + ); + const nextFields = cloneData(controller.getValue('fields') || {}); + await updateDoc(docRef, { + fields: nextFields, + 'sys.modifiedAt': serverTimestamp(), + 'sys.modifiedBy': window.firebase.user.email, + }); + const updatedDoc = { + ...cloneData(initialDoc || {}), + fields: nextFields, + }; + setDocToCache(props.docId!, updatedDoc); + showNotification({ + title: 'Saved changes', + message: `Saved ${props.docId}`, + color: 'green', + }); + props.onClose(); + }); + setSaving(false); + } + + if (!props.docId) { + return null; + } + + return ( + + + + + + {loading ? ( + + + + ) : !objectField || !draftContext ? ( + + Unable to load reference fields. + + ) : ( + + + + )} + + + + + + + ); +} diff --git a/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.css b/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.css index 5cb489ad..73640aa8 100644 --- a/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.css +++ b/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.css @@ -15,10 +15,6 @@ border-bottom: 1px solid var(--color-border); } -.ReferencesField__card__preview { - padding: 0 !important; -} - .ReferencesField__card:first-child { border-top: 1px solid var(--color-border); } @@ -52,3 +48,7 @@ align-items: flex-start; padding: 0 8px; } + +.ReferencesField__card__previewCard { + padding: 0 !important; +} diff --git a/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.tsx b/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.tsx index e546a73f..d9bf592a 100644 --- a/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.tsx +++ b/packages/root-cms/ui/components/DocEditor/fields/ReferencesField.tsx @@ -15,12 +15,14 @@ import {joinClassNames} from '../../../utils/classes.js'; import {useDocPickerModal} from '../../DocPickerModal/DocPickerModal.js'; import {DocPreviewCard} from '../../DocPreviewCard/DocPreviewCard.js'; import {FieldProps} from './FieldProps.js'; +import {ReferenceFieldEditorModal} from './ReferenceFieldEditorModal.js'; import {ReferenceFieldValue} from './ReferenceField.js'; export function ReferencesField(props: FieldProps) { const field = props.field as schema.ReferencesField; const [refIds, setRefIds] = useState([]); const draft = useDraftDoc().controller; + const [quickEditDocId, setQuickEditDocId] = useState(null); function onChange(newIds: string[]) { if (newIds.length) { @@ -119,13 +121,31 @@ export function ReferencesField(props: FieldProps) { >
- + onClickCapture={(event) => { + if ( + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return; + } + event.preventDefault(); + event.stopPropagation(); + setQuickEditDocId(refId); + }} + > + +
openDocPickerModal()}> {field.buttonLabel || 'Select'} + setQuickEditDocId(null)} + />
); } diff --git a/packages/root-cms/ui/components/RichTextEditor/lexical/utils/InMemoryDraftDocController.ts b/packages/root-cms/ui/components/RichTextEditor/lexical/utils/InMemoryDraftDocController.ts index fc042894..2228da7a 100644 --- a/packages/root-cms/ui/components/RichTextEditor/lexical/utils/InMemoryDraftDocController.ts +++ b/packages/root-cms/ui/components/RichTextEditor/lexical/utils/InMemoryDraftDocController.ts @@ -13,9 +13,14 @@ export class InMemoryDraftDocController extends EventListener { collectionId = 'custom-block'; slug = 'custom-block'; - constructor(initialValue: Record, rootKey = 'block') { + constructor( + initialValue: Record, + rootKey: string | null = 'block' + ) { super(); - this.data = {[rootKey]: cloneData(initialValue)}; + this.data = rootKey + ? {[rootKey]: cloneData(initialValue)} + : cloneData(initialValue); } getValue(key: string): any {