@agent-native/core 0.123.1 → 0.123.2
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +2 -2
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
21
21
|
import type { Editor } from "@tiptap/react";
|
|
22
22
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
23
|
-
import { useEffect, useState } from "react";
|
|
23
|
+
import { useCallback, useEffect, useState } from "react";
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
26
|
Tooltip,
|
|
@@ -83,11 +83,35 @@ function selectionIncludesBubbleToolbarExcludedNode(
|
|
|
83
83
|
return includesExcludedNode;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function shouldShowBubbleToolbar({
|
|
87
|
+
editor,
|
|
88
|
+
element,
|
|
89
|
+
state,
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
}: {
|
|
93
|
+
editor: Editor;
|
|
94
|
+
element: HTMLElement;
|
|
95
|
+
state: EditorState;
|
|
96
|
+
from: number;
|
|
97
|
+
to: number;
|
|
98
|
+
}) {
|
|
99
|
+
const focusBelongsToToolbar = element.contains(document.activeElement);
|
|
100
|
+
if (!editor.view.hasFocus() && !focusBelongsToToolbar) return false;
|
|
101
|
+
if (from === to) return false;
|
|
102
|
+
return !selectionIncludesBubbleToolbarExcludedNode(state, from, to);
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
87
106
|
const t = useT();
|
|
88
107
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
89
108
|
const [linkUrl, setLinkUrl] = useState("");
|
|
90
109
|
|
|
110
|
+
const openLinkInput = useCallback(() => {
|
|
111
|
+
setLinkUrl(editor.getAttributes("link").href || "");
|
|
112
|
+
setShowLinkInput(true);
|
|
113
|
+
}, [editor]);
|
|
114
|
+
|
|
91
115
|
useEffect(() => {
|
|
92
116
|
const plugin = new Plugin<SelectionFillRange | null>({
|
|
93
117
|
key: selectionFillPluginKey,
|
|
@@ -105,6 +129,29 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
105
129
|
},
|
|
106
130
|
},
|
|
107
131
|
props: {
|
|
132
|
+
handleKeyDown(_view, event) {
|
|
133
|
+
if (
|
|
134
|
+
!(event.metaKey || event.ctrlKey) ||
|
|
135
|
+
event.shiftKey ||
|
|
136
|
+
event.altKey ||
|
|
137
|
+
event.key.toLowerCase() !== "k"
|
|
138
|
+
) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { state } = editor;
|
|
143
|
+
const { from, to } = state.selection;
|
|
144
|
+
if (
|
|
145
|
+
from === to ||
|
|
146
|
+
selectionIncludesBubbleToolbarExcludedNode(state, from, to)
|
|
147
|
+
) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
openLinkInput();
|
|
153
|
+
return true;
|
|
154
|
+
},
|
|
108
155
|
decorations(state) {
|
|
109
156
|
const range = selectionFillPluginKey.getState(state);
|
|
110
157
|
if (!range || range.from === range.to) return DecorationSet.empty;
|
|
@@ -153,7 +200,7 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
153
200
|
editor.off("blur", syncSelectionFill);
|
|
154
201
|
editor.unregisterPlugin(selectionFillPluginKey);
|
|
155
202
|
};
|
|
156
|
-
}, [editor]);
|
|
203
|
+
}, [editor, openLinkInput]);
|
|
157
204
|
|
|
158
205
|
const handleSetLink = () => {
|
|
159
206
|
if (linkUrl.trim()) {
|
|
@@ -175,9 +222,7 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
175
222
|
editor.chain().focus().unsetLink().run();
|
|
176
223
|
return;
|
|
177
224
|
}
|
|
178
|
-
|
|
179
|
-
setLinkUrl(previousUrl);
|
|
180
|
-
setShowLinkInput(true);
|
|
225
|
+
openLinkInput();
|
|
181
226
|
};
|
|
182
227
|
|
|
183
228
|
const items = [
|
|
@@ -276,12 +321,8 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
276
321
|
<BubbleMenu
|
|
277
322
|
editor={editor}
|
|
278
323
|
className="bubble-toolbar"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const isSelection = from !== to;
|
|
282
|
-
if (!isSelection) return false;
|
|
283
|
-
return !selectionIncludesBubbleToolbarExcludedNode(state, from, to);
|
|
284
|
-
}}
|
|
324
|
+
updateDelay={0}
|
|
325
|
+
shouldShow={shouldShowBubbleToolbar}
|
|
285
326
|
>
|
|
286
327
|
{showLinkInput ? (
|
|
287
328
|
<div
|
|
@@ -291,6 +332,7 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
291
332
|
<input
|
|
292
333
|
autoFocus
|
|
293
334
|
type="url"
|
|
335
|
+
aria-label={t("editor.pasteLink")}
|
|
294
336
|
placeholder={t("editor.pasteLink")}
|
|
295
337
|
value={linkUrl}
|
|
296
338
|
onChange={(e) => setLinkUrl(e.target.value)}
|
|
@@ -336,7 +378,15 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
|
|
|
336
378
|
<Tooltip key={title}>
|
|
337
379
|
<TooltipTrigger asChild>
|
|
338
380
|
<button
|
|
339
|
-
|
|
381
|
+
onPointerDown={(event) => {
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
event.stopPropagation();
|
|
384
|
+
action();
|
|
385
|
+
}}
|
|
386
|
+
onClick={(event) => {
|
|
387
|
+
if (event.detail === 0) action();
|
|
388
|
+
}}
|
|
389
|
+
aria-label={title}
|
|
340
390
|
className={cn(
|
|
341
391
|
"p-2 rounded",
|
|
342
392
|
isActive()
|
|
@@ -145,6 +145,28 @@ export function titleMatchConfirmsSave(args: {
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export function refreshUnchangedContentSaveWatermark(args: {
|
|
149
|
+
serverContent: string;
|
|
150
|
+
serverUpdatedAt: string | null;
|
|
151
|
+
lastSaved: ContentSaveWatermark;
|
|
152
|
+
}): ContentSaveWatermark {
|
|
153
|
+
if (
|
|
154
|
+
args.serverContent !== args.lastSaved.content ||
|
|
155
|
+
!args.serverUpdatedAt ||
|
|
156
|
+
(args.lastSaved.updatedAt &&
|
|
157
|
+
args.serverUpdatedAt <= args.lastSaved.updatedAt)
|
|
158
|
+
) {
|
|
159
|
+
return args.lastSaved;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// documents.updatedAt versions the whole row, not just the body. If the
|
|
163
|
+
// fetched body still byte-matches our saved baseline, a newer timestamp can
|
|
164
|
+
// only describe a title/icon/metadata update. Advance the content CAS base so
|
|
165
|
+
// a local rich-text tail is not silently preflight-dropped. A concurrent body
|
|
166
|
+
// edit still differs here and remains protected by the server CAS.
|
|
167
|
+
return { ...args.lastSaved, updatedAt: args.serverUpdatedAt };
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
function adoptConfirmedSaveWatermarks({
|
|
149
171
|
saved,
|
|
150
172
|
savedAt,
|
|
@@ -219,7 +241,13 @@ function DocumentUnavailable({ onOpenHome }: { onOpenHome: () => void }) {
|
|
|
219
241
|
* an infinite spinner plus repeating 404/403 polls in the console.
|
|
220
242
|
*/
|
|
221
243
|
export function DocumentEditor({ documentId }: DocumentEditorProps) {
|
|
222
|
-
const
|
|
244
|
+
const documentQuery = useDocument(documentId);
|
|
245
|
+
const {
|
|
246
|
+
data: queriedDocument,
|
|
247
|
+
isError,
|
|
248
|
+
isFetchedAfterMount,
|
|
249
|
+
isFetching,
|
|
250
|
+
} = documentQuery;
|
|
223
251
|
const navigate = useNavigate();
|
|
224
252
|
const document =
|
|
225
253
|
queriedDocument?.id === documentId ? queriedDocument : undefined;
|
|
@@ -230,13 +258,32 @@ export function DocumentEditor({ documentId }: DocumentEditorProps) {
|
|
|
230
258
|
|
|
231
259
|
// If we have a doc (real or optimistic from create) render the editor —
|
|
232
260
|
// an `isError` blip during a just-fired create shouldn't flash "not found".
|
|
233
|
-
|
|
261
|
+
// A database/list snapshot can optimistically seed the document cache with a
|
|
262
|
+
// body that predates the latest collaborative save. Mounting ProseMirror from
|
|
263
|
+
// that snapshot lets reconcile briefly insert the stale tail beside the
|
|
264
|
+
// already-current Y.Doc. Wait only for this mount's first dedicated
|
|
265
|
+
// get-document response; later poll/SSE refetches remain live and reconcile
|
|
266
|
+
// without replacing the editor.
|
|
267
|
+
if (
|
|
268
|
+
!document ||
|
|
269
|
+
shouldAwaitAuthoritativeDocument({ isFetching, isFetchedAfterMount })
|
|
270
|
+
) {
|
|
234
271
|
return <DocumentEditorSkeleton />;
|
|
235
272
|
}
|
|
236
273
|
|
|
237
274
|
return <DocumentEditorBody documentId={documentId} document={document} />;
|
|
238
275
|
}
|
|
239
276
|
|
|
277
|
+
export function shouldAwaitAuthoritativeDocument({
|
|
278
|
+
isFetching,
|
|
279
|
+
isFetchedAfterMount,
|
|
280
|
+
}: {
|
|
281
|
+
isFetching: boolean;
|
|
282
|
+
isFetchedAfterMount: boolean;
|
|
283
|
+
}) {
|
|
284
|
+
return isFetching && !isFetchedAfterMount;
|
|
285
|
+
}
|
|
286
|
+
|
|
240
287
|
interface DocumentEditorBodyProps {
|
|
241
288
|
documentId: string;
|
|
242
289
|
document: Document;
|
|
@@ -262,6 +309,23 @@ type DocumentSaveResult = {
|
|
|
262
309
|
contentPersisted: boolean;
|
|
263
310
|
};
|
|
264
311
|
|
|
312
|
+
export function enqueueDocumentSave<T>(
|
|
313
|
+
queueRef: MutableRefObject<Promise<void>>,
|
|
314
|
+
save: () => Promise<T>,
|
|
315
|
+
): Promise<T> {
|
|
316
|
+
// Content CAS assumes each local save starts from the result of the previous
|
|
317
|
+
// local save. Debounced typing and structural "save now" operations can
|
|
318
|
+
// otherwise overlap with the same baseUpdatedAt: the shorter request wins,
|
|
319
|
+
// and the later, fuller document is rejected as a conflict. Keep the safety
|
|
320
|
+
// guard and serialize this editor's writes instead of weakening CAS.
|
|
321
|
+
const queued = queueRef.current.then(save, save);
|
|
322
|
+
queueRef.current = queued.then(
|
|
323
|
+
() => undefined,
|
|
324
|
+
() => undefined,
|
|
325
|
+
);
|
|
326
|
+
return queued;
|
|
327
|
+
}
|
|
328
|
+
|
|
265
329
|
function useMinViewportWidth(minWidth: number) {
|
|
266
330
|
const [matches, setMatches] = useState(false);
|
|
267
331
|
|
|
@@ -543,6 +607,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
543
607
|
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
544
608
|
const promotedBuilderBodyRef = useRef<string | null>(null);
|
|
545
609
|
const pendingDocumentSaveRef = useRef<PendingDocumentSave | null>(null);
|
|
610
|
+
const documentSaveQueueRef = useRef<Promise<void>>(Promise.resolve());
|
|
546
611
|
// Separate freshness watermarks for title and content so that a content save
|
|
547
612
|
// never suppresses adopting a newer external title and vice versa.
|
|
548
613
|
const lastSavedTitleRef = useRef<{ title: string; updatedAt: string | null }>(
|
|
@@ -563,6 +628,8 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
563
628
|
document.updatedAt ?? null,
|
|
564
629
|
);
|
|
565
630
|
documentUpdatedAtRef.current = document.updatedAt ?? null;
|
|
631
|
+
const documentContentRef = useRef(document.content);
|
|
632
|
+
documentContentRef.current = document.content;
|
|
566
633
|
const handleBackgroundSaveError = useCallback(
|
|
567
634
|
(error: unknown) => {
|
|
568
635
|
toast.error(t("empty.genericError"), {
|
|
@@ -651,7 +718,6 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
651
718
|
const {
|
|
652
719
|
ydoc,
|
|
653
720
|
awareness,
|
|
654
|
-
isLoading: collabLoading,
|
|
655
721
|
isSynced: collabSynced,
|
|
656
722
|
activeUsers,
|
|
657
723
|
agentActive,
|
|
@@ -663,12 +729,12 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
663
729
|
});
|
|
664
730
|
const bodyHydrationPending = documentBodyHydrationIsPending(document);
|
|
665
731
|
const editorCanEdit =
|
|
666
|
-
canEdit && !bodyHydrationPending && (isLocalFileDocument ||
|
|
732
|
+
canEdit && !bodyHydrationPending && (isLocalFileDocument || collabSynced);
|
|
667
733
|
// Bind an editor's stable Y.Doc on its first mount, even while the initial
|
|
668
|
-
// state is loading. Editability
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
734
|
+
// state is loading. Editability and the reconcile hook share the exact
|
|
735
|
+
// `collabSynced` boundary; "not loading" can precede persisted Y.Doc
|
|
736
|
+
// projection and briefly expose duplicated blocks. Keeping the Y.Doc binding
|
|
737
|
+
// stable avoids a snapshot -> collab remount while the read-only editor waits.
|
|
672
738
|
const collabEditorEnabled = collabEnabled && canEdit && !bodyHydrationPending;
|
|
673
739
|
canEditRef.current = editorCanEdit;
|
|
674
740
|
|
|
@@ -912,6 +978,11 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
912
978
|
content: string,
|
|
913
979
|
options: DocumentSaveOptions = {},
|
|
914
980
|
): Promise<DocumentSaveResult> => {
|
|
981
|
+
lastSavedContentRef.current = refreshUnchangedContentSaveWatermark({
|
|
982
|
+
serverContent: documentContentRef.current,
|
|
983
|
+
serverUpdatedAt: documentUpdatedAtRef.current,
|
|
984
|
+
lastSaved: lastSavedContentRef.current,
|
|
985
|
+
});
|
|
915
986
|
// Never clobber a newer server version (e.g. an agent edit we haven't
|
|
916
987
|
// reconciled into the editor yet) with the editor's current — possibly
|
|
917
988
|
// stale — content. Guard per-field using the field's own watermark.
|
|
@@ -1001,6 +1072,13 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1001
1072
|
queryClient,
|
|
1002
1073
|
],
|
|
1003
1074
|
);
|
|
1075
|
+
const queueDocumentSave = useCallback(
|
|
1076
|
+
(title: string, content: string, options: DocumentSaveOptions = {}) =>
|
|
1077
|
+
enqueueDocumentSave(documentSaveQueueRef, () =>
|
|
1078
|
+
saveDocumentImmediately(title, content, options),
|
|
1079
|
+
),
|
|
1080
|
+
[saveDocumentImmediately],
|
|
1081
|
+
);
|
|
1004
1082
|
const flushPendingDocumentSave = useCallback(
|
|
1005
1083
|
(pending: PendingDocumentSave) => {
|
|
1006
1084
|
if (!pending.canEditWhenQueued) return;
|
|
@@ -1019,7 +1097,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1019
1097
|
const pending: PendingDocumentSave = {
|
|
1020
1098
|
title,
|
|
1021
1099
|
content,
|
|
1022
|
-
save:
|
|
1100
|
+
save: queueDocumentSave,
|
|
1023
1101
|
canEditWhenQueued: canEditRef.current,
|
|
1024
1102
|
timeout: setTimeout(() => {
|
|
1025
1103
|
if (pendingDocumentSaveRef.current === pending) {
|
|
@@ -1032,7 +1110,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1032
1110
|
pendingDocumentSaveRef.current = pending;
|
|
1033
1111
|
saveTimeoutRef.current = pending.timeout;
|
|
1034
1112
|
},
|
|
1035
|
-
[flushPendingDocumentSave,
|
|
1113
|
+
[flushPendingDocumentSave, queueDocumentSave],
|
|
1036
1114
|
);
|
|
1037
1115
|
|
|
1038
1116
|
useEffect(() => {
|
|
@@ -1328,13 +1406,10 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1328
1406
|
}
|
|
1329
1407
|
localContentRef.current = newContent;
|
|
1330
1408
|
setLocalContent(newContent);
|
|
1331
|
-
const result = await
|
|
1332
|
-
localTitleRef.current,
|
|
1333
|
-
newContent,
|
|
1334
|
-
);
|
|
1409
|
+
const result = await queueDocumentSave(localTitleRef.current, newContent);
|
|
1335
1410
|
return result.contentPersisted;
|
|
1336
1411
|
},
|
|
1337
|
-
[editorCanEdit,
|
|
1412
|
+
[editorCanEdit, queueDocumentSave],
|
|
1338
1413
|
);
|
|
1339
1414
|
|
|
1340
1415
|
// Comments state — pending comment from text selection
|
|
@@ -1909,7 +1984,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1909
1984
|
{!bodyHydrationPending &&
|
|
1910
1985
|
!isLocalFileDocument &&
|
|
1911
1986
|
canEdit &&
|
|
1912
|
-
|
|
1987
|
+
!collabSynced ? (
|
|
1913
1988
|
<div
|
|
1914
1989
|
className="mt-4 inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
|
|
1915
1990
|
role="status"
|
|
@@ -2595,7 +2595,20 @@ function DateValueEditor({
|
|
|
2595
2595
|
className="grid gap-2"
|
|
2596
2596
|
onSubmit={(event) => {
|
|
2597
2597
|
event.preventDefault();
|
|
2598
|
-
|
|
2598
|
+
const formData = new FormData(event.currentTarget);
|
|
2599
|
+
const submittedStartValue = formData.get("property-start-value");
|
|
2600
|
+
const submittedEndValue = formData.get("property-end-value");
|
|
2601
|
+
|
|
2602
|
+
// Native date controls can update their displayed DOM value before
|
|
2603
|
+
// React receives the corresponding change event. Read the submitted
|
|
2604
|
+
// form so Save never clears a date that is visibly present.
|
|
2605
|
+
void save(
|
|
2606
|
+
buildValue(
|
|
2607
|
+
typeof submittedStartValue === "string" ? submittedStartValue : "",
|
|
2608
|
+
typeof submittedEndValue === "string" ? submittedEndValue : "",
|
|
2609
|
+
formData.has("property-include-time"),
|
|
2610
|
+
),
|
|
2611
|
+
);
|
|
2599
2612
|
}}
|
|
2600
2613
|
>
|
|
2601
2614
|
<div className="grid grid-cols-2 gap-1">
|
|
@@ -2690,6 +2703,7 @@ function DateValueEditor({
|
|
|
2690
2703
|
</label>
|
|
2691
2704
|
<label className="flex items-center gap-2 rounded-md border px-2.5 py-2 text-sm">
|
|
2692
2705
|
<input
|
|
2706
|
+
name="property-include-time"
|
|
2693
2707
|
type="checkbox"
|
|
2694
2708
|
checked={includeTime}
|
|
2695
2709
|
onChange={(event) => {
|