@agent-native/core 0.82.0 → 0.83.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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +34 -12
- package/corpus/templates/analytics/actions/hubspot-deals.ts +64 -4
- package/corpus/templates/content/.env.local.example +6 -0
- package/corpus/templates/content/AGENTS.md +10 -1
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +14 -1
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +43 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +79 -7
- package/corpus/templates/content/actions/_database-source-utils.ts +987 -22
- package/corpus/templates/content/actions/_database-utils.ts +41 -0
- package/corpus/templates/content/actions/_local-file-documents.ts +74 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +33 -8
- package/corpus/templates/content/actions/change-content-database-source-role.ts +14 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +71 -3
- package/corpus/templates/content/actions/list-content-databases.ts +41 -33
- package/corpus/templates/content/actions/process-builder-body-hydration.ts +47 -0
- package/corpus/templates/content/actions/update-document.ts +93 -0
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +277 -0
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +2 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +45 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +5 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +92 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +5 -3
- package/corpus/templates/content/app/components/editor/extensions/registryBlocks.ts +41 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +4 -0
- package/corpus/templates/content/app/global.css +10 -3
- package/corpus/templates/content/app/hooks/use-content-database.ts +24 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +24 -0
- package/corpus/templates/content/app/i18n-data.ts +212 -0
- package/corpus/templates/content/app/lib/content-command-search.ts +64 -0
- package/corpus/templates/content/app/root.tsx +236 -8
- package/corpus/templates/content/changelog/2026-06-29-builder-cms-sources-now-sync-article-bodies-through-content.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-article-media-now-render-as-images-and-embeds-when-r.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-builder-source-components-now-render-as-preserved-preview-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-06-30-cmd-k-search-now-opens-from-the-editor-and-finds-real-content.md +6 -0
- package/corpus/templates/content/package.json +3 -1
- package/corpus/templates/content/scripts/check-native-deps.mjs +57 -0
- package/corpus/templates/content/scripts/dev-database.mjs +83 -0
- package/corpus/templates/content/scripts/seed-demo-user.mjs +107 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +33 -0
- package/corpus/templates/content/shared/api.ts +45 -0
- package/corpus/templates/content/shared/builder-mdx.ts +905 -5
- package/corpus/templates/content/shared/nfm-registry.ts +2 -0
- package/corpus/templates/content/shared/source-component-block.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +5 -1
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +229 -131
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +103 -6
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +6 -3
- package/corpus/templates/design/app/pages/DesignEditor.tsx +382 -72
- package/corpus/templates/design/changelog/2026-06-30-canvas-editing-now-keeps-undo-redo-and-cross-screen-layer-mo.md +6 -0
- package/dist/client/CommandMenu.d.ts +6 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +25 -14
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseDocumentHideFromSearch,
|
|
11
11
|
} from "../server/lib/documents.js";
|
|
12
12
|
import type { DocumentUpdateResponse } from "../shared/api.js";
|
|
13
|
+
import { BUILDER_CMS_BODY_CONTENT_KEY } from "./_builder-cms-source-adapter.js";
|
|
13
14
|
import { reconcileInlineDatabasesForDocument } from "./_content-database-lifecycle.js";
|
|
14
15
|
import { serializeDocumentSource } from "./_document-source.js";
|
|
15
16
|
import {
|
|
@@ -33,6 +34,60 @@ function canManageRole(role: string) {
|
|
|
33
34
|
return role === "owner" || role === "admin";
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function builderBodyWithoutImageSourceComponentMarkers(
|
|
38
|
+
content: string | null | undefined,
|
|
39
|
+
) {
|
|
40
|
+
return (content ?? "")
|
|
41
|
+
.replace(/(?:^|\n)<SourceComponent\b[\s\S]*?\/>[ \t]*(?=\n|$)/g, (marker) =>
|
|
42
|
+
marker.includes('componentName="Image"') ? "\n" : marker,
|
|
43
|
+
)
|
|
44
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
45
|
+
.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function builderBodyWithoutMarkdownImages(content: string | null | undefined) {
|
|
49
|
+
return (content ?? "")
|
|
50
|
+
.replace(
|
|
51
|
+
/(?:^|\n)!\[(?:\\.|[^\]\\])*\]\(\S+?(?:\s+"[^"]*")?\)[ \t]*(?=\n|$)/g,
|
|
52
|
+
"\n",
|
|
53
|
+
)
|
|
54
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizedBuilderBodyProse(content: string | null | undefined) {
|
|
59
|
+
return (content ?? "")
|
|
60
|
+
.normalize("NFKC")
|
|
61
|
+
.replace(/\u00a0/g, " ")
|
|
62
|
+
.replace(/\s+/g, " ")
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isStaleBuilderImageSourceComponentSave(args: {
|
|
67
|
+
incomingContent: string;
|
|
68
|
+
currentContent: string;
|
|
69
|
+
sourceContent: string | null | undefined;
|
|
70
|
+
}) {
|
|
71
|
+
const sourceContent = args.sourceContent ?? "";
|
|
72
|
+
const currentMatchesSource =
|
|
73
|
+
normalizedBuilderBodyProse(args.currentContent) ===
|
|
74
|
+
normalizedBuilderBodyProse(sourceContent);
|
|
75
|
+
if (
|
|
76
|
+
!args.incomingContent.includes('componentName="Image"') ||
|
|
77
|
+
!sourceContent.includes("![") ||
|
|
78
|
+
sourceContent.includes('componentName="Image"') ||
|
|
79
|
+
!currentMatchesSource
|
|
80
|
+
) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
normalizedBuilderBodyProse(
|
|
85
|
+
builderBodyWithoutImageSourceComponentMarkers(args.incomingContent),
|
|
86
|
+
) ===
|
|
87
|
+
normalizedBuilderBodyProse(builderBodyWithoutMarkdownImages(sourceContent))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
36
91
|
export default defineAction({
|
|
37
92
|
description:
|
|
38
93
|
"Update an existing document's title, content, icon, or favorite status.",
|
|
@@ -79,6 +134,44 @@ export default defineAction({
|
|
|
79
134
|
content = content.slice(h1Match[0].length).trimStart();
|
|
80
135
|
}
|
|
81
136
|
}
|
|
137
|
+
if (
|
|
138
|
+
content.includes('componentName="Image"') &&
|
|
139
|
+
existing.content.includes("![")
|
|
140
|
+
) {
|
|
141
|
+
const [builderBody] = await db
|
|
142
|
+
.select({
|
|
143
|
+
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
144
|
+
})
|
|
145
|
+
.from(schema.contentDatabaseSourceRows)
|
|
146
|
+
.innerJoin(
|
|
147
|
+
schema.contentDatabaseSources,
|
|
148
|
+
eq(
|
|
149
|
+
schema.contentDatabaseSources.id,
|
|
150
|
+
schema.contentDatabaseSourceRows.sourceId,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
.where(
|
|
154
|
+
and(
|
|
155
|
+
eq(schema.contentDatabaseSourceRows.documentId, id),
|
|
156
|
+
eq(schema.contentDatabaseSources.sourceType, "builder-cms"),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
.limit(1);
|
|
160
|
+
const sourceValues = JSON.parse(
|
|
161
|
+
builderBody?.sourceValuesJson ?? "{}",
|
|
162
|
+
) as Record<string, unknown>;
|
|
163
|
+
const sourceContent = sourceValues[BUILDER_CMS_BODY_CONTENT_KEY];
|
|
164
|
+
if (
|
|
165
|
+
typeof sourceContent === "string" &&
|
|
166
|
+
isStaleBuilderImageSourceComponentSave({
|
|
167
|
+
incomingContent: content,
|
|
168
|
+
currentContent: existing.content,
|
|
169
|
+
sourceContent,
|
|
170
|
+
})
|
|
171
|
+
) {
|
|
172
|
+
content = existing.content;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
82
175
|
}
|
|
83
176
|
|
|
84
177
|
// Detect actual changes — a no-op call (e.g. the editor echoing back the
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { BlockReadProps, BlockSpec } from "@agent-native/core/blocks";
|
|
2
|
+
import { useT } from "@agent-native/core/client";
|
|
3
|
+
import {
|
|
4
|
+
sourceComponentBlockConfig,
|
|
5
|
+
type SourceComponentData,
|
|
6
|
+
} from "@shared/source-component-block";
|
|
7
|
+
import {
|
|
8
|
+
IconBox,
|
|
9
|
+
IconDatabase,
|
|
10
|
+
IconExternalLink,
|
|
11
|
+
IconPhoto,
|
|
12
|
+
IconLink,
|
|
13
|
+
IconTable,
|
|
14
|
+
} from "@tabler/icons-react";
|
|
15
|
+
|
|
16
|
+
function providerLabel(provider: string) {
|
|
17
|
+
if (provider === "builder") return "Builder";
|
|
18
|
+
return provider.trim() || "Source";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safeHttpUrl(value: string | undefined) {
|
|
22
|
+
if (!value) return null;
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(value);
|
|
25
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
26
|
+
return url;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function youtubeEmbedUrl(value: string | undefined) {
|
|
33
|
+
const url = safeHttpUrl(value);
|
|
34
|
+
if (!url) return null;
|
|
35
|
+
if (url.hostname === "youtu.be") {
|
|
36
|
+
const id = url.pathname.replace(/^\/+/, "").split("/")[0];
|
|
37
|
+
return id ? `https://www.youtube.com/embed/${id}` : null;
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
url.hostname === "youtube.com" ||
|
|
41
|
+
url.hostname === "www.youtube.com" ||
|
|
42
|
+
url.hostname === "m.youtube.com"
|
|
43
|
+
) {
|
|
44
|
+
const id = url.searchParams.get("v");
|
|
45
|
+
if (id) return `https://www.youtube.com/embed/${id}`;
|
|
46
|
+
if (url.pathname.startsWith("/embed/")) return url.toString();
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function SourceImagePreview({ url, alt }: { url: string; alt?: string }) {
|
|
52
|
+
return (
|
|
53
|
+
<figure className="mt-2 overflow-hidden rounded-md border bg-background">
|
|
54
|
+
<img
|
|
55
|
+
src={url}
|
|
56
|
+
alt={alt || ""}
|
|
57
|
+
loading="lazy"
|
|
58
|
+
className="max-h-[520px] w-full object-contain"
|
|
59
|
+
/>
|
|
60
|
+
{alt ? (
|
|
61
|
+
<figcaption className="border-t px-2.5 py-2 text-xs leading-5 text-muted-foreground">
|
|
62
|
+
{alt}
|
|
63
|
+
</figcaption>
|
|
64
|
+
) : null}
|
|
65
|
+
</figure>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function SourceEmbedPreview({ url }: { url: string }) {
|
|
70
|
+
const embedUrl = youtubeEmbedUrl(url);
|
|
71
|
+
if (embedUrl) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="mt-2 overflow-hidden rounded-md border bg-background">
|
|
74
|
+
<iframe
|
|
75
|
+
data-plan-interactive
|
|
76
|
+
className="aspect-video w-full"
|
|
77
|
+
src={embedUrl}
|
|
78
|
+
title="Builder embedded video"
|
|
79
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
80
|
+
allowFullScreen
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<a
|
|
87
|
+
data-plan-interactive
|
|
88
|
+
className="mt-2 flex min-w-0 items-center gap-2 rounded-md border bg-background px-2.5 py-2 text-xs text-muted-foreground hover:text-foreground"
|
|
89
|
+
href={url}
|
|
90
|
+
target="_blank"
|
|
91
|
+
rel="noreferrer"
|
|
92
|
+
>
|
|
93
|
+
<IconLink className="size-4 shrink-0" />
|
|
94
|
+
<span className="truncate">{url}</span>
|
|
95
|
+
</a>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function SourceComponentPreview({ data }: { data: SourceComponentData }) {
|
|
100
|
+
const t = useT();
|
|
101
|
+
const preview = data.preview;
|
|
102
|
+
const previewUrl = preview?.url ?? data.previewUrl;
|
|
103
|
+
const safePreviewUrl = safeHttpUrl(previewUrl)?.toString();
|
|
104
|
+
const imageAlt =
|
|
105
|
+
preview?.fields?.find((field) => field.label.toLowerCase() === "alt")
|
|
106
|
+
?.value ?? preview?.summary;
|
|
107
|
+
if (data.componentName === "Image" && safePreviewUrl) {
|
|
108
|
+
return <SourceImagePreview url={safePreviewUrl} alt={imageAlt} />;
|
|
109
|
+
}
|
|
110
|
+
if (preview?.kind === "table" && preview.table) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="mt-2 overflow-hidden rounded-md border bg-background text-xs">
|
|
113
|
+
<table className="w-full table-fixed border-collapse">
|
|
114
|
+
<thead className="bg-muted/60 text-muted-foreground">
|
|
115
|
+
<tr>
|
|
116
|
+
{preview.table.columns.map((column) => (
|
|
117
|
+
<th
|
|
118
|
+
key={column.id}
|
|
119
|
+
className="border-b px-2 py-1.5 text-left font-medium"
|
|
120
|
+
>
|
|
121
|
+
<span className="block truncate">{column.label}</span>
|
|
122
|
+
</th>
|
|
123
|
+
))}
|
|
124
|
+
</tr>
|
|
125
|
+
</thead>
|
|
126
|
+
<tbody>
|
|
127
|
+
{preview.table.rows.length ? (
|
|
128
|
+
preview.table.rows.map((row, index) => (
|
|
129
|
+
<tr key={index} className="border-b last:border-b-0">
|
|
130
|
+
{preview.table?.columns.map((column) => (
|
|
131
|
+
<td key={column.id} className="px-2 py-1.5">
|
|
132
|
+
<span className="block truncate">
|
|
133
|
+
{row[column.id] ?? ""}
|
|
134
|
+
</span>
|
|
135
|
+
</td>
|
|
136
|
+
))}
|
|
137
|
+
</tr>
|
|
138
|
+
))
|
|
139
|
+
) : (
|
|
140
|
+
<tr>
|
|
141
|
+
<td
|
|
142
|
+
className="px-2 py-2 text-muted-foreground"
|
|
143
|
+
colSpan={Math.max(preview.table.columns.length, 1)}
|
|
144
|
+
>
|
|
145
|
+
{t("editor.sourceComponent.noPreviewRows")}
|
|
146
|
+
</td>
|
|
147
|
+
</tr>
|
|
148
|
+
)}
|
|
149
|
+
</tbody>
|
|
150
|
+
</table>
|
|
151
|
+
{preview.table.truncated ? (
|
|
152
|
+
<div className="border-t px-2 py-1.5 text-muted-foreground">
|
|
153
|
+
{t("editor.sourceComponent.previewTruncated")}
|
|
154
|
+
</div>
|
|
155
|
+
) : null}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (preview?.kind === "embed" && preview.url) {
|
|
160
|
+
return <SourceEmbedPreview url={preview.url} />;
|
|
161
|
+
}
|
|
162
|
+
if (preview?.fields?.length) {
|
|
163
|
+
return (
|
|
164
|
+
<div className="mt-2 flex flex-wrap gap-1.5 text-xs text-muted-foreground">
|
|
165
|
+
{preview.fields.map((field) => (
|
|
166
|
+
<span
|
|
167
|
+
key={`${field.label}:${field.value}`}
|
|
168
|
+
className="rounded border bg-background px-1.5 py-0.5"
|
|
169
|
+
>
|
|
170
|
+
{field.label}: {field.value}
|
|
171
|
+
</span>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (data.previewKind === "table") {
|
|
177
|
+
return (
|
|
178
|
+
<div className="mt-2 flex flex-wrap items-center gap-2 rounded-md border bg-background px-2.5 py-2 text-xs text-muted-foreground">
|
|
179
|
+
<IconTable className="size-4 shrink-0" />
|
|
180
|
+
{(data.previewItems?.length ? data.previewItems : ["Table"]).map(
|
|
181
|
+
(item) => (
|
|
182
|
+
<span
|
|
183
|
+
key={item}
|
|
184
|
+
className="rounded border bg-muted/40 px-1.5 py-0.5"
|
|
185
|
+
>
|
|
186
|
+
{item}
|
|
187
|
+
</span>
|
|
188
|
+
),
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (data.previewKind === "embed" && data.previewUrl) {
|
|
194
|
+
return <SourceEmbedPreview url={data.previewUrl} />;
|
|
195
|
+
}
|
|
196
|
+
if (data.previewItems?.length) {
|
|
197
|
+
return (
|
|
198
|
+
<div className="mt-2 flex flex-wrap gap-1.5 text-xs text-muted-foreground">
|
|
199
|
+
{data.previewItems.map((item) => (
|
|
200
|
+
<span
|
|
201
|
+
key={item}
|
|
202
|
+
className="rounded border bg-background px-1.5 py-0.5"
|
|
203
|
+
>
|
|
204
|
+
{item}
|
|
205
|
+
</span>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function SourceComponentRead({
|
|
214
|
+
data,
|
|
215
|
+
blockId,
|
|
216
|
+
}: BlockReadProps<SourceComponentData>) {
|
|
217
|
+
const t = useT();
|
|
218
|
+
const label =
|
|
219
|
+
data.title ||
|
|
220
|
+
data.componentName ||
|
|
221
|
+
t("editor.sourceComponent.defaultTitle");
|
|
222
|
+
const status = data.preview?.status ?? data.previewStatus ?? "unavailable";
|
|
223
|
+
const statusLabel =
|
|
224
|
+
status === "available"
|
|
225
|
+
? t("editor.sourceComponent.previewAvailable")
|
|
226
|
+
: status === "warning"
|
|
227
|
+
? t("editor.sourceComponent.needsAttention")
|
|
228
|
+
: t("editor.sourceComponent.previewUnavailable");
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<section
|
|
232
|
+
data-block-id={blockId}
|
|
233
|
+
data-plan-interactive
|
|
234
|
+
className="an-block rounded-md border border-dashed bg-muted/20 px-3 py-3 text-sm"
|
|
235
|
+
>
|
|
236
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
237
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
238
|
+
{data.componentName === "Image" ? (
|
|
239
|
+
<IconPhoto className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
240
|
+
) : (
|
|
241
|
+
<IconBox className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
242
|
+
)}
|
|
243
|
+
<div className="min-w-0">
|
|
244
|
+
<div className="truncate font-medium text-foreground">{label}</div>
|
|
245
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
|
|
246
|
+
<span className="inline-flex items-center gap-1">
|
|
247
|
+
<IconDatabase className="size-3.5" />
|
|
248
|
+
{providerLabel(data.provider)}
|
|
249
|
+
</span>
|
|
250
|
+
<span>{data.componentName}</span>
|
|
251
|
+
{data.sourceLabel ? <span>{data.sourceLabel}</span> : null}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<span className="shrink-0 rounded-full border bg-background px-2 py-0.5 text-[11px] text-muted-foreground">
|
|
256
|
+
{statusLabel}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
{data.summary ? (
|
|
260
|
+
<div className="mt-2 text-sm leading-6 text-muted-foreground">
|
|
261
|
+
{data.summary}
|
|
262
|
+
</div>
|
|
263
|
+
) : null}
|
|
264
|
+
<SourceComponentPreview data={data} />
|
|
265
|
+
<div className="mt-2 flex min-w-0 items-center gap-1 truncate text-[11px] text-muted-foreground">
|
|
266
|
+
<IconExternalLink className="size-3.5 shrink-0" />
|
|
267
|
+
<span className="truncate">{data.rawRef}</span>
|
|
268
|
+
</div>
|
|
269
|
+
</section>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const sourceComponentBlock: BlockSpec<SourceComponentData> = {
|
|
274
|
+
...sourceComponentBlockConfig,
|
|
275
|
+
Read: SourceComponentRead,
|
|
276
|
+
icon: IconBox,
|
|
277
|
+
};
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
ContentBlockMarkdownEditor,
|
|
27
27
|
} from "./ContentBlockMarkdown";
|
|
28
28
|
import { inlineDatabaseBlock } from "./InlineDatabaseBlock";
|
|
29
|
+
import { sourceComponentBlock } from "./SourceComponentBlock";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Content's BROWSER block registry. Registers the same structured-block library
|
|
@@ -58,6 +59,7 @@ registerLibraryBlocks(contentBlockRegistry, {
|
|
|
58
59
|
for (const block of builderDocsBlocks) {
|
|
59
60
|
contentBlockRegistry.register(block);
|
|
60
61
|
}
|
|
62
|
+
contentBlockRegistry.register(sourceComponentBlock);
|
|
61
63
|
contentBlockRegistry.register(inlineDatabaseBlock);
|
|
62
64
|
|
|
63
65
|
type ContentBlockRenderContext = BlockRenderContext & {
|
|
@@ -34,6 +34,7 @@ import { Button } from "@/components/ui/button";
|
|
|
34
34
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
|
35
35
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
36
36
|
import { useComments } from "@/hooks/use-comments";
|
|
37
|
+
import { useProcessBuilderBodyHydration } from "@/hooks/use-content-database";
|
|
37
38
|
import {
|
|
38
39
|
useDocument,
|
|
39
40
|
useDocuments,
|
|
@@ -85,14 +86,18 @@ function adoptConfirmedSaveWatermarks({
|
|
|
85
86
|
savedAt: string;
|
|
86
87
|
title: string;
|
|
87
88
|
content: string;
|
|
88
|
-
updates:
|
|
89
|
+
updates: {
|
|
90
|
+
title?: string;
|
|
91
|
+
content?: string;
|
|
92
|
+
icon?: string | null;
|
|
93
|
+
};
|
|
89
94
|
lastSavedTitleRef: MutableRefObject<FieldSaveWatermark>;
|
|
90
95
|
lastSavedContentRef: MutableRefObject<ContentSaveWatermark>;
|
|
91
96
|
}) {
|
|
92
97
|
if (updates.title !== undefined) {
|
|
93
98
|
lastSavedTitleRef.current = { title, updatedAt: savedAt };
|
|
94
99
|
} else if (
|
|
95
|
-
updates.content !== undefined &&
|
|
100
|
+
(updates.content !== undefined || updates.icon !== undefined) &&
|
|
96
101
|
saved?.title === lastSavedTitleRef.current.title
|
|
97
102
|
) {
|
|
98
103
|
lastSavedTitleRef.current = {
|
|
@@ -103,7 +108,7 @@ function adoptConfirmedSaveWatermarks({
|
|
|
103
108
|
if (updates.content !== undefined) {
|
|
104
109
|
lastSavedContentRef.current = { content, updatedAt: savedAt };
|
|
105
110
|
} else if (
|
|
106
|
-
updates.title !== undefined &&
|
|
111
|
+
(updates.title !== undefined || updates.icon !== undefined) &&
|
|
107
112
|
saved?.content === lastSavedContentRef.current.content
|
|
108
113
|
) {
|
|
109
114
|
lastSavedContentRef.current = {
|
|
@@ -326,6 +331,9 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
326
331
|
const t = useT();
|
|
327
332
|
const updateDocument = useUpdateDocument();
|
|
328
333
|
const queryClient = useQueryClient();
|
|
334
|
+
const processBuilderBodies = useProcessBuilderBodyHydration(
|
|
335
|
+
document.databaseMembership?.databaseDocumentId ?? documentId,
|
|
336
|
+
);
|
|
329
337
|
const canEdit = document.canEdit ?? true;
|
|
330
338
|
const canEditRef = useRef(canEdit);
|
|
331
339
|
canEditRef.current = canEdit;
|
|
@@ -358,6 +366,7 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
358
366
|
string | null
|
|
359
367
|
>(document.updatedAt ?? null);
|
|
360
368
|
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
369
|
+
const promotedBuilderBodyRef = useRef<string | null>(null);
|
|
361
370
|
const pendingDocumentSaveRef = useRef<PendingDocumentSave | null>(null);
|
|
362
371
|
// Separate freshness watermarks for title and content so that a content save
|
|
363
372
|
// never suppresses adopting a newer external title and vice versa.
|
|
@@ -388,6 +397,26 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
388
397
|
},
|
|
389
398
|
[t],
|
|
390
399
|
);
|
|
400
|
+
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
const membership = document.databaseMembership;
|
|
403
|
+
const hydration = membership?.bodyHydration;
|
|
404
|
+
if (
|
|
405
|
+
!membership?.sourceId ||
|
|
406
|
+
!hydration ||
|
|
407
|
+
(hydration.status !== "pending" && hydration.status !== "error")
|
|
408
|
+
) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const promotionKey = `${membership.sourceId}:${documentId}:${hydration.status}:${hydration.version ?? ""}`;
|
|
412
|
+
if (promotedBuilderBodyRef.current === promotionKey) return;
|
|
413
|
+
promotedBuilderBodyRef.current = promotionKey;
|
|
414
|
+
processBuilderBodies.mutate({
|
|
415
|
+
sourceId: membership.sourceId,
|
|
416
|
+
documentId,
|
|
417
|
+
limit: 1,
|
|
418
|
+
});
|
|
419
|
+
}, [document.databaseMembership, documentId, processBuilderBodies.mutate]);
|
|
391
420
|
const titleFocusedRef = useRef(false);
|
|
392
421
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
393
422
|
const titleInputRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -1160,7 +1189,19 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
1160
1189
|
}
|
|
1161
1190
|
onSelect={(emoji) => {
|
|
1162
1191
|
void (async () => {
|
|
1163
|
-
await persistDocumentUpdates({
|
|
1192
|
+
const saved = await persistDocumentUpdates({
|
|
1193
|
+
icon: emoji,
|
|
1194
|
+
});
|
|
1195
|
+
adoptConfirmedSaveWatermarks({
|
|
1196
|
+
saved,
|
|
1197
|
+
savedAt:
|
|
1198
|
+
saved?.updatedAt ?? new Date().toISOString(),
|
|
1199
|
+
title: localTitleRef.current,
|
|
1200
|
+
content: localContentRef.current,
|
|
1201
|
+
updates: { icon: emoji },
|
|
1202
|
+
lastSavedTitleRef,
|
|
1203
|
+
lastSavedContentRef,
|
|
1204
|
+
});
|
|
1164
1205
|
})().catch(handleBackgroundSaveError);
|
|
1165
1206
|
}}
|
|
1166
1207
|
/>
|
|
@@ -66,7 +66,10 @@ import {
|
|
|
66
66
|
type NotionPageLink,
|
|
67
67
|
} from "./extensions/NotionExtensions";
|
|
68
68
|
import { notionFidelityExtensions } from "./extensions/NotionFidelity";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
LockedSourceComponentBlocks,
|
|
71
|
+
RegistryBlockNode,
|
|
72
|
+
} from "./extensions/registryBlocks";
|
|
70
73
|
import { VideoNode } from "./extensions/VideoNode";
|
|
71
74
|
import {
|
|
72
75
|
getImageFiles,
|
|
@@ -1307,6 +1310,7 @@ export function createVisualEditorExtensions({
|
|
|
1307
1310
|
// `VisualEditor` below. Mounted after the Notion nodes and before the
|
|
1308
1311
|
// Markdown extension so the NFM <-> doc round-trip recognizes the node.
|
|
1309
1312
|
RegistryBlockNode,
|
|
1313
|
+
LockedSourceComponentBlocks,
|
|
1310
1314
|
ContentReferenceNode.configure({
|
|
1311
1315
|
currentPath: localFilePath ?? null,
|
|
1312
1316
|
referenceDepth,
|
|
@@ -151,6 +151,7 @@ import {
|
|
|
151
151
|
useExecuteBuilderSourceExecution,
|
|
152
152
|
useMoveDatabaseItem,
|
|
153
153
|
usePrepareBuilderSourceReview,
|
|
154
|
+
useProcessBuilderBodyHydration,
|
|
154
155
|
useRefreshContentDatabaseSource,
|
|
155
156
|
useSetContentDatabaseSourceWriteMode,
|
|
156
157
|
useSuggestSourceJoinKey,
|
|
@@ -479,6 +480,7 @@ function DatabaseTable({
|
|
|
479
480
|
const changeSourceRole = useChangeContentDatabaseSourceRole(document.id);
|
|
480
481
|
const refreshSource = useRefreshContentDatabaseSource(document.id);
|
|
481
482
|
const disconnectSource = useDisconnectContentDatabaseSource(document.id);
|
|
483
|
+
const processBuilderBodies = useProcessBuilderBodyHydration(document.id);
|
|
482
484
|
const prepareBuilderReview = usePrepareBuilderSourceReview(document.id);
|
|
483
485
|
const executeBuilderExecution = useExecuteBuilderSourceExecution(document.id);
|
|
484
486
|
const setSourceWriteMode = useSetContentDatabaseSourceWriteMode(document.id);
|
|
@@ -1570,6 +1572,9 @@ function DatabaseTable({
|
|
|
1570
1572
|
sourceId,
|
|
1571
1573
|
})
|
|
1572
1574
|
}
|
|
1575
|
+
onHydrateBuilderBodies={(sourceId) =>
|
|
1576
|
+
processBuilderBodies.mutate({ sourceId })
|
|
1577
|
+
}
|
|
1573
1578
|
onDisconnectSource={(sourceId) =>
|
|
1574
1579
|
disconnectSource.mutate(
|
|
1575
1580
|
{
|
|
@@ -1635,6 +1640,7 @@ function DatabaseTable({
|
|
|
1635
1640
|
attachSource.isPending ||
|
|
1636
1641
|
changeSourceRole.isPending ||
|
|
1637
1642
|
refreshSource.isPending ||
|
|
1643
|
+
processBuilderBodies.isPending ||
|
|
1638
1644
|
disconnectSource.isPending ||
|
|
1639
1645
|
prepareBuilderReview.isPending ||
|
|
1640
1646
|
executeBuilderExecution.isPending ||
|
|
@@ -3539,6 +3545,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3539
3545
|
onChangeSourceRole,
|
|
3540
3546
|
onDisconnectSecondary,
|
|
3541
3547
|
onRefreshSource,
|
|
3548
|
+
onHydrateBuilderBodies,
|
|
3542
3549
|
onDisconnectSource,
|
|
3543
3550
|
onReviewBuilderUpdate,
|
|
3544
3551
|
onSetBuilderLiveWrites,
|
|
@@ -3580,6 +3587,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3580
3587
|
) => Promise<ContentDatabaseResponse>;
|
|
3581
3588
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
3582
3589
|
onRefreshSource: (sourceId?: string) => void;
|
|
3590
|
+
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
3583
3591
|
onDisconnectSource: (sourceId?: string) => void;
|
|
3584
3592
|
onReviewBuilderUpdate: () => void;
|
|
3585
3593
|
onSetBuilderLiveWrites: (enabled: boolean) => void;
|
|
@@ -3677,6 +3685,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3677
3685
|
setSourceNavStack([]);
|
|
3678
3686
|
}}
|
|
3679
3687
|
onRefreshSource={onRefreshSource}
|
|
3688
|
+
onHydrateBuilderBodies={onHydrateBuilderBodies}
|
|
3680
3689
|
onDisconnectSource={onDisconnectSource}
|
|
3681
3690
|
onReviewBuilderUpdate={onReviewBuilderUpdate}
|
|
3682
3691
|
onSetBuilderLiveWrites={onSetBuilderLiveWrites}
|
|
@@ -3976,6 +3985,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
3976
3985
|
onChangeSourceRole,
|
|
3977
3986
|
onDisconnectSecondary,
|
|
3978
3987
|
onRefreshSource,
|
|
3988
|
+
onHydrateBuilderBodies,
|
|
3979
3989
|
onDisconnectSource,
|
|
3980
3990
|
onReviewBuilderUpdate,
|
|
3981
3991
|
onSetBuilderLiveWrites,
|
|
@@ -4004,6 +4014,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
4004
4014
|
) => Promise<ContentDatabaseResponse>;
|
|
4005
4015
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
4006
4016
|
onRefreshSource: (sourceId?: string) => void;
|
|
4017
|
+
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
4007
4018
|
onDisconnectSource: (sourceId?: string) => void;
|
|
4008
4019
|
onReviewBuilderUpdate: () => void;
|
|
4009
4020
|
onSetBuilderLiveWrites: (enabled: boolean) => void;
|
|
@@ -4442,6 +4453,15 @@ function DatabaseSettingsSourcePanel({
|
|
|
4442
4453
|
</div>
|
|
4443
4454
|
) : null}
|
|
4444
4455
|
|
|
4456
|
+
{isBuilderSource && source.bodyHydration ? (
|
|
4457
|
+
<BuilderBodyHydrationCard
|
|
4458
|
+
source={source}
|
|
4459
|
+
canEdit={canEdit}
|
|
4460
|
+
pending={sourceActionPending}
|
|
4461
|
+
onHydrate={() => onHydrateBuilderBodies(source.id)}
|
|
4462
|
+
/>
|
|
4463
|
+
) : null}
|
|
4464
|
+
|
|
4445
4465
|
{isCodeMode ? (
|
|
4446
4466
|
<>
|
|
4447
4467
|
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3 text-sm">
|
|
@@ -5173,6 +5193,78 @@ function SourceRoleCard({
|
|
|
5173
5193
|
);
|
|
5174
5194
|
}
|
|
5175
5195
|
|
|
5196
|
+
function BuilderBodyHydrationCard({
|
|
5197
|
+
source,
|
|
5198
|
+
canEdit,
|
|
5199
|
+
pending,
|
|
5200
|
+
onHydrate,
|
|
5201
|
+
}: {
|
|
5202
|
+
source: ContentDatabaseSource;
|
|
5203
|
+
canEdit: boolean;
|
|
5204
|
+
pending: boolean;
|
|
5205
|
+
onHydrate: () => void;
|
|
5206
|
+
}) {
|
|
5207
|
+
const summary = source.bodyHydration;
|
|
5208
|
+
if (!summary || summary.total === 0) return null;
|
|
5209
|
+
const activeCount = summary.pending + summary.hydrating;
|
|
5210
|
+
const needsWork = activeCount > 0 || summary.error > 0;
|
|
5211
|
+
const hydratedLabel = dbText("builderBodiesHydrated", {
|
|
5212
|
+
hydrated: summary.hydrated,
|
|
5213
|
+
total: summary.total,
|
|
5214
|
+
});
|
|
5215
|
+
const detail = needsWork
|
|
5216
|
+
? [
|
|
5217
|
+
activeCount > 0
|
|
5218
|
+
? dbText("builderBodiesQueued", { count: activeCount })
|
|
5219
|
+
: null,
|
|
5220
|
+
summary.error > 0
|
|
5221
|
+
? dbText("builderBodySyncFailed", { count: summary.error })
|
|
5222
|
+
: null,
|
|
5223
|
+
]
|
|
5224
|
+
.filter(Boolean)
|
|
5225
|
+
.join(" · ")
|
|
5226
|
+
: dbText("builderBodiesReadyLocally");
|
|
5227
|
+
|
|
5228
|
+
return (
|
|
5229
|
+
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3">
|
|
5230
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
5231
|
+
<div className="min-w-0">
|
|
5232
|
+
<div className="text-xs font-medium">{dbText("builderBodySync")}</div>
|
|
5233
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
5234
|
+
{hydratedLabel}
|
|
5235
|
+
</div>
|
|
5236
|
+
</div>
|
|
5237
|
+
{needsWork ? (
|
|
5238
|
+
<Button
|
|
5239
|
+
type="button"
|
|
5240
|
+
size="sm"
|
|
5241
|
+
variant="outline"
|
|
5242
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
5243
|
+
disabled={!canEdit || pending}
|
|
5244
|
+
onClick={onHydrate}
|
|
5245
|
+
>
|
|
5246
|
+
{pending ? (
|
|
5247
|
+
<Spinner className="mr-1 size-3.5" />
|
|
5248
|
+
) : (
|
|
5249
|
+
<IconRefresh className="mr-1 size-3.5" />
|
|
5250
|
+
)}
|
|
5251
|
+
{summary.error > 0 ? dbText("retry") : dbText("resume")}
|
|
5252
|
+
</Button>
|
|
5253
|
+
) : null}
|
|
5254
|
+
</div>
|
|
5255
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
5256
|
+
<div
|
|
5257
|
+
className="h-full rounded-full bg-foreground transition-[width]"
|
|
5258
|
+
style={{
|
|
5259
|
+
width: `${Math.round((summary.hydrated / summary.total) * 100)}%`,
|
|
5260
|
+
}}
|
|
5261
|
+
/>
|
|
5262
|
+
</div>
|
|
5263
|
+
<div className="break-words text-xs text-muted-foreground">{detail}</div>
|
|
5264
|
+
</div>
|
|
5265
|
+
);
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5176
5268
|
const DETAIL_FIELD_NAME_HINTS = [
|
|
5177
5269
|
"name",
|
|
5178
5270
|
"title",
|