@anchrd/intel-ui 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import { filterSuggestionItems } from "@blocknote/core";
|
|
|
9
9
|
import { SuggestionMenuController, useCreateBlockNote } from "@blocknote/react";
|
|
10
10
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
11
11
|
import { Link2 } from "lucide-react";
|
|
12
|
-
import { useEffect, useMemo, useState } from "react";
|
|
12
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
14
14
|
import { BlockNoteView, defaultSlashMenuItems } from "@/blocknote-view/blocknote-view.tsx";
|
|
15
15
|
import type { IntelDataProvider } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
@@ -34,6 +34,28 @@ function storedBlocks(content: string | null): IntelEditorPartialBlock[] | undef
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* What the editor holds, in a form two states can be compared by.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ This is what "unsaved" is answered with, and it deliberately is not "`onChange` fired". The
|
|
41
|
+
* editor changes for reasons that are not edits — loading the content is one (`replaceBlocks`
|
|
42
|
+
* below), and BlockNote normalizing a freshly mounted document is another. Neither is anything a
|
|
43
|
+
* reader did, and a warning that appears on every open is a warning nobody reads on the one day it
|
|
44
|
+
* is true. The canvas next door draws the same line with `editsNodes`/`editsEdges`.
|
|
45
|
+
*
|
|
46
|
+
* `id`s are part of it on purpose: BlockNote keeps them stable across edits within a session, and
|
|
47
|
+
* dropping them would call two different paragraphs equal whenever their text happens to match.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ It runs on every keystroke, and it walks the whole document to do so. That is affordable
|
|
50
|
+
* because `documentLinkIds` beside it already does, and because it is what buys the case a one-way
|
|
51
|
+
* flag cannot have: typing something and taking it back leaves nothing to save. There is no early
|
|
52
|
+
* exit — checking "already dirty, skip" is exactly what would lose that case. If documents ever
|
|
53
|
+
* grow far enough for this to be felt, the answer is to compare less often, not less carefully.
|
|
54
|
+
*/
|
|
55
|
+
function editorSnapshot(blocks: unknown): string {
|
|
56
|
+
return JSON.stringify(blocks);
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
export function KnowledgeEditor({
|
|
38
60
|
data,
|
|
39
61
|
document,
|
|
@@ -57,6 +79,13 @@ export function KnowledgeEditor({
|
|
|
57
79
|
// conditional that this repo's `exactOptionalPropertyTypes` defeats, so it hands back the
|
|
58
80
|
// default-schema editor even though it built Intel's. Same upstream seam as `BlockNoteView`.
|
|
59
81
|
}) as unknown as IntelEditor;
|
|
82
|
+
// The content as it stands on the server, in comparable form. Every question of "is there
|
|
83
|
+
// anything to save" is answered against this, and saving moves it forward.
|
|
84
|
+
//
|
|
85
|
+
// ⚠️ A ref, not state: it is read inside `onChange` on every keystroke and must be the current
|
|
86
|
+
// value there, not the one captured when that handler was created. Nothing renders from it — what
|
|
87
|
+
// renders is `dirty` — so there is nothing to re-render when it moves.
|
|
88
|
+
const savedSnapshot = useRef(editorSnapshot(editor.document));
|
|
60
89
|
const [dirty, setDirty] = useState(false);
|
|
61
90
|
const [picking, setPicking] = useState(false);
|
|
62
91
|
// Which documents the text links to right now. It is recomputed from the editor rather than from
|
|
@@ -69,25 +98,37 @@ export function KnowledgeEditor({
|
|
|
69
98
|
queryFn: () => data.resolveKnowledgeLinks({ nodeIds: linkedIds }),
|
|
70
99
|
enabled: linkedIds.length > 0,
|
|
71
100
|
});
|
|
101
|
+
// One place decides whether there is anything to save, so the answer cannot differ between the
|
|
102
|
+
// button, the guard and the save itself.
|
|
103
|
+
function settle(): void {
|
|
104
|
+
setDirty(editorSnapshot(editor.document) !== savedSnapshot.current);
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
const save = useMutation({
|
|
73
108
|
mutationFn: async () => {
|
|
109
|
+
const blocks = editor.document;
|
|
74
110
|
const payload = BlockNoteDocument.parse({
|
|
75
111
|
format: "blocknote",
|
|
76
112
|
schemaVersion: 1,
|
|
77
|
-
blocks
|
|
78
|
-
markdown: await editor.blocksToMarkdownLossy(
|
|
113
|
+
blocks,
|
|
114
|
+
markdown: await editor.blocksToMarkdownLossy(blocks),
|
|
79
115
|
});
|
|
80
|
-
|
|
116
|
+
const node = await data.saveKnowledge({
|
|
81
117
|
nodeId: document.node.id,
|
|
82
118
|
baseVersionId: document.version?.id ?? null,
|
|
83
119
|
content: JSON.stringify(payload),
|
|
84
120
|
mediaType: BlockNoteMediaType,
|
|
85
121
|
idempotencyKey: crypto.randomUUID(),
|
|
86
122
|
});
|
|
123
|
+
// ⚠️ The snapshot of what was SENT, not of what stands in the editor when the answer comes
|
|
124
|
+
// back. Saving crosses the network, and typing during it is normal — taking the state at
|
|
125
|
+
// arrival would call that typing saved and lose it at the next navigation.
|
|
126
|
+
return { node, snapshot: editorSnapshot(blocks) };
|
|
87
127
|
},
|
|
88
|
-
onSuccess: (
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
onSuccess: ({ node, snapshot }) => {
|
|
129
|
+
savedSnapshot.current = snapshot;
|
|
130
|
+
settle();
|
|
131
|
+
onSaved(node);
|
|
91
132
|
},
|
|
92
133
|
});
|
|
93
134
|
|
|
@@ -95,6 +136,11 @@ export function KnowledgeEditor({
|
|
|
95
136
|
if (!document.content || initialBlocks) return;
|
|
96
137
|
const blocks = editor.tryParseMarkdownToBlocks(document.content);
|
|
97
138
|
editor.replaceBlocks(editor.document, blocks);
|
|
139
|
+
// ⚠️ Loading the content IS a change to the editor, and `onChange` reports it as one. This is
|
|
140
|
+
// the line that keeps a document nobody has touched from asking to be saved: what was just
|
|
141
|
+
// loaded is by definition what is stored.
|
|
142
|
+
savedSnapshot.current = editorSnapshot(editor.document);
|
|
143
|
+
setDirty(false);
|
|
98
144
|
}, [document.content, editor, initialBlocks]);
|
|
99
145
|
|
|
100
146
|
function insertLink(nodeId: string): void {
|
|
@@ -105,7 +151,7 @@ export function KnowledgeEditor({
|
|
|
105
151
|
" ",
|
|
106
152
|
]);
|
|
107
153
|
setPicking(false);
|
|
108
|
-
|
|
154
|
+
settle();
|
|
109
155
|
setLinkedIds(documentLinkIds(editor.document));
|
|
110
156
|
}
|
|
111
157
|
|
|
@@ -135,7 +181,7 @@ export function KnowledgeEditor({
|
|
|
135
181
|
<BlockNoteView
|
|
136
182
|
editor={editor}
|
|
137
183
|
onChange={() => {
|
|
138
|
-
|
|
184
|
+
settle();
|
|
139
185
|
setLinkedIds(documentLinkIds(editor.document));
|
|
140
186
|
}}
|
|
141
187
|
>
|