Refactor save UX
This commit is contained in:
parent
df340e363c
commit
689bd1f224
|
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue