Enhance editing logic

This commit is contained in:
Oliver Walters
2026-05-26 12:05:19 +00:00
parent 664eed1cae
commit 36006c6416
2 changed files with 49 additions and 32 deletions
@@ -37,12 +37,14 @@ import {
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconCheck,
IconCirclePlus, IconCirclePlus,
IconColumnInsertLeft, IconColumnInsertLeft,
IconColumnInsertRight, IconColumnInsertRight,
IconColumnRemove, IconColumnRemove,
IconDeviceFloppy, IconDeviceFloppy,
IconInfoCircle, IconInfoCircle,
IconPencil,
IconPhoto, IconPhoto,
IconReload, IconReload,
IconRowInsertBottom, IconRowInsertBottom,
@@ -108,20 +110,18 @@ function NoteInfoHover({ note }: { note: any }) {
export default function NotesEditor({ export default function NotesEditor({
modelType, modelType,
modelId, modelId
editable: _editable,
setDirtyCallback: _setDirtyCallback
}: Readonly<{ }: Readonly<{
modelType: ModelType; modelType: ModelType;
modelId: number; modelId: number;
editable?: boolean;
setDirtyCallback?: (dirty: boolean) => void;
}>) { }>) {
const api = useApi(); const api = useApi();
const user = useUserState(); const user = useUserState();
const [_language] = useLocalState(useShallow((s) => [s.language])); const [_language] = useLocalState(useShallow((s) => [s.language]));
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [isEditing, setIsEditing] = useState<boolean>(false);
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
const [selectedNoteId, setSelectedNoteId] = useState<number | undefined>( const [selectedNoteId, setSelectedNoteId] = useState<number | undefined>(
@@ -151,6 +151,7 @@ export default function NotesEditor({
}, [uploadFile]); }, [uploadFile]);
const editor = useEditor({ const editor = useEditor({
editable: false,
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
link: { openOnClick: false } link: { openOnClick: false }
@@ -210,6 +211,7 @@ export default function NotesEditor({
); );
useEffect(() => { useEffect(() => {
// setIsEditing(false);
loadNote(selectedNoteId ?? -1); loadNote(selectedNoteId ?? -1);
}, [editor, selectedNoteId, notesQuery.data]); }, [editor, selectedNoteId, notesQuery.data]);
@@ -238,7 +240,7 @@ export default function NotesEditor({
setSelectedNoteId((primary ?? notesQuery.data[0])?.pk ?? undefined); setSelectedNoteId((primary ?? notesQuery.data[0])?.pk ?? undefined);
}, [notesQuery.data]); }, [notesQuery.data]);
const canEdit = useMemo( const canEdit: boolean = useMemo(
() => () =>
user.hasChangePermission(modelType) && user.hasChangePermission(modelType) &&
notesQuery.isFetched && notesQuery.isFetched &&
@@ -254,8 +256,8 @@ export default function NotesEditor({
// Sync editor editable state when permissions change // Sync editor editable state when permissions change
useEffect(() => { useEffect(() => {
editor?.setEditable(canEdit); editor?.setEditable(canEdit && isEditing);
}, [editor, canEdit]); }, [editor, canEdit, isEditing]);
const hasNotes = useMemo(() => { const hasNotes = useMemo(() => {
return notesQuery.data && notesQuery.data.length > 0; return notesQuery.data && notesQuery.data.length > 0;
@@ -381,11 +383,21 @@ export default function NotesEditor({
</Group> </Group>
{canEdit && ( {canEdit && (
<Group justify='right' gap='xs'> <Group justify='right' gap='xs'>
{isDirty && ( {!isEditing && (
<Tooltip label={t`Edit note`}>
<ActionIcon
variant='transparent'
onClick={() => setIsEditing(true)}
>
<IconPencil />
</ActionIcon>
</Tooltip>
)}
{isEditing && isDirty && (
<Badge color='yellow'>{t`Unsaved Changes`}</Badge> <Badge color='yellow'>{t`Unsaved Changes`}</Badge>
)} )}
{isDirty && ( {isEditing && isDirty && (
<Tooltip label={t`Save Notes (Ctrl+S)`}> <Tooltip label={t`Save note`}>
<ActionIcon <ActionIcon
variant='transparent' variant='transparent'
color={'green'} color={'green'}
@@ -396,8 +408,8 @@ export default function NotesEditor({
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}
{isDirty && ( {isEditing && isDirty && (
<Tooltip label={t`Reset Notes`}> <Tooltip label={t`Reset note content`}>
<ActionIcon <ActionIcon
variant='transparent' variant='transparent'
onClick={reloadNote} onClick={reloadNote}
@@ -407,6 +419,17 @@ export default function NotesEditor({
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}
{isEditing && !isDirty && (
<Tooltip label={t`Finish editing`}>
<ActionIcon
variant='transparent'
onClick={() => setIsEditing(false)}
color='green'
>
<IconCheck />
</ActionIcon>
</Tooltip>
)}
<OptionsActionDropdown <OptionsActionDropdown
tooltip={t`Note Actions`} tooltip={t`Note Actions`}
actions={[ actions={[
@@ -440,7 +463,7 @@ export default function NotesEditor({
editor={editor} editor={editor}
style={{ minHeight: '400px' }} style={{ minHeight: '400px' }}
> >
{canEdit && ( {canEdit && isEditing && (
<RichTextEditor.Toolbar sticky> <RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup>
<RichTextEditor.Bold /> <RichTextEditor.Bold />
@@ -602,15 +625,16 @@ export default function NotesEditor({
</Box> </Box>
<Paper p='xs' shadow='sm' withBorder style={{ width: '200px' }}> <Paper p='xs' shadow='sm' withBorder style={{ width: '200px' }}>
<Stack gap='xs'> <Stack gap='xs'>
<Button {canEdit && (
color='green' <Button
leftSection={<IconCirclePlus />} color='green'
onClick={createNote.open} leftSection={<IconCirclePlus />}
// disabled={!canEdit || isDirty} onClick={createNote.open}
> disabled={isEditing}
{t`Add Note`} >
</Button> {t`Add Note`}
</Button>
)}
<Tabs <Tabs
orientation='vertical' orientation='vertical'
placement='right' placement='right'
@@ -620,7 +644,7 @@ export default function NotesEditor({
{notesQuery.data?.map((note: any) => ( {notesQuery.data?.map((note: any) => (
<Tabs.Tab <Tabs.Tab
key={note.pk} key={note.pk}
disabled={isDirty} disabled={isEditing}
value={note.pk?.toString()} value={note.pk?.toString()}
onClick={() => { onClick={() => {
setSelectedNoteId(note.pk); setSelectedNoteId(note.pk);
@@ -4,7 +4,6 @@ import { IconNotes } from '@tabler/icons-react';
import type { ModelType } from '@lib/enums/ModelType'; import type { ModelType } from '@lib/enums/ModelType';
import type { PanelType } from '@lib/types/Panel'; import type { PanelType } from '@lib/types/Panel';
import { useUserState } from '../../states/UserState';
import NotesEditor from '../editors/NotesEditor'; import NotesEditor from '../editors/NotesEditor';
// const NotesEditor = lazy(() => import('../editors/NotesEditor')); // const NotesEditor = lazy(() => import('../editors/NotesEditor'));
@@ -20,8 +19,6 @@ export default function NotesPanel({
editable?: boolean; editable?: boolean;
has_note?: boolean; has_note?: boolean;
}): PanelType { }): PanelType {
const user = useUserState.getState();
return { return {
name: 'notes', name: 'notes',
label: t`Notes`, label: t`Notes`,
@@ -29,11 +26,7 @@ export default function NotesPanel({
notification_dot: has_note ? 'info' : null, notification_dot: has_note ? 'info' : null,
content: content:
model_type && model_id ? ( model_type && model_id ? (
<NotesEditor <NotesEditor modelType={model_type} modelId={model_id} />
modelType={model_type}
modelId={model_id}
editable={editable ?? user.hasChangePermission(model_type)}
/>
) : ( ) : (
<Skeleton /> <Skeleton />
), ),