Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@
font-weight: 500;
color: #ababab;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,7 @@ export function ReferenceField(props: FieldProps) {
}

const docPickerModal = useDocPickerModal();
const [quickEditDocId, setQuickEditDocId] = useState<string | null>(null);

function openDocPicker() {
const initialCollection = refId
Expand All @@ -53,7 +55,10 @@ export function ReferenceField(props: FieldProps) {
<div className="ReferenceField">
{refId ? (
<div className="ReferenceField__ref">
<ReferenceField.Preview id={refId} />
<ReferenceField.Preview
id={refId}
onOpenQuickEdit={(docId) => setQuickEditDocId(docId)}
/>
<div className="ReferenceField__remove">
<Tooltip label="Remove">
<ActionIcon
Expand All @@ -71,12 +76,18 @@ export function ReferenceField(props: FieldProps) {
<Button color="dark" size="xs" onClick={() => openDocPicker()}>
{field.buttonLabel || 'Select'}
</Button>
<ReferenceFieldEditorModal
docId={quickEditDocId}
opened={!!quickEditDocId}
onClose={() => setQuickEditDocId(null)}
/>
</div>
);
}

interface ReferencePreviewProps {
id: string;
onOpenQuickEdit?: (docId: string) => void;
}

ReferenceField.Preview = (props: ReferencePreviewProps) => {
Expand All @@ -103,7 +114,10 @@ ReferenceField.Preview = (props: ReferencePreviewProps) => {
<Loader color="gray" size="sm" />
</div>
) : previewDoc ? (
<ReferenceField.DocCard doc={previewDoc} />
<ReferenceField.DocCard
doc={previewDoc}
onOpenQuickEdit={props.onOpenQuickEdit}
/>
) : (
<div className="ReferenceField__Preview__notfound">
Doc not found: <b>{props.id}</b> (was it deleted?). Select a new doc
Expand All @@ -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 || '';
Expand Down Expand Up @@ -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);
}}
>
<div className="ReferenceField__DocCard__image">
<Image
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.ReferenceFieldEditorModal {
max-height: calc(100vh - 160px);
overflow: auto;
}

.ReferenceFieldEditorModal__footer {
border-top: 1px solid var(--color-border);
padding-top: 12px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import './ReferenceFieldEditorModal.css';

import {Button, Group, Loader, Modal, Stack, Text} from '@mantine/core';
import {showNotification} from '@mantine/notifications';
import {IconExternalLink} from '@tabler/icons-preact';
import {doc, serverTimestamp, updateDoc} from 'firebase/firestore';
import {useEffect, useMemo, useState} from 'preact/hooks';
import * as schema from '../../../../core/schema.js';
import {
DraftDocContext,
DraftDocContextProvider,
} from '../../../hooks/useDraftDoc.js';
import {useCollectionSchema} from '../../../hooks/useCollectionSchema.js';
import {useModalTheme} from '../../../hooks/useModalTheme.js';
import {
getDocFromCacheOrFetch,
setDocToCache,
} from '../../../utils/doc-cache.js';
import {notifyErrors} from '../../../utils/notifications.js';
import {cloneData} from '../../../utils/objects.js';
import {DocEditor} from '../DocEditor.js';
import {InMemoryDraftDocController} from '../../RichTextEditor/lexical/utils/InMemoryDraftDocController.js';

interface ReferenceFieldEditorModalProps {
docId: string | null;
opened: boolean;
onClose: () => 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<any>(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<schema.ObjectField | null>(() => {
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 (
<Modal
{...modalTheme}
opened={props.opened}
onClose={props.onClose}
title="Reference Editor"
size="90%"
zIndex={190}
>
<Stack className="ReferenceFieldEditorModal" spacing="md">
<Group position="right">
<Button
component="a"
href={`/cms/content/${props.docId}`}
target="_blank"
rel="noopener noreferrer"
size="xs"
variant="default"
leftIcon={<IconExternalLink size={14} />}
>
Open in new tab
</Button>
</Group>
{loading ? (
<Group position="center" py="md">
<Loader color="gray" size="sm" />
</Group>
) : !objectField || !draftContext ? (
<Text size="sm" color="dimmed">
Unable to load reference fields.
</Text>
) : (
<DraftDocContextProvider value={draftContext}>
<DocEditor.ObjectField field={objectField} deepKey="fields" />
</DraftDocContextProvider>
)}
<Group position="right" className="ReferenceFieldEditorModal__footer">
<Button
variant="default"
size="xs"
onClick={props.onClose}
disabled={saving}
>
Cancel
</Button>
<Button size="xs" onClick={onSave} loading={saving}>
Save
</Button>
</Group>
</Stack>
</Modal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -52,3 +48,7 @@
align-items: flex-start;
padding: 0 8px;
}

.ReferencesField__card__previewCard {
padding: 0 !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>([]);
const draft = useDraftDoc().controller;
const [quickEditDocId, setQuickEditDocId] = useState<string | null>(null);

function onChange(newIds: string[]) {
if (newIds.length) {
Expand Down Expand Up @@ -119,13 +121,31 @@ export function ReferencesField(props: FieldProps) {
>
<IconGripVertical size={16} stroke={'1.5'} />
</div>
<DocPreviewCard
<div
className="ReferencesField__card__preview"
docId={refId}
variant="compact"
clickable
statusBadges
/>
onClickCapture={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
event.stopPropagation();
setQuickEditDocId(refId);
}}
>
<DocPreviewCard
className="ReferencesField__card__previewCard"
docId={refId}
variant="compact"
clickable
statusBadges
/>
</div>
<div className="ReferencesField__card__controls">
<Tooltip label="Remove">
<ActionIcon
Expand All @@ -151,6 +171,11 @@ export function ReferencesField(props: FieldProps) {
<Button color="dark" size="xs" onClick={() => openDocPickerModal()}>
{field.buttonLabel || 'Select'}
</Button>
<ReferenceFieldEditorModal
docId={quickEditDocId}
opened={!!quickEditDocId}
onClose={() => setQuickEditDocId(null)}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ export class InMemoryDraftDocController extends EventListener {
collectionId = 'custom-block';
slug = 'custom-block';

constructor(initialValue: Record<string, any>, rootKey = 'block') {
constructor(
initialValue: Record<string, any>,
rootKey: string | null = 'block'
) {
super();
this.data = {[rootKey]: cloneData(initialValue)};
this.data = rootKey
? {[rootKey]: cloneData(initialValue)}
: cloneData(initialValue);
}

getValue(key: string): any {
Expand Down