Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/early-dingos-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@blinkk/root-cms': patch
---

feat: add quick edit modal for reference fields
6 changes: 4 additions & 2 deletions packages/root-cms/ui/components/DocEditor/DocEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ import {SelectField} from './fields/SelectField.js';
import {StringField} from './fields/StringField.js';

interface DocEditorProps {
className?: string;
docId: string;
hideStatusBar?: boolean;
}

const COLLECTION_SCHEMA_TYPES_CONTEXT = createContext<
Expand All @@ -142,12 +144,12 @@ export function DocEditor(props: DocEditorProps) {
value={collection?.schema?.types || {}}
>
<DeeplinkProvider>
<div className="DocEditor">
<div className={joinClassNames(props.className, 'DocEditor')}>
<LoadingOverlay
visible={loading}
loaderProps={{color: 'gray', size: 'xl'}}
/>
{!loading && (
{!loading && !props.hideStatusBar && (
<DocEditor.StatusBar
{...props}
draft={draft}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './ReferenceField.css';

import {ActionIcon, Button, Image, Loader, Tooltip} from '@mantine/core';
import {IconTrash} from '@tabler/icons-preact';
import {IconPencil, IconTrash} from '@tabler/icons-preact';
import {useEffect, useState} from 'preact/hooks';
import * as schema from '../../../../core/schema.js';
import {useDraftDocValue} from '../../../hooks/useDraftDoc.js';
Expand All @@ -10,6 +10,7 @@ import {parseDocId} from '../../../utils/doc.js';
import {notifyErrors} from '../../../utils/notifications.js';
import {getNestedValue} from '../../../utils/objects.js';
import {useDocPickerModal} from '../../DocPickerModal/DocPickerModal.js';
import {useReferenceFieldEditorModal} from '../../ReferenceFieldEditorModal/ReferenceFieldEditorModal.js';
import {FieldProps} from './FieldProps.js';

export interface ReferenceFieldValue {
Expand All @@ -34,6 +35,7 @@ export function ReferenceField(props: FieldProps) {
}

const docPickerModal = useDocPickerModal();
const referenceFieldEditorModal = useReferenceFieldEditorModal();

function openDocPicker() {
const initialCollection = refId
Expand All @@ -55,6 +57,14 @@ export function ReferenceField(props: FieldProps) {
<div className="ReferenceField__ref">
<ReferenceField.Preview id={refId} />
<div className="ReferenceField__remove">
<Tooltip label="Quick edit">
<ActionIcon
className="ReferenceField__remove__icon"
onClick={() => referenceFieldEditorModal.open({docId: refId})}
>
<IconPencil size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Remove">
<ActionIcon
className="ReferenceField__remove__icon"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
DropResult,
} from '@hello-pangea/dnd';
import {ActionIcon, Button, Tooltip} from '@mantine/core';
import {IconGripVertical, IconTrash} from '@tabler/icons-preact';
import {IconGripVertical, IconPencil, IconTrash} from '@tabler/icons-preact';
import {useState} from 'preact/hooks';
import * as schema from '../../../../core/schema.js';
import {useDraftDoc, useDraftDocField} from '../../../hooks/useDraftDoc.js';
import {joinClassNames} from '../../../utils/classes.js';
import {useDocPickerModal} from '../../DocPickerModal/DocPickerModal.js';
import {DocPreviewCard} from '../../DocPreviewCard/DocPreviewCard.js';
import {useReferenceFieldEditorModal} from '../../ReferenceFieldEditorModal/ReferenceFieldEditorModal.js';
import {FieldProps} from './FieldProps.js';
import {ReferenceFieldValue} from './ReferenceField.js';

Expand Down Expand Up @@ -44,6 +45,7 @@ export function ReferencesField(props: FieldProps) {
});

const docPickerModal = useDocPickerModal();
const referenceFieldEditorModal = useReferenceFieldEditorModal();

function openDocPickerModal() {
docPickerModal.open({
Expand Down Expand Up @@ -127,6 +129,15 @@ export function ReferencesField(props: FieldProps) {
statusBadges
/>
<div className="ReferencesField__card__controls">
<Tooltip label="Quick edit">
<ActionIcon
onClick={() =>
referenceFieldEditorModal.open({docId: refId})
}
>
<IconPencil size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="Remove">
<ActionIcon
className="ReferencesField__card__controls__remove"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.ReferenceFieldEditorModalWrap .mantine-Modal-modal {
padding: 0;
}

.ReferenceFieldEditorModalWrap .mantine-Modal-header {
padding: 12px 20px;
margin-bottom: 0;
}

.mantine-Modal-body {
display: flex;
flex-direction: column;
}

.ReferenceFieldEditorModal {
display: contents;
}

.ReferenceFieldEditorModal__DocEditor {
flex: 1;
overflow: auto;
padding: 0 20px 40px;
padding-bottom: 80px;
}

.ReferenceFieldEditorModal__footer {
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid var(--color-border);
padding: 12px 20px;
}

.ReferenceFieldEditorModal__footer__start {
flex: 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import './ReferenceFieldEditorModal.css';

import {ActionIcon, Button, Tooltip} from '@mantine/core';
import {ContextModalProps, useModals} from '@mantine/modals';
import {IconArrowUpRight} from '@tabler/icons-preact';
import {DraftDocProvider, useDraftDoc} from '../../hooks/useDraftDoc.js';
import {useModalTheme} from '../../hooks/useModalTheme.js';
import {DocEditor} from '../DocEditor/DocEditor.js';

const MODAL_ID = 'ReferenceFieldEditorModal';

export interface ReferenceFieldEditorModalProps {
[key: string]: unknown;
docId: string;
}

export function useReferenceFieldEditorModal() {
const modals = useModals();
const modalTheme = useModalTheme();
return {
open: (props: ReferenceFieldEditorModalProps) => {
modals.openContextModal<ReferenceFieldEditorModalProps>(MODAL_ID, {
...modalTheme,
className: 'ReferenceFieldEditorModalWrap',
innerProps: props,
title: `Edit ${props.docId}`,
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<ReferenceFieldEditorModalProps>
) {
const {id, context, innerProps} = modalProps;
const docId = innerProps.docId;
return (
<div className="ReferenceFieldEditorModal">
<DraftDocProvider docId={docId} autoSave={false} flushOnStop={false}>
<DocEditor
className="ReferenceFieldEditorModal__DocEditor"
docId={innerProps.docId}
hideStatusBar
/>
<ReferenceFieldEditorModal.Footer
docId={docId}
onCancel={() => context.closeModal(id)}
/>
</DraftDocProvider>
</div>
);
}

ReferenceFieldEditorModal.Footer = (props: {
docId: string;
onCancel: () => void;
}) => {
const draft = useDraftDoc();

return (
<div className="ReferenceFieldEditorModal__footer">
<div className="ReferenceFieldEditorModal__footer__start">
<Button
component="a"
variant="default"
size="xs"
rightIcon={<IconArrowUpRight size={16} />}
href={`/cms/content/${props.docId}`}
target="_blank"
>
Open {props.docId}
</Button>
</div>
<Button variant="default" size="xs" onClick={props.onCancel}>
Cancel
</Button>
<Button
color="dark"
size="xs"
onClick={async () => {
await draft.controller.flush();
props.onCancel();
}}
>
Save
</Button>
</div>
);
};

ReferenceFieldEditorModal.id = MODAL_ID;
26 changes: 22 additions & 4 deletions packages/root-cms/ui/hooks/useDraftDoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -144,7 +148,9 @@ export class DraftDocController extends EventListener {
if (this.dbUnsubscribe) {
this.dbUnsubscribe();
}
this.flush();
if (this.flushOnStop) {
this.flush();
}
this.started = false;
}

Expand Down Expand Up @@ -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();
}
}

/**
Expand All @@ -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();
}
}

/**
Expand All @@ -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();
}
}

/**
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/root-cms/ui/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -127,6 +128,7 @@ function App() {
[LocalizationModal.id]: LocalizationModal,
[LockPublishingModal.id]: LockPublishingModal,
[PublishDocModal.id]: PublishDocModal,
[ReferenceFieldEditorModal.id]: ReferenceFieldEditorModal,
[ScheduleReleaseModal.id]: ScheduleReleaseModal,
[VersionHistoryModal.id]: VersionHistoryModal,
}}
Expand Down