Refactor save UX

This commit is contained in:
Oliver Walters 2026-05-21 11:45:01 +00:00
parent df340e363c
commit 689bd1f224
1 changed files with 83 additions and 30 deletions

View File

@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import EasyMDE, { type default as SimpleMde } from 'easymde'; import EasyMDE, { type default as SimpleMde } from 'easymde';
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import { useHotkeys } from '@mantine/hooks';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
@ -18,20 +19,22 @@ import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react'; import { useCreateBlockNote } from '@blocknote/react';
import { import {
ActionIcon,
Alert, Alert,
Box, Box,
Button,
Flex, Flex,
Group, Group,
Paper, Paper,
Stack, Stack,
Tabs, Tabs,
Text Text,
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { import {
IconCirclePlus, IconCirclePlus,
IconDeviceFloppy,
IconInfoCircle, IconInfoCircle,
IconUpload IconReload
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useNoteFields } from '../../forms/CommonForms'; import { useNoteFields } from '../../forms/CommonForms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
@ -53,6 +56,8 @@ export default function NotesEditor({
const editor = useCreateBlockNote(); const editor = useCreateBlockNote();
const [isDirty, setIsDirty] = useState(false);
// The ID of the selected note // The ID of the selected note
const [selectedNote, setSelectedNote] = useState<number | undefined>( const [selectedNote, setSelectedNote] = useState<number | undefined>(
undefined undefined
@ -78,31 +83,43 @@ export default function NotesEditor({
}); });
useEffect(() => { useEffect(() => {
const note = notesQuery.data?.find((note: any) => note.pk === selectedNote); return editor.onChange(() => setIsDirty(true));
}, [editor]);
if (note) { const loadNote = useCallback(
const blocks = editor.tryParseHTMLToBlocks(note.content ?? ''); (noteId: number) => {
const note = notesQuery.data?.find((note: any) => note.pk === noteId);
if (blocks) { if (note) {
editor.replaceBlocks(editor.document, blocks); const blocks = editor.tryParseHTMLToBlocks(note.content ?? '');
if (blocks) {
editor.replaceBlocks(editor.document, blocks);
}
} else {
editor.replaceBlocks(editor.document, []);
} }
} else {
editor.replaceBlocks(editor.document, []); setIsDirty(false);
} },
[editor, notesQuery.data]
);
useEffect(() => {
loadNote(selectedNote ?? -1);
}, [editor, selectedNote, notesQuery.data]); }, [editor, selectedNote, notesQuery.data]);
// Adjust the note selection // Adjust the note selection
useEffect(() => { useEffect(() => {
// If the currently selected note is not in the list of available notes, then we need to adjust the selection if (!notesQuery.data) return;
if (
const stillExists =
selectedNote && selectedNote &&
notesQuery.data && notesQuery.data.some((note: any) => note.pk === selectedNote);
!notesQuery.data.some((note: any) => note.pk === selectedNote) if (stillExists) return;
) {
setSelectedNote( const primary = notesQuery.data.find((note: any) => note.primary);
notesQuery.data.length > 0 ? notesQuery.data[0].pk : undefined setSelectedNote((primary ?? notesQuery.data[0])?.pk ?? undefined);
);
}
}, [notesQuery.data]); }, [notesQuery.data]);
const canEdit = useMemo( const canEdit = useMemo(
@ -134,10 +151,14 @@ export default function NotesEditor({
} }
}); });
const reloadNote = useCallback(() => {
loadNote(selectedNote ?? -1);
}, [selectedNote, loadNote]);
const saveNote = useCallback(() => { const saveNote = useCallback(() => {
// if (!selectedNote) { if (!selectedNote) {
// return; return;
// } }
const blocks = editor.document; const blocks = editor.document;
const html = editor.blocksToHTMLLossy(blocks); const html = editor.blocksToHTMLLossy(blocks);
@ -152,6 +173,7 @@ export default function NotesEditor({
api api
.patch(url, { content: html }) .patch(url, { content: html })
.then(() => { .then(() => {
setIsDirty(false);
notifications.show({ notifications.show({
title: t`Success`, title: t`Success`,
message: t`Note updated`, message: t`Note updated`,
@ -173,7 +195,9 @@ export default function NotesEditor({
notesQuery.refetch(); notesQuery.refetch();
}); });
} }
}, [selectedNote, editor]); }, [selectedNote, editor, setIsDirty]);
useHotkeys([['mod+s', saveNote]]);
return ( return (
<> <>
@ -198,12 +222,40 @@ export default function NotesEditor({
</Box> </Box>
<Paper p='sm' shadow='sm' withBorder ml='md' style={{ width: '200px' }}> <Paper p='sm' shadow='sm' withBorder ml='md' style={{ width: '200px' }}>
<Stack gap='xs'> <Stack gap='xs'>
<Button leftSection={<IconUpload />} onClick={saveNote}> {canEdit && (
{t`Save`} <Group justify='space-apart' grow>
</Button> <Tooltip label={t`Save Notes (Ctrl+S)`}>
<Button leftSection={<IconCirclePlus />} onClick={createNote.open}> <ActionIcon
{t`Add Note`} variant='transparent'
</Button> color={isDirty ? 'yellow' : undefined}
onClick={saveNote}
disabled={!canEdit || !isDirty}
>
<IconDeviceFloppy />
</ActionIcon>
</Tooltip>
<Tooltip label={t`Reset Notes`}>
<ActionIcon
variant='transparent'
color='red'
onClick={reloadNote}
disabled={!canEdit || !isDirty}
>
<IconReload />
</ActionIcon>
</Tooltip>
<Tooltip label={t`New Note`}>
<ActionIcon
variant='transparent'
color='green'
onClick={createNote.open}
disabled={!canEdit || isDirty}
>
<IconCirclePlus />
</ActionIcon>
</Tooltip>
</Group>
)}
<Tabs <Tabs
orientation='vertical' orientation='vertical'
placement='right' placement='right'
@ -213,6 +265,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}
value={note.pk?.toString()} value={note.pk?.toString()}
onClick={() => setSelectedNote(note.pk)} onClick={() => setSelectedNote(note.pk)}
> >