@agent-native/core 0.119.6 → 0.120.0
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 +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/index.ts +64 -3
- package/corpus/core/src/templates/default/package.json +1 -1
- package/corpus/templates/assets/package.json +1 -1
- package/corpus/templates/brain/package.json +1 -1
- package/corpus/templates/calendar/package.json +1 -1
- package/corpus/templates/clips/package.json +1 -1
- package/corpus/templates/content/actions/add-database-item.ts +1 -0
- package/corpus/templates/content/actions/set-document-property.ts +0 -4
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +10 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +10 -4
- package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +5 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +52 -33
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +16 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +16 -7
- package/corpus/templates/content/app/hooks/use-create-page.ts +8 -3
- package/corpus/templates/content/app/hooks/use-document-properties.ts +1 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +3 -1
- package/corpus/templates/content/app/lib/optimistic-document.ts +29 -0
- package/corpus/templates/content/changelog/2026-07-21-editing-a-database-property-no-longer-refreshes-unrelated-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-23-new-database-ready.md +6 -0
- package/corpus/templates/content/package.json +1 -1
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/design/package.json +1 -1
- package/corpus/templates/dispatch/package.json +1 -1
- package/corpus/templates/forms/package.json +1 -1
- package/corpus/templates/macros/package.json +1 -1
- package/corpus/templates/mail/package.json +1 -1
- package/corpus/templates/slides/package.json +1 -1
- package/dist/cli/index.js +53 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/templates/default/package.json +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +64 -3
- package/src/templates/default/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.120.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 20ebb96: `agent-native dev --inspect` (and `--inspect-brk`, optionally `=<port>`) now
|
|
8
|
+
attaches the Node inspector to **only** the Nitro API-server process, on a
|
|
9
|
+
single known port (default 9229). It selects Nitro's `node-process` dev runner
|
|
10
|
+
so the server is a real, attachable process, and injects `NODE_OPTIONS` through
|
|
11
|
+
a Vite preload that runs before Vite's own startup — so Vite, pnpm, and the CLI
|
|
12
|
+
are never inspected and there is exactly one debugger target. Set
|
|
13
|
+
`NITRO_DEV_RUNNER` yourself to override the runner.
|
|
14
|
+
|
|
3
15
|
## 0.119.6
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.120.0",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -276,6 +276,22 @@ function findViteBin(): string {
|
|
|
276
276
|
return findBinUpwards("vite") ?? "vite";
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
function findViteJsEntry(): string | null {
|
|
280
|
+
try {
|
|
281
|
+
const require = createRequire(path.join(process.cwd(), "package.json"));
|
|
282
|
+
const pkgJsonPath = require.resolve("vite/package.json");
|
|
283
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as {
|
|
284
|
+
bin?: string | Record<string, string>;
|
|
285
|
+
};
|
|
286
|
+
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.vite;
|
|
287
|
+
if (!rel) return null;
|
|
288
|
+
const entry = path.join(path.dirname(pkgJsonPath), rel);
|
|
289
|
+
return fs.existsSync(entry) ? entry : null;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
function findTsxBin(): string {
|
|
280
296
|
const localTsx = path.resolve("node_modules/.bin/tsx");
|
|
281
297
|
if (fs.existsSync(localTsx)) return localTsx;
|
|
@@ -386,15 +402,31 @@ function isWorkspaceRoot(): boolean {
|
|
|
386
402
|
}
|
|
387
403
|
}
|
|
388
404
|
|
|
405
|
+
function extractNodeInspectFlag(args: string[]): {
|
|
406
|
+
inspectFlag: string | null;
|
|
407
|
+
rest: string[];
|
|
408
|
+
} {
|
|
409
|
+
const rest: string[] = [];
|
|
410
|
+
let inspectFlag: string | null = null;
|
|
411
|
+
for (const arg of args) {
|
|
412
|
+
if (/^--inspect(-brk)?(=.+)?$/.test(arg)) {
|
|
413
|
+
inspectFlag = arg;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
rest.push(arg);
|
|
417
|
+
}
|
|
418
|
+
return { inspectFlag, rest };
|
|
419
|
+
}
|
|
420
|
+
|
|
389
421
|
function run(
|
|
390
422
|
cmd: string,
|
|
391
423
|
cmdArgs: string[],
|
|
392
|
-
opts?: { stdio?: "inherit" | "pipe" },
|
|
424
|
+
opts?: { stdio?: "inherit" | "pipe"; env?: NodeJS.ProcessEnv },
|
|
393
425
|
) {
|
|
394
426
|
const child = spawn(cmd, cmdArgs, {
|
|
395
427
|
stdio: opts?.stdio ?? "inherit",
|
|
396
428
|
shell: process.platform === "win32",
|
|
397
|
-
env: process.env,
|
|
429
|
+
env: opts?.env ?? process.env,
|
|
398
430
|
});
|
|
399
431
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
400
432
|
// Forward signals to child so Cmd+C doesn't leave zombie processes holding ports
|
|
@@ -545,7 +577,36 @@ switch (command) {
|
|
|
545
577
|
break;
|
|
546
578
|
}
|
|
547
579
|
const vite = findViteBin();
|
|
548
|
-
|
|
580
|
+
const { inspectFlag, rest } = extractNodeInspectFlag(args);
|
|
581
|
+
if (!inspectFlag) {
|
|
582
|
+
run(vite, rest);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
const viteJsEntry = findViteJsEntry();
|
|
586
|
+
if (!viteJsEntry) {
|
|
587
|
+
console.warn(
|
|
588
|
+
"[agent-native] Could not resolve Vite's JS entry; starting dev " +
|
|
589
|
+
"server without the debugger.",
|
|
590
|
+
);
|
|
591
|
+
run(vite, rest);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
// Attach inspect flag to server process (not Vite or Nitro process)
|
|
595
|
+
const parsed = inspectFlag.match(/^--(inspect(?:-brk)?)(?:=(.+))?$/);
|
|
596
|
+
const kind = parsed?.[1] ?? "inspect";
|
|
597
|
+
const target = parsed?.[2] ?? "9229";
|
|
598
|
+
const directive = `--${kind}=${target}`;
|
|
599
|
+
const preload =
|
|
600
|
+
"data:text/javascript," +
|
|
601
|
+
encodeURIComponent(
|
|
602
|
+
`process.env.NODE_OPTIONS=((process.env.NODE_OPTIONS??"")+" ${directive}").trim();`,
|
|
603
|
+
);
|
|
604
|
+
const env = {
|
|
605
|
+
...process.env,
|
|
606
|
+
NITRO_DEV_RUNNER: process.env.NITRO_DEV_RUNNER ?? "node-process",
|
|
607
|
+
};
|
|
608
|
+
console.log(`[agent-native] API server debugger listening on ${target}`);
|
|
609
|
+
run(process.execPath, ["--import", preload, viteJsEntry, ...rest], { env });
|
|
549
610
|
break;
|
|
550
611
|
}
|
|
551
612
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
3
|
import { and, eq } from "drizzle-orm";
|
|
5
4
|
import { z } from "zod";
|
|
@@ -87,7 +86,6 @@ export default defineAction({
|
|
|
87
86
|
now,
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
|
-
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
91
89
|
return {
|
|
92
90
|
documentId,
|
|
93
91
|
databaseId: database.id,
|
|
@@ -133,8 +131,6 @@ export default defineAction({
|
|
|
133
131
|
});
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
137
|
-
|
|
138
134
|
return {
|
|
139
135
|
documentId,
|
|
140
136
|
databaseId: database.id,
|
|
@@ -38,6 +38,7 @@ const BLOCK_FIELD_DRAG_THRESHOLD = 6;
|
|
|
38
38
|
|
|
39
39
|
interface DocumentBlockFieldsProps {
|
|
40
40
|
documentId: string;
|
|
41
|
+
databaseDocumentId: string;
|
|
41
42
|
canEdit: boolean;
|
|
42
43
|
/**
|
|
43
44
|
* The fully-wired collaborative body editor for the primary "Content" field.
|
|
@@ -238,6 +239,7 @@ export function blockFieldsRenderState(args: {
|
|
|
238
239
|
*/
|
|
239
240
|
export function DocumentBlockFields({
|
|
240
241
|
documentId,
|
|
242
|
+
databaseDocumentId,
|
|
241
243
|
canEdit,
|
|
242
244
|
primaryEditor,
|
|
243
245
|
}: DocumentBlockFieldsProps) {
|
|
@@ -303,6 +305,7 @@ export function DocumentBlockFields({
|
|
|
303
305
|
// field while SAVING to another across an identity change.
|
|
304
306
|
key={`${documentId}:${state.field.definition.id}`}
|
|
305
307
|
documentId={documentId}
|
|
308
|
+
databaseDocumentId={databaseDocumentId}
|
|
306
309
|
property={state.field}
|
|
307
310
|
canEdit={canEdit}
|
|
308
311
|
/>
|
|
@@ -318,6 +321,7 @@ export function DocumentBlockFields({
|
|
|
318
321
|
return (
|
|
319
322
|
<MultiBlockFields
|
|
320
323
|
documentId={documentId}
|
|
324
|
+
databaseDocumentId={databaseDocumentId}
|
|
321
325
|
canEdit={canEdit}
|
|
322
326
|
blockFields={state.fields}
|
|
323
327
|
primaryEditor={primaryEditor}
|
|
@@ -329,12 +333,14 @@ export function DocumentBlockFields({
|
|
|
329
333
|
|
|
330
334
|
function MultiBlockFields({
|
|
331
335
|
documentId,
|
|
336
|
+
databaseDocumentId,
|
|
332
337
|
canEdit,
|
|
333
338
|
blockFields,
|
|
334
339
|
primaryEditor,
|
|
335
340
|
t,
|
|
336
341
|
}: {
|
|
337
342
|
documentId: string;
|
|
343
|
+
databaseDocumentId: string;
|
|
338
344
|
canEdit: boolean;
|
|
339
345
|
blockFields: DocumentProperty[];
|
|
340
346
|
primaryEditor: ReactNode;
|
|
@@ -504,6 +510,7 @@ function MultiBlockFields({
|
|
|
504
510
|
// doc's edits to the old field's closure.
|
|
505
511
|
key={`${documentId}:${property.definition.id}`}
|
|
506
512
|
documentId={documentId}
|
|
513
|
+
databaseDocumentId={databaseDocumentId}
|
|
507
514
|
property={property}
|
|
508
515
|
canEdit={canEdit}
|
|
509
516
|
/>
|
|
@@ -789,14 +796,16 @@ export function useBlockFieldEditor({
|
|
|
789
796
|
*/
|
|
790
797
|
function AdditionalBlockEditor({
|
|
791
798
|
documentId,
|
|
799
|
+
databaseDocumentId,
|
|
792
800
|
property,
|
|
793
801
|
canEdit,
|
|
794
802
|
}: {
|
|
795
803
|
documentId: string;
|
|
804
|
+
databaseDocumentId: string;
|
|
796
805
|
property: DocumentProperty;
|
|
797
806
|
canEdit: boolean;
|
|
798
807
|
}) {
|
|
799
|
-
const setProperty = useSetDocumentProperty(documentId);
|
|
808
|
+
const setProperty = useSetDocumentProperty(documentId, databaseDocumentId);
|
|
800
809
|
const propertyId = property.definition.id;
|
|
801
810
|
const initialContent =
|
|
802
811
|
typeof property.value === "string" ? property.value : "";
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
canWriteLinkedLocalSource,
|
|
81
81
|
writeDocumentToLinkedLocalSource,
|
|
82
82
|
} from "@/lib/local-content-source-files";
|
|
83
|
+
import { isDatabaseChoicePending } from "@/lib/optimistic-document";
|
|
83
84
|
import { cn } from "@/lib/utils";
|
|
84
85
|
|
|
85
86
|
import {
|
|
@@ -1519,6 +1520,10 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1519
1520
|
);
|
|
1520
1521
|
const defaultIconKind = documentEditorDefaultIconKind(document);
|
|
1521
1522
|
const isDatabasePage = Boolean(document.database);
|
|
1523
|
+
const databaseChoicePending = isDatabaseChoicePending(
|
|
1524
|
+
document,
|
|
1525
|
+
createDatabase.isPending,
|
|
1526
|
+
);
|
|
1522
1527
|
const showNewDocumentTypeChooser =
|
|
1523
1528
|
canEdit &&
|
|
1524
1529
|
!isLocalFileDocument &&
|
|
@@ -1791,12 +1796,10 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1791
1796
|
type="button"
|
|
1792
1797
|
variant="outline"
|
|
1793
1798
|
className="justify-start gap-2"
|
|
1794
|
-
disabled={
|
|
1795
|
-
!editorCanEdit || createDatabase.isPending
|
|
1796
|
-
}
|
|
1799
|
+
disabled={!editorCanEdit || databaseChoicePending}
|
|
1797
1800
|
onClick={() => void handleChooseDatabase()}
|
|
1798
1801
|
>
|
|
1799
|
-
{
|
|
1802
|
+
{databaseChoicePending ? (
|
|
1800
1803
|
<IconLoader2 className="animate-spin" />
|
|
1801
1804
|
) : (
|
|
1802
1805
|
<IconDatabase />
|
|
@@ -1867,6 +1870,9 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1867
1870
|
return (
|
|
1868
1871
|
<DocumentBlockFields
|
|
1869
1872
|
documentId={documentId}
|
|
1873
|
+
databaseDocumentId={
|
|
1874
|
+
document.databaseMembership.databaseDocumentId
|
|
1875
|
+
}
|
|
1870
1876
|
canEdit={editorCanEdit}
|
|
1871
1877
|
primaryEditor={primaryEditor}
|
|
1872
1878
|
/>
|
|
@@ -32,7 +32,11 @@ export function DocumentInfoPanel({
|
|
|
32
32
|
onSave={onSaveDescription}
|
|
33
33
|
/>
|
|
34
34
|
{document.databaseMembership && !isLocalFileDocument ? (
|
|
35
|
-
<DocumentProperties
|
|
35
|
+
<DocumentProperties
|
|
36
|
+
documentId={document.id}
|
|
37
|
+
databaseDocumentId={document.databaseMembership.databaseDocumentId}
|
|
38
|
+
canEdit={canEdit}
|
|
39
|
+
/>
|
|
36
40
|
) : null}
|
|
37
41
|
</div>
|
|
38
42
|
);
|
|
@@ -165,6 +165,7 @@ function tWithFallback(
|
|
|
165
165
|
|
|
166
166
|
interface DocumentPropertiesProps {
|
|
167
167
|
documentId: string;
|
|
168
|
+
databaseDocumentId: string;
|
|
168
169
|
canEdit: boolean;
|
|
169
170
|
popoversPortalled?: boolean;
|
|
170
171
|
}
|
|
@@ -796,6 +797,7 @@ function scalarPlaceholder(type: DocumentPropertyType, t: TFunction) {
|
|
|
796
797
|
|
|
797
798
|
export function DocumentProperties({
|
|
798
799
|
documentId,
|
|
800
|
+
databaseDocumentId,
|
|
799
801
|
canEdit,
|
|
800
802
|
popoversPortalled = true,
|
|
801
803
|
}: DocumentPropertiesProps) {
|
|
@@ -826,6 +828,7 @@ export function DocumentProperties({
|
|
|
826
828
|
key={property.definition.id}
|
|
827
829
|
property={property}
|
|
828
830
|
documentId={documentId}
|
|
831
|
+
databaseDocumentId={databaseDocumentId}
|
|
829
832
|
canEdit={canEdit}
|
|
830
833
|
popoversPortalled={popoversPortalled}
|
|
831
834
|
t={t}
|
|
@@ -927,12 +930,14 @@ function HiddenPropertiesMenu({
|
|
|
927
930
|
function PropertyRow({
|
|
928
931
|
property,
|
|
929
932
|
documentId,
|
|
933
|
+
databaseDocumentId,
|
|
930
934
|
canEdit,
|
|
931
935
|
popoversPortalled,
|
|
932
936
|
t,
|
|
933
937
|
}: {
|
|
934
938
|
property: DocumentProperty;
|
|
935
939
|
documentId: string;
|
|
940
|
+
databaseDocumentId: string;
|
|
936
941
|
canEdit: boolean;
|
|
937
942
|
popoversPortalled: boolean;
|
|
938
943
|
t: TFunction;
|
|
@@ -978,6 +983,7 @@ function PropertyRow({
|
|
|
978
983
|
<PropertyValuePopover
|
|
979
984
|
property={property}
|
|
980
985
|
documentId={documentId}
|
|
986
|
+
databaseDocumentId={databaseDocumentId}
|
|
981
987
|
portalled={popoversPortalled}
|
|
982
988
|
>
|
|
983
989
|
{value}
|
|
@@ -197,6 +197,7 @@ import {
|
|
|
197
197
|
} from "@/hooks/use-document-properties";
|
|
198
198
|
import {
|
|
199
199
|
isDocumentUpdateConflict,
|
|
200
|
+
type DocumentUpdateResult,
|
|
200
201
|
useDeleteDocument,
|
|
201
202
|
useDocument,
|
|
202
203
|
seedDatabaseItemDocumentCaches,
|
|
@@ -254,7 +255,10 @@ import { EmojiPicker } from "../EmojiPicker";
|
|
|
254
255
|
import {
|
|
255
256
|
createPreviewDocumentSaveController,
|
|
256
257
|
deferredPreviewDocumentSave,
|
|
258
|
+
type PreviewDocumentPayload,
|
|
257
259
|
type PreviewDocumentSaveAdapter,
|
|
260
|
+
type PreviewDocumentSaveDeferred,
|
|
261
|
+
type PreviewDocumentSaveSuccess,
|
|
258
262
|
} from "../previewDocumentSaveController";
|
|
259
263
|
import {
|
|
260
264
|
acquirePreviewDocumentSaveController,
|
|
@@ -436,7 +440,8 @@ export function databaseCreatedItemForImmediatePreview(
|
|
|
436
440
|
if (returnedItem) return returnedItem;
|
|
437
441
|
if (!response.createdItemId || !response.createdDocumentId) return null;
|
|
438
442
|
|
|
439
|
-
const now =
|
|
443
|
+
const now =
|
|
444
|
+
response.createdDocumentUpdatedAt ?? args.now ?? new Date().toISOString();
|
|
440
445
|
const position = Math.max(
|
|
441
446
|
0,
|
|
442
447
|
(response.pagination?.totalItems ?? response.items.length + 1) - 1,
|
|
@@ -473,6 +478,42 @@ export function databaseCreatedItemForImmediatePreview(
|
|
|
473
478
|
};
|
|
474
479
|
}
|
|
475
480
|
|
|
481
|
+
export function previewDocumentSaveResult(args: {
|
|
482
|
+
result: DocumentUpdateResult;
|
|
483
|
+
payload: PreviewDocumentPayload;
|
|
484
|
+
baseline?: PreviewDocumentPayload;
|
|
485
|
+
contentChanged: boolean;
|
|
486
|
+
}): PreviewDocumentSaveDeferred | PreviewDocumentSaveSuccess {
|
|
487
|
+
const serverDocument = isDocumentUpdateConflict(args.result)
|
|
488
|
+
? args.result.document
|
|
489
|
+
: args.result;
|
|
490
|
+
const titleSaveObservedExternalBody =
|
|
491
|
+
!args.contentChanged && serverDocument.content !== args.payload.content;
|
|
492
|
+
|
|
493
|
+
if (isDocumentUpdateConflict(args.result) || titleSaveObservedExternalBody) {
|
|
494
|
+
return deferredPreviewDocumentSave("conflict", {
|
|
495
|
+
lastSaved: args.baseline ?? args.payload,
|
|
496
|
+
pending: {
|
|
497
|
+
title: serverDocument.title,
|
|
498
|
+
content: serverDocument.content,
|
|
499
|
+
loadedUpdatedAt: serverDocument.updatedAt,
|
|
500
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
501
|
+
serverDocument.content,
|
|
502
|
+
),
|
|
503
|
+
},
|
|
504
|
+
deferredReason: "conflict",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
outcome: "saved",
|
|
510
|
+
loadedUpdatedAt: args.result.updatedAt,
|
|
511
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
512
|
+
args.result.content,
|
|
513
|
+
),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
476
517
|
export function databaseBuilderHydrationSourceForItem(
|
|
477
518
|
item: ContentDatabaseItem,
|
|
478
519
|
sources: ContentDatabaseSource[],
|
|
@@ -4098,8 +4139,6 @@ function DatabaseItemPreview({
|
|
|
4098
4139
|
// ever touch its own row's state. See previewDocumentSaveRegistry.
|
|
4099
4140
|
const updateDocumentRef = useRef(updateDocument);
|
|
4100
4141
|
updateDocumentRef.current = updateDocument;
|
|
4101
|
-
const queryClientRef = useRef(queryClient);
|
|
4102
|
-
queryClientRef.current = queryClient;
|
|
4103
4142
|
const bodyHydrationPendingRef = useRef(bodyHydrationPending);
|
|
4104
4143
|
bodyHydrationPendingRef.current = bodyHydrationPending;
|
|
4105
4144
|
const draftVersionsRef = useRef<Map<string, number | null>>(new Map());
|
|
@@ -4244,30 +4283,14 @@ function DatabaseItemPreview({
|
|
|
4244
4283
|
},
|
|
4245
4284
|
{
|
|
4246
4285
|
onSuccess: (result) => {
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4256
|
-
result.document.content,
|
|
4257
|
-
),
|
|
4258
|
-
},
|
|
4259
|
-
deferredReason: "conflict",
|
|
4260
|
-
}),
|
|
4261
|
-
);
|
|
4262
|
-
return;
|
|
4263
|
-
}
|
|
4264
|
-
resolve({
|
|
4265
|
-
outcome: "saved" as const,
|
|
4266
|
-
loadedUpdatedAt: result.updatedAt,
|
|
4267
|
-
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4268
|
-
result.content,
|
|
4269
|
-
),
|
|
4270
|
-
});
|
|
4286
|
+
resolve(
|
|
4287
|
+
previewDocumentSaveResult({
|
|
4288
|
+
result,
|
|
4289
|
+
payload,
|
|
4290
|
+
baseline,
|
|
4291
|
+
contentChanged,
|
|
4292
|
+
}),
|
|
4293
|
+
);
|
|
4271
4294
|
},
|
|
4272
4295
|
onError: reject,
|
|
4273
4296
|
},
|
|
@@ -4276,12 +4299,6 @@ function DatabaseItemPreview({
|
|
|
4276
4299
|
onSaved: (persistedPayload) => {
|
|
4277
4300
|
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4278
4301
|
if (controller) enqueueDraftWrite(controller, "delete", persistedPayload);
|
|
4279
|
-
void queryClientRef.current.invalidateQueries({
|
|
4280
|
-
queryKey: contentDatabaseQueryKey(databaseDocumentId),
|
|
4281
|
-
});
|
|
4282
|
-
void queryClientRef.current.invalidateQueries({
|
|
4283
|
-
queryKey: ["action", "list-documents"],
|
|
4284
|
-
});
|
|
4285
4302
|
},
|
|
4286
4303
|
onError: (err) => {
|
|
4287
4304
|
toast.error(dbText("failedToSavePagePreview"), {
|
|
@@ -4876,6 +4893,7 @@ function DatabaseItemPreview({
|
|
|
4876
4893
|
{previewDocument.databaseMembership ? (
|
|
4877
4894
|
<DocumentProperties
|
|
4878
4895
|
documentId={previewDocument.id}
|
|
4896
|
+
databaseDocumentId={databaseDocumentId}
|
|
4879
4897
|
canEdit={previewCanEdit}
|
|
4880
4898
|
popoversPortalled={false}
|
|
4881
4899
|
/>
|
|
@@ -4913,6 +4931,7 @@ function DatabaseItemPreview({
|
|
|
4913
4931
|
const editor = previewDocument.databaseMembership ? (
|
|
4914
4932
|
<DocumentBlockFields
|
|
4915
4933
|
documentId={previewDocument.id}
|
|
4934
|
+
databaseDocumentId={databaseDocumentId}
|
|
4916
4935
|
canEdit={previewCanEdit}
|
|
4917
4936
|
primaryEditor={primaryEditor}
|
|
4918
4937
|
/>
|
|
@@ -130,8 +130,8 @@ export interface PreviewDocumentSaveController {
|
|
|
130
130
|
/** Adopt `payload` as the confirmed-saved baseline (no save scheduled). */
|
|
131
131
|
mark(payload: PreviewDocumentPayload): void;
|
|
132
132
|
/**
|
|
133
|
-
* Adopt a fresher server baseline while retaining the user
|
|
134
|
-
*
|
|
133
|
+
* Adopt a fresher server baseline while retaining only fields the user
|
|
134
|
+
* changed locally. Used only after an explicit "keep local draft" choice.
|
|
135
135
|
*/
|
|
136
136
|
rebasePending(payload: PreviewDocumentPayload): void;
|
|
137
137
|
/** Replace callbacks captured by an older preview mount. */
|
|
@@ -261,8 +261,7 @@ export function createPreviewDocumentSaveController(
|
|
|
261
261
|
// the controller was created/last marked — e.g. "empty" for a
|
|
262
262
|
// brand-new page — forever, even after real content has been saved.
|
|
263
263
|
const success = asSaveSuccess(result);
|
|
264
|
-
|
|
265
|
-
...attempted,
|
|
264
|
+
const savedMetadata = {
|
|
266
265
|
...(success?.loadedUpdatedAt !== undefined
|
|
267
266
|
? { loadedUpdatedAt: success.loadedUpdatedAt }
|
|
268
267
|
: {}),
|
|
@@ -270,6 +269,15 @@ export function createPreviewDocumentSaveController(
|
|
|
270
269
|
? { loadedContentWasEmpty: success.loadedContentWasEmpty }
|
|
271
270
|
: {}),
|
|
272
271
|
};
|
|
272
|
+
lastSaved = {
|
|
273
|
+
...attempted,
|
|
274
|
+
...savedMetadata,
|
|
275
|
+
};
|
|
276
|
+
// A later keystroke starts from `pending`, including while this save is
|
|
277
|
+
// in flight. Rebase that trailing payload onto our own successful write
|
|
278
|
+
// so its next CAS does not mistake the preceding save for an external
|
|
279
|
+
// change.
|
|
280
|
+
pending = { ...pending, ...savedMetadata };
|
|
273
281
|
hasSavedLocally = true;
|
|
274
282
|
deferredReason = null;
|
|
275
283
|
inFlight = null;
|
|
@@ -336,9 +344,13 @@ export function createPreviewDocumentSaveController(
|
|
|
336
344
|
},
|
|
337
345
|
rebasePending(payload: PreviewDocumentPayload) {
|
|
338
346
|
clearTimer();
|
|
347
|
+
const titleChangedLocally = pending.title !== lastSaved.title;
|
|
348
|
+
const contentChangedLocally = pending.content !== lastSaved.content;
|
|
339
349
|
lastSaved = { ...payload };
|
|
340
350
|
pending = {
|
|
341
351
|
...pending,
|
|
352
|
+
title: titleChangedLocally ? pending.title : payload.title,
|
|
353
|
+
content: contentChangedLocally ? pending.content : payload.content,
|
|
342
354
|
loadedUpdatedAt: payload.loadedUpdatedAt,
|
|
343
355
|
loadedContentWasEmpty: payload.loadedContentWasEmpty,
|
|
344
356
|
};
|
|
@@ -121,6 +121,10 @@ import {
|
|
|
121
121
|
filterDocumentTreeDocuments,
|
|
122
122
|
} from "@/hooks/use-documents";
|
|
123
123
|
import { useLocalStorage } from "@/hooks/use-local-storage";
|
|
124
|
+
import {
|
|
125
|
+
markDocumentCreationPending,
|
|
126
|
+
shouldCreateDocumentOptimistically,
|
|
127
|
+
} from "@/lib/optimistic-document";
|
|
124
128
|
import { cn } from "@/lib/utils";
|
|
125
129
|
|
|
126
130
|
import {
|
|
@@ -747,7 +751,12 @@ export function DocumentSidebar({
|
|
|
747
751
|
optimisticId?: string,
|
|
748
752
|
rootFilesDatabaseId?: string,
|
|
749
753
|
) => {
|
|
750
|
-
if (
|
|
754
|
+
if (
|
|
755
|
+
!shouldCreateDocumentOptimistically({
|
|
756
|
+
localFileMode,
|
|
757
|
+
filesDatabaseId: rootFilesDatabaseId,
|
|
758
|
+
})
|
|
759
|
+
) {
|
|
751
760
|
try {
|
|
752
761
|
const created = await createDocument.mutateAsync({
|
|
753
762
|
title: "",
|
|
@@ -774,7 +783,7 @@ export function DocumentSidebar({
|
|
|
774
783
|
|
|
775
784
|
const id = optimisticId ?? nanoid();
|
|
776
785
|
const now = new Date().toISOString();
|
|
777
|
-
const tempDoc
|
|
786
|
+
const tempDoc = markDocumentCreationPending({
|
|
778
787
|
id,
|
|
779
788
|
parentId: parentId ?? null,
|
|
780
789
|
title: "",
|
|
@@ -789,7 +798,7 @@ export function DocumentSidebar({
|
|
|
789
798
|
canManage: true,
|
|
790
799
|
createdAt: now,
|
|
791
800
|
updatedAt: now,
|
|
792
|
-
};
|
|
801
|
+
});
|
|
793
802
|
|
|
794
803
|
// Optimistically inject into caches so UI updates immediately
|
|
795
804
|
queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
|
|
@@ -824,14 +833,14 @@ export function DocumentSidebar({
|
|
|
824
833
|
spaceId: parentId ? undefined : rootSpaceId,
|
|
825
834
|
});
|
|
826
835
|
const nextId = created?.id || id;
|
|
836
|
+
queryClient.setQueryData(
|
|
837
|
+
["action", "get-document", { id: nextId }],
|
|
838
|
+
created,
|
|
839
|
+
);
|
|
827
840
|
if (nextId !== id) {
|
|
828
841
|
queryClient.removeQueries({
|
|
829
842
|
queryKey: ["action", "get-document", { id }],
|
|
830
843
|
});
|
|
831
|
-
queryClient.setQueryData(
|
|
832
|
-
["action", "get-document", { id: nextId }],
|
|
833
|
-
created,
|
|
834
|
-
);
|
|
835
844
|
navigateToDocument(nextId);
|
|
836
845
|
}
|
|
837
846
|
// Replace optimistic doc with real server doc + clear any 404 error
|