@agent-native/core 0.101.7 → 0.101.10
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 +22 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
- package/corpus/templates/content/app/i18n-data.ts +287 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
PreviewBuilderSourceReviewRequest,
|
|
7
|
+
PreviewBuilderSourceReviewResponse,
|
|
8
|
+
} from "../shared/api.js";
|
|
9
|
+
import {
|
|
10
|
+
getContentDatabaseSourceSnapshotForWrite,
|
|
11
|
+
resolveDatabaseForSourceMutation,
|
|
12
|
+
} from "./_database-source-utils.js";
|
|
13
|
+
import {
|
|
14
|
+
BUILDER_SOURCE_REVIEW_PREPARE_LIMIT,
|
|
15
|
+
buildBuilderSourceReviewPayload,
|
|
16
|
+
reviewPreparePriority,
|
|
17
|
+
withAuthoritativeBuilderTargetRows,
|
|
18
|
+
} from "./prepare-builder-source-review.js";
|
|
19
|
+
|
|
20
|
+
export default defineAction({
|
|
21
|
+
description:
|
|
22
|
+
"Preview complete Builder CMS review diffs, including document body changes, without approving or writing anything.",
|
|
23
|
+
schema: z.object({
|
|
24
|
+
databaseId: z.string().optional().describe("Database ID"),
|
|
25
|
+
documentId: z.string().optional().describe("Database document/page ID"),
|
|
26
|
+
sourceId: z.string().optional().describe("Specific Builder source ID"),
|
|
27
|
+
scope: z.enum(["selected", "all"]).optional().default("all"),
|
|
28
|
+
documentIds: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.max(BUILDER_SOURCE_REVIEW_PREPARE_LIMIT)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Optional selected document IDs that bound the preview"),
|
|
33
|
+
}),
|
|
34
|
+
http: { method: "GET" },
|
|
35
|
+
readOnly: true,
|
|
36
|
+
run: async (
|
|
37
|
+
args: PreviewBuilderSourceReviewRequest,
|
|
38
|
+
): Promise<PreviewBuilderSourceReviewResponse> => {
|
|
39
|
+
const database = await resolveDatabaseForSourceMutation(args);
|
|
40
|
+
if (!database) throw new Error("Database not found.");
|
|
41
|
+
await assertAccess("document", database.documentId, "editor");
|
|
42
|
+
|
|
43
|
+
const source = await getContentDatabaseSourceSnapshotForWrite(
|
|
44
|
+
database,
|
|
45
|
+
args.sourceId,
|
|
46
|
+
);
|
|
47
|
+
if (!source || source.sourceType !== "builder-cms") {
|
|
48
|
+
throw new Error("Attach a Builder CMS source before reviewing updates.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const selectedDocumentIds = new Set(args.documentIds ?? []);
|
|
52
|
+
if (args.scope === "selected" && selectedDocumentIds.size === 0) {
|
|
53
|
+
throw new Error("Select at least one Builder row before reviewing it.");
|
|
54
|
+
}
|
|
55
|
+
const allReviewableChanges = source.changeSets.filter((changeSet) => {
|
|
56
|
+
if (
|
|
57
|
+
changeSet.direction !== "outbound" ||
|
|
58
|
+
(changeSet.state !== "pending_push" &&
|
|
59
|
+
changeSet.state !== "staged_revision" &&
|
|
60
|
+
changeSet.state !== "approved")
|
|
61
|
+
) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
args.scope === "all" ||
|
|
66
|
+
(!!changeSet.documentId &&
|
|
67
|
+
selectedDocumentIds.has(changeSet.documentId))
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
const changeSets = [...allReviewableChanges]
|
|
71
|
+
.sort(
|
|
72
|
+
(left, right) =>
|
|
73
|
+
reviewPreparePriority(left) - reviewPreparePriority(right),
|
|
74
|
+
)
|
|
75
|
+
.slice(0, BUILDER_SOURCE_REVIEW_PREPARE_LIMIT);
|
|
76
|
+
const authoritativeSource = await withAuthoritativeBuilderTargetRows({
|
|
77
|
+
source,
|
|
78
|
+
changeSets,
|
|
79
|
+
});
|
|
80
|
+
const review =
|
|
81
|
+
changeSets.length > 0
|
|
82
|
+
? buildBuilderSourceReviewPayload({
|
|
83
|
+
source: authoritativeSource,
|
|
84
|
+
changeSets,
|
|
85
|
+
})
|
|
86
|
+
: null;
|
|
87
|
+
if (review) {
|
|
88
|
+
review.totalRowCount = allReviewableChanges.length;
|
|
89
|
+
review.preparedRowLimit = changeSets.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sourceId: source.id,
|
|
94
|
+
sourceTable: source.sourceTable,
|
|
95
|
+
changeSetIds: changeSets.map((changeSet) => changeSet.id),
|
|
96
|
+
review,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -38,7 +38,7 @@ function executableWriteModes(
|
|
|
38
38
|
|
|
39
39
|
export default defineAction({
|
|
40
40
|
description:
|
|
41
|
-
"Set the tiered Builder CMS write mode for one source. Writes stay off by default and
|
|
41
|
+
"Set the tiered Builder CMS write mode for one source. Writes stay off by default and require document administrator access to enable or change.",
|
|
42
42
|
schema: z.object({
|
|
43
43
|
databaseId: z.string().optional().describe("Database ID"),
|
|
44
44
|
documentId: z.string().optional().describe("Database document/page ID"),
|
|
@@ -79,7 +79,7 @@ export default defineAction({
|
|
|
79
79
|
): Promise<ContentDatabaseResponse> => {
|
|
80
80
|
const database = await resolveDatabaseForSourceMutation(args);
|
|
81
81
|
if (!database) throw new Error("Database not found.");
|
|
82
|
-
await assertAccess("document", database.documentId, "
|
|
82
|
+
await assertAccess("document", database.documentId, "admin");
|
|
83
83
|
|
|
84
84
|
const db = getDb();
|
|
85
85
|
const source = await getExistingSourceForWrite(database.id, args.sourceId);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server";
|
|
6
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
7
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
11
|
+
import { readPreviewDocumentDraft } from "./_preview-document-draft.js";
|
|
12
|
+
|
|
13
|
+
const draftPayload = z.object({
|
|
14
|
+
title: z.string().max(10_000),
|
|
15
|
+
content: z.string().max(500_000),
|
|
16
|
+
baseDocumentUpdatedAt: z.string().max(100).nullable(),
|
|
17
|
+
loadedContentWasEmpty: z.boolean(),
|
|
18
|
+
deferredReason: z.enum(["hydration", "conflict"]).nullable(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function draftId() {
|
|
22
|
+
return crypto.randomUUID();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default defineAction({
|
|
26
|
+
description:
|
|
27
|
+
"Atomically save or delete the current user's private preview draft.",
|
|
28
|
+
schema: z.discriminatedUnion("operation", [
|
|
29
|
+
z.object({
|
|
30
|
+
operation: z.literal("upsert"),
|
|
31
|
+
documentId: z.string().min(1),
|
|
32
|
+
expectedVersion: z.number().int().positive().nullable(),
|
|
33
|
+
draft: draftPayload,
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
operation: z.literal("delete"),
|
|
37
|
+
documentId: z.string().min(1),
|
|
38
|
+
expectedVersion: z.number().int().positive(),
|
|
39
|
+
expectedTitle: z.string().max(10_000),
|
|
40
|
+
expectedContent: z.string().max(500_000),
|
|
41
|
+
}),
|
|
42
|
+
]),
|
|
43
|
+
agentTool: false,
|
|
44
|
+
toolCallable: false,
|
|
45
|
+
run: async (args, ctx) => {
|
|
46
|
+
const userEmail = getRequestUserEmail();
|
|
47
|
+
const orgId = getRequestOrgId() ?? "";
|
|
48
|
+
if (!userEmail) throw new Error("Not authenticated.");
|
|
49
|
+
await assertAccess("document", args.documentId, "editor");
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const ownerFilter = and(
|
|
52
|
+
eq(schema.documentPreviewDrafts.ownerEmail, userEmail),
|
|
53
|
+
eq(schema.documentPreviewDrafts.orgId, orgId),
|
|
54
|
+
eq(schema.documentPreviewDrafts.documentId, args.documentId),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (args.operation === "delete") {
|
|
58
|
+
const deleted = await db
|
|
59
|
+
.delete(schema.documentPreviewDrafts)
|
|
60
|
+
.where(
|
|
61
|
+
and(
|
|
62
|
+
ownerFilter,
|
|
63
|
+
eq(schema.documentPreviewDrafts.version, args.expectedVersion),
|
|
64
|
+
eq(schema.documentPreviewDrafts.title, args.expectedTitle),
|
|
65
|
+
eq(schema.documentPreviewDrafts.content, args.expectedContent),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
.returning({ id: schema.documentPreviewDrafts.id });
|
|
69
|
+
if (deleted.length > 0)
|
|
70
|
+
return { status: "deleted" as const, draft: null };
|
|
71
|
+
return {
|
|
72
|
+
status: "conflict" as const,
|
|
73
|
+
draft: await readPreviewDocumentDraft(
|
|
74
|
+
userEmail,
|
|
75
|
+
orgId,
|
|
76
|
+
args.documentId,
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
if (args.expectedVersion === null) {
|
|
83
|
+
const inserted = await db
|
|
84
|
+
.insert(schema.documentPreviewDrafts)
|
|
85
|
+
.values({
|
|
86
|
+
id: draftId(),
|
|
87
|
+
ownerEmail: userEmail,
|
|
88
|
+
orgId,
|
|
89
|
+
documentId: args.documentId,
|
|
90
|
+
title: args.draft.title,
|
|
91
|
+
content: args.draft.content,
|
|
92
|
+
baseDocumentUpdatedAt: args.draft.baseDocumentUpdatedAt,
|
|
93
|
+
loadedContentWasEmpty: args.draft.loadedContentWasEmpty ? 1 : 0,
|
|
94
|
+
deferredReason: args.draft.deferredReason,
|
|
95
|
+
version: 1,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
})
|
|
99
|
+
.onConflictDoNothing({
|
|
100
|
+
target: [
|
|
101
|
+
schema.documentPreviewDrafts.ownerEmail,
|
|
102
|
+
schema.documentPreviewDrafts.orgId,
|
|
103
|
+
schema.documentPreviewDrafts.documentId,
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
.returning({ version: schema.documentPreviewDrafts.version });
|
|
107
|
+
if (inserted.length > 0) {
|
|
108
|
+
return {
|
|
109
|
+
status: "saved" as const,
|
|
110
|
+
draft: await readPreviewDocumentDraft(
|
|
111
|
+
userEmail,
|
|
112
|
+
orgId,
|
|
113
|
+
args.documentId,
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const updated = await db
|
|
119
|
+
.update(schema.documentPreviewDrafts)
|
|
120
|
+
.set({
|
|
121
|
+
title: args.draft.title,
|
|
122
|
+
content: args.draft.content,
|
|
123
|
+
baseDocumentUpdatedAt: args.draft.baseDocumentUpdatedAt,
|
|
124
|
+
loadedContentWasEmpty: args.draft.loadedContentWasEmpty ? 1 : 0,
|
|
125
|
+
deferredReason: args.draft.deferredReason,
|
|
126
|
+
version: sql`${schema.documentPreviewDrafts.version} + 1`,
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
})
|
|
129
|
+
.where(
|
|
130
|
+
and(
|
|
131
|
+
ownerFilter,
|
|
132
|
+
eq(schema.documentPreviewDrafts.version, args.expectedVersion),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
.returning({ version: schema.documentPreviewDrafts.version });
|
|
136
|
+
if (updated.length > 0) {
|
|
137
|
+
return {
|
|
138
|
+
status: "saved" as const,
|
|
139
|
+
draft: await readPreviewDocumentDraft(
|
|
140
|
+
userEmail,
|
|
141
|
+
orgId,
|
|
142
|
+
args.documentId,
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
status: "conflict" as const,
|
|
150
|
+
draft: await readPreviewDocumentDraft(userEmail, orgId, args.documentId),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
@@ -489,6 +489,24 @@ export function filesMediaEditorValue(value: DocumentProperty["value"]) {
|
|
|
489
489
|
return filesMediaItems(value).join("\n");
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
export function isValidFilesMediaLink(value: string) {
|
|
493
|
+
try {
|
|
494
|
+
const url = new URL(value.trim());
|
|
495
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function mergeFilesMediaItems(items: string[], pendingLink: string) {
|
|
502
|
+
const trimmed = pendingLink.trim();
|
|
503
|
+
if (!trimmed || !isValidFilesMediaLink(trimmed)) return items;
|
|
504
|
+
if (items.some((item) => item.toLowerCase() === trimmed.toLowerCase())) {
|
|
505
|
+
return items;
|
|
506
|
+
}
|
|
507
|
+
return [...items, trimmed];
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
export function filesMediaLabel(value: string) {
|
|
493
511
|
const trimmed = value.trim();
|
|
494
512
|
if (!trimmed) return "File";
|
|
@@ -2060,16 +2078,8 @@ function FilesMediaValueEditor({
|
|
|
2060
2078
|
}, []);
|
|
2061
2079
|
|
|
2062
2080
|
function addItem(value: string) {
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
setItems((current) => {
|
|
2066
|
-
if (
|
|
2067
|
-
current.some((item) => item.toLowerCase() === trimmed.toLowerCase())
|
|
2068
|
-
) {
|
|
2069
|
-
return current;
|
|
2070
|
-
}
|
|
2071
|
-
return [...current, trimmed];
|
|
2072
|
-
});
|
|
2081
|
+
if (!isValidFilesMediaLink(value)) return;
|
|
2082
|
+
setItems((current) => mergeFilesMediaItems(current, value));
|
|
2073
2083
|
setLinkValue("");
|
|
2074
2084
|
}
|
|
2075
2085
|
|
|
@@ -2126,11 +2136,11 @@ function FilesMediaValueEditor({
|
|
|
2126
2136
|
className="grid gap-3"
|
|
2127
2137
|
onSubmit={(event) => {
|
|
2128
2138
|
event.preventDefault();
|
|
2129
|
-
if (linkValue.trim()) {
|
|
2130
|
-
|
|
2139
|
+
if (linkValue.trim() && !isValidFilesMediaLink(linkValue)) {
|
|
2140
|
+
linkInputRef.current?.reportValidity();
|
|
2131
2141
|
return;
|
|
2132
2142
|
}
|
|
2133
|
-
void save();
|
|
2143
|
+
void save(mergeFilesMediaItems(items, linkValue));
|
|
2134
2144
|
}}
|
|
2135
2145
|
>
|
|
2136
2146
|
<div className="grid max-h-48 gap-1 overflow-auto">
|
|
@@ -2174,9 +2184,11 @@ function FilesMediaValueEditor({
|
|
|
2174
2184
|
<div className="flex gap-1">
|
|
2175
2185
|
<Input
|
|
2176
2186
|
ref={linkInputRef}
|
|
2177
|
-
aria-label={t("editor.properties.
|
|
2187
|
+
aria-label={t("editor.properties.editValue", {
|
|
2178
2188
|
name: property.definition.name,
|
|
2179
2189
|
})}
|
|
2190
|
+
type="url"
|
|
2191
|
+
pattern="[hH][tT][tT][pP][sS]?://.*"
|
|
2180
2192
|
value={linkValue}
|
|
2181
2193
|
placeholder={t("editor.properties.pasteFileOrMediaLink")}
|
|
2182
2194
|
onChange={(event) => setLinkValue(event.target.value)}
|
|
@@ -2193,7 +2205,7 @@ function FilesMediaValueEditor({
|
|
|
2193
2205
|
size="sm"
|
|
2194
2206
|
className="shrink-0"
|
|
2195
2207
|
onClick={() => addItem(linkValue)}
|
|
2196
|
-
disabled={!linkValue
|
|
2208
|
+
disabled={!isValidFilesMediaLink(linkValue) || mutation.isPending}
|
|
2197
2209
|
>
|
|
2198
2210
|
<IconPlus className="size-3.5" />
|
|
2199
2211
|
{t("editor.properties.add")}
|
|
@@ -924,6 +924,28 @@ export function shouldPersistLocalFileEditorUpdate({
|
|
|
924
924
|
return Boolean(transactionUiEvent);
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
+
function isEffectivelyEmptyEditorContent(value: string): boolean {
|
|
928
|
+
const normalized = value.trim();
|
|
929
|
+
return normalized === "" || normalized === "<empty-block/>";
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function shouldPersistEffectivelyEmptyEditorUpdate({
|
|
933
|
+
nextContent,
|
|
934
|
+
userInitiated,
|
|
935
|
+
}: {
|
|
936
|
+
nextContent: string;
|
|
937
|
+
userInitiated: boolean;
|
|
938
|
+
}): boolean {
|
|
939
|
+
if (!isEffectivelyEmptyEditorContent(nextContent)) return true;
|
|
940
|
+
// Empty editor state is never worth persisting without a user gesture. This
|
|
941
|
+
// also covers the preview remount window where `content` can briefly be an
|
|
942
|
+
// empty list snapshot even though its retained save controller still has a
|
|
943
|
+
// rich confirmed baseline. Comparing only to this render's prop would let
|
|
944
|
+
// that mount-time filler mark the retained controller dirty and its
|
|
945
|
+
// flush-on-release path would then overwrite SQL.
|
|
946
|
+
return userInitiated;
|
|
947
|
+
}
|
|
948
|
+
|
|
927
949
|
function isActiveSlashCommandDraft(editor: CoreEditor): boolean {
|
|
928
950
|
const { state } = editor;
|
|
929
951
|
if (!state.selection.empty) return false;
|
|
@@ -1086,7 +1108,9 @@ function getVisualEditorPlaceholder({
|
|
|
1086
1108
|
if (level === 1) return "Heading 1";
|
|
1087
1109
|
if (level === 2) return "Heading 2";
|
|
1088
1110
|
if (level === 3) return "Heading 3";
|
|
1089
|
-
return "Heading 4";
|
|
1111
|
+
if (level === 4) return "Heading 4";
|
|
1112
|
+
if (level === 5) return "Heading 5";
|
|
1113
|
+
return "Heading 6";
|
|
1090
1114
|
}
|
|
1091
1115
|
|
|
1092
1116
|
if (
|
|
@@ -1300,6 +1324,7 @@ export function createVisualEditorExtensions({
|
|
|
1300
1324
|
starterKit: {
|
|
1301
1325
|
blockquote: false,
|
|
1302
1326
|
paragraph: false,
|
|
1327
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
1303
1328
|
horizontalRule: {},
|
|
1304
1329
|
dropcursor: { color: false, width: 3, class: "notion-dropcursor" },
|
|
1305
1330
|
},
|
|
@@ -1742,7 +1767,11 @@ export function VisualEditor({
|
|
|
1742
1767
|
const persistEditorContent = useCallback(
|
|
1743
1768
|
(
|
|
1744
1769
|
editorToPersist: CoreEditor,
|
|
1745
|
-
options?: {
|
|
1770
|
+
options?: {
|
|
1771
|
+
markdown?: string;
|
|
1772
|
+
immediate?: boolean;
|
|
1773
|
+
userInitiated?: boolean;
|
|
1774
|
+
},
|
|
1746
1775
|
) => {
|
|
1747
1776
|
const guards = guardsRef.current;
|
|
1748
1777
|
if (!guards) return false;
|
|
@@ -1750,6 +1779,20 @@ export function VisualEditor({
|
|
|
1750
1779
|
const normalized =
|
|
1751
1780
|
options?.markdown ?? docToNfm(editorToPersist.getJSON() as any);
|
|
1752
1781
|
if (localFileMode && normalized === content) return true;
|
|
1782
|
+
// TipTap/Yjs can emit a local-looking empty-paragraph transaction while
|
|
1783
|
+
// an editor is mounting or reconciling. Content serializes that filler
|
|
1784
|
+
// as `<empty-block/>`, so the generic whitespace-only collab guard does
|
|
1785
|
+
// not catch it. Never let that lifecycle normalization clear a saved
|
|
1786
|
+
// body. A real Select All/Delete (or Cut) records user intent through
|
|
1787
|
+
// the DOM handlers below and is still allowed to persist normally.
|
|
1788
|
+
if (
|
|
1789
|
+
!shouldPersistEffectivelyEmptyEditorUpdate({
|
|
1790
|
+
nextContent: normalized,
|
|
1791
|
+
userInitiated: options?.userInitiated === true,
|
|
1792
|
+
})
|
|
1793
|
+
) {
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1753
1796
|
if (options?.immediate && onSaveContentRef.current) {
|
|
1754
1797
|
return onSaveContentRef.current(normalized);
|
|
1755
1798
|
}
|
|
@@ -1923,7 +1966,12 @@ export function VisualEditor({
|
|
|
1923
1966
|
return;
|
|
1924
1967
|
}
|
|
1925
1968
|
if (isActiveSlashCommandDraft(editor)) return;
|
|
1926
|
-
persistEditorContent(editor
|
|
1969
|
+
persistEditorContent(editor, {
|
|
1970
|
+
userInitiated:
|
|
1971
|
+
transaction.getMeta(LOCAL_FILE_USER_EDIT_META) === true ||
|
|
1972
|
+
Date.now() - lastUserEditIntentAtRef.current < 2000 ||
|
|
1973
|
+
Boolean(transaction.getMeta("uiEvent")),
|
|
1974
|
+
});
|
|
1927
1975
|
},
|
|
1928
1976
|
});
|
|
1929
1977
|
|
|
@@ -2239,10 +2287,14 @@ export function VisualEditor({
|
|
|
2239
2287
|
documentId={documentId}
|
|
2240
2288
|
notionPageId={notionPageId}
|
|
2241
2289
|
onDraftCommitted={() => {
|
|
2242
|
-
void persistEditorContent(editor);
|
|
2290
|
+
void persistEditorContent(editor, { userInitiated: true });
|
|
2243
2291
|
}}
|
|
2244
2292
|
onDraftPersisted={(markdown) =>
|
|
2245
|
-
persistEditorContent(editor, {
|
|
2293
|
+
persistEditorContent(editor, {
|
|
2294
|
+
markdown,
|
|
2295
|
+
immediate: true,
|
|
2296
|
+
userInitiated: true,
|
|
2297
|
+
})
|
|
2246
2298
|
}
|
|
2247
2299
|
/>
|
|
2248
2300
|
) : null}
|
|
@@ -89,6 +89,33 @@ export function previewBodyHydrationIsPending(args: {
|
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A draft that began from an empty source snapshot must not automatically
|
|
94
|
+
* replace a non-empty Builder body that arrived while hydration was running.
|
|
95
|
+
* Keep the draft dirty/recoverable and let the user decide which body to keep.
|
|
96
|
+
*/
|
|
97
|
+
export function previewDraftConflictsWithHydratedBody(args: {
|
|
98
|
+
loadedContent: string | null | undefined;
|
|
99
|
+
loadedUpdatedAt: string | null | undefined;
|
|
100
|
+
loadedContentWasEmpty: boolean | undefined;
|
|
101
|
+
pendingContent: string | null | undefined;
|
|
102
|
+
hydratedContent: string | null | undefined;
|
|
103
|
+
hydratedUpdatedAt: string | null | undefined;
|
|
104
|
+
}) {
|
|
105
|
+
const sourceChangedSinceDraftLoaded =
|
|
106
|
+
(!!args.loadedUpdatedAt &&
|
|
107
|
+
!!args.hydratedUpdatedAt &&
|
|
108
|
+
args.loadedUpdatedAt !== args.hydratedUpdatedAt) ||
|
|
109
|
+
args.loadedContent !== args.hydratedContent;
|
|
110
|
+
return (
|
|
111
|
+
!isEffectivelyEmptyDocumentContent(args.pendingContent) &&
|
|
112
|
+
args.pendingContent !== args.hydratedContent &&
|
|
113
|
+
sourceChangedSinceDraftLoaded &&
|
|
114
|
+
(args.loadedContentWasEmpty === true ||
|
|
115
|
+
args.loadedContent !== args.hydratedContent)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
export function previewBodyHydrationIsTerminalError(args: {
|
|
93
120
|
item: Pick<ContentDatabaseItem, "bodyHydration" | "document">;
|
|
94
121
|
document: Pick<Document, "databaseMembership"> | null | undefined;
|