@agent-native/core 0.123.1 → 0.124.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 +20 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +38 -21
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +23 -20
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -1,175 +1,19 @@
|
|
|
1
|
-
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
-
import { IconChevronDown, IconCheck } from "@tabler/icons-react";
|
|
3
1
|
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
|
|
4
|
-
import {
|
|
5
|
-
NodeViewContent,
|
|
6
|
-
NodeViewWrapper,
|
|
7
|
-
ReactNodeViewRenderer,
|
|
8
|
-
type NodeViewProps,
|
|
9
|
-
} from "@tiptap/react";
|
|
10
2
|
import { common, createLowlight } from "lowlight";
|
|
11
|
-
import { useState, useRef, useEffect, useCallback } from "react";
|
|
12
|
-
|
|
13
|
-
import { cn } from "@/lib/utils";
|
|
14
3
|
|
|
15
4
|
const lowlight = createLowlight(common);
|
|
16
5
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{ value: "json", label: "JSON" },
|
|
28
|
-
{ value: "yaml", label: "YAML" },
|
|
29
|
-
{ value: "markdown", label: "Markdown" },
|
|
30
|
-
{ value: "sql", label: "SQL" },
|
|
31
|
-
{ value: "java", label: "Java" },
|
|
32
|
-
{ value: "c", label: "C" },
|
|
33
|
-
{ value: "cpp", label: "C++" },
|
|
34
|
-
{ value: "csharp", label: "C#" },
|
|
35
|
-
{ value: "go", label: "Go" },
|
|
36
|
-
{ value: "rust", label: "Rust" },
|
|
37
|
-
{ value: "ruby", label: "Ruby" },
|
|
38
|
-
{ value: "php", label: "PHP" },
|
|
39
|
-
{ value: "swift", label: "Swift" },
|
|
40
|
-
{ value: "kotlin", label: "Kotlin" },
|
|
41
|
-
{ value: "scala", label: "Scala" },
|
|
42
|
-
{ value: "r", label: "R" },
|
|
43
|
-
{ value: "perl", label: "Perl" },
|
|
44
|
-
{ value: "lua", label: "Lua" },
|
|
45
|
-
{ value: "xml", label: "XML" },
|
|
46
|
-
{ value: "graphql", label: "GraphQL" },
|
|
47
|
-
{ value: "dockerfile", label: "Dockerfile" },
|
|
48
|
-
{ value: "ini", label: "INI" },
|
|
49
|
-
{ value: "diff", label: "Diff" },
|
|
50
|
-
] as const;
|
|
51
|
-
|
|
52
|
-
function languageLabel(
|
|
53
|
-
language: (typeof LANGUAGES)[number],
|
|
54
|
-
t: ReturnType<typeof useT>,
|
|
55
|
-
) {
|
|
56
|
-
return "labelKey" in language ? t(language.labelKey) : language.label;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function CodeBlockView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
60
|
-
const t = useT();
|
|
61
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
62
|
-
const [filter, setFilter] = useState("");
|
|
63
|
-
const pickerRef = useRef<HTMLDivElement>(null);
|
|
64
|
-
const filterRef = useRef<HTMLInputElement>(null);
|
|
65
|
-
const isEditable = editor.isEditable;
|
|
66
|
-
|
|
67
|
-
const currentLang = node.attrs.language as string | null;
|
|
68
|
-
const currentLanguage = LANGUAGES.find(
|
|
69
|
-
(l) =>
|
|
70
|
-
l.value === currentLang ||
|
|
71
|
-
(l.value === null && !currentLang) ||
|
|
72
|
-
((l.value as string) === "plaintext" && !currentLang),
|
|
73
|
-
);
|
|
74
|
-
const displayLabel =
|
|
75
|
-
(currentLanguage ? languageLabel(currentLanguage, t) : currentLang) ??
|
|
76
|
-
t("editor.plainText");
|
|
77
|
-
|
|
78
|
-
const filteredLanguages = LANGUAGES.filter((l) =>
|
|
79
|
-
languageLabel(l, t).toLowerCase().includes(filter.toLowerCase()),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const closePicker = useCallback(() => {
|
|
83
|
-
setShowPicker(false);
|
|
84
|
-
setFilter("");
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (!showPicker) return;
|
|
89
|
-
filterRef.current?.focus();
|
|
90
|
-
const handleClick = (e: MouseEvent) => {
|
|
91
|
-
if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) {
|
|
92
|
-
closePicker();
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
document.addEventListener("mousedown", handleClick);
|
|
96
|
-
return () => document.removeEventListener("mousedown", handleClick);
|
|
97
|
-
}, [showPicker, closePicker]);
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<NodeViewWrapper className="notion-code-block-wrapper">
|
|
101
|
-
<div className="notion-code-block-header" contentEditable={false}>
|
|
102
|
-
<div className="relative" ref={pickerRef}>
|
|
103
|
-
{isEditable ? (
|
|
104
|
-
<button
|
|
105
|
-
type="button"
|
|
106
|
-
className="notion-code-lang-btn"
|
|
107
|
-
onClick={() => setShowPicker(!showPicker)}
|
|
108
|
-
>
|
|
109
|
-
{displayLabel}
|
|
110
|
-
<IconChevronDown size={12} />
|
|
111
|
-
</button>
|
|
112
|
-
) : (
|
|
113
|
-
<span className="notion-code-lang-btn notion-code-lang-btn--readonly">
|
|
114
|
-
{displayLabel}
|
|
115
|
-
</span>
|
|
116
|
-
)}
|
|
117
|
-
{showPicker && isEditable && (
|
|
118
|
-
<div className="notion-code-lang-picker">
|
|
119
|
-
<input
|
|
120
|
-
ref={filterRef}
|
|
121
|
-
type="text"
|
|
122
|
-
value={filter}
|
|
123
|
-
onChange={(e) => setFilter(e.target.value)}
|
|
124
|
-
onKeyDown={(e) => {
|
|
125
|
-
if (e.key === "Escape") closePicker();
|
|
126
|
-
if (e.key === "Enter" && filteredLanguages.length > 0) {
|
|
127
|
-
updateAttributes({
|
|
128
|
-
language: filteredLanguages[0].value || "",
|
|
129
|
-
});
|
|
130
|
-
closePicker();
|
|
131
|
-
}
|
|
132
|
-
}}
|
|
133
|
-
placeholder={t("editor.searchLanguages")}
|
|
134
|
-
className="notion-code-lang-search"
|
|
135
|
-
/>
|
|
136
|
-
<div className="notion-code-lang-list">
|
|
137
|
-
{filteredLanguages.map((lang) => (
|
|
138
|
-
<button
|
|
139
|
-
key={lang.value ?? "plain"}
|
|
140
|
-
type="button"
|
|
141
|
-
className={cn(
|
|
142
|
-
"notion-code-lang-option",
|
|
143
|
-
(currentLang === lang.value ||
|
|
144
|
-
(!currentLang && !lang.value)) &&
|
|
145
|
-
"active",
|
|
146
|
-
)}
|
|
147
|
-
onClick={() => {
|
|
148
|
-
updateAttributes({ language: lang.value || "" });
|
|
149
|
-
closePicker();
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
{languageLabel(lang, t)}
|
|
153
|
-
{(currentLang === lang.value ||
|
|
154
|
-
(!currentLang && !lang.value)) && <IconCheck size={14} />}
|
|
155
|
-
</button>
|
|
156
|
-
))}
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
<pre>
|
|
163
|
-
<NodeViewContent as={"code" as any} />
|
|
164
|
-
</pre>
|
|
165
|
-
</NodeViewWrapper>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Content's fenced code block.
|
|
8
|
+
*
|
|
9
|
+
* Keep the node on Tiptap's native DOM renderer. Mounting the former React
|
|
10
|
+
* node view could synchronously lock a collaborative editor as soon as a code
|
|
11
|
+
* block entered the document. The native renderer preserves the same
|
|
12
|
+
* `codeBlock` schema, language attribute, lowlight decorations, markdown
|
|
13
|
+
* round-trip, and keyboard behavior without a second React render tree inside
|
|
14
|
+
* ProseMirror.
|
|
15
|
+
*/
|
|
169
16
|
export const CodeBlock = CodeBlockLowlight.extend({
|
|
170
|
-
addNodeView() {
|
|
171
|
-
return ReactNodeViewRenderer(CodeBlockView);
|
|
172
|
-
},
|
|
173
17
|
addKeyboardShortcuts() {
|
|
174
18
|
return {
|
|
175
19
|
...this.parent?.(),
|
|
@@ -151,9 +151,7 @@ export function VideoBlock({
|
|
|
151
151
|
}: NodeViewProps) {
|
|
152
152
|
const t = useT();
|
|
153
153
|
const [isHovered, setIsHovered] = useState(false);
|
|
154
|
-
const [sourcePanelOpen, setSourcePanelOpen] = useState(false);
|
|
155
154
|
const [sourcePanelDismissed, setSourcePanelDismissed] = useState(false);
|
|
156
|
-
const [sourceTab, setSourceTab] = useState<VideoSourceTab>("upload");
|
|
157
155
|
const [videoUrl, setVideoUrl] = useState("");
|
|
158
156
|
const [dragWidth, setDragWidth] = useState<number | null>(null);
|
|
159
157
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
@@ -165,6 +163,9 @@ export function VideoBlock({
|
|
|
165
163
|
const resizeStateRef = useRef<VideoResizeState | null>(null);
|
|
166
164
|
const isEditable = editor.isEditable;
|
|
167
165
|
const src = node.attrs.src as string;
|
|
166
|
+
const sourcePanelOpen = node.attrs.sourcePanelOpen === true;
|
|
167
|
+
const sourceTab: VideoSourceTab =
|
|
168
|
+
node.attrs.sourceTab === "link" ? "link" : "upload";
|
|
168
169
|
const poster = (node.attrs.poster as string) || "";
|
|
169
170
|
const isUploading = Boolean(node.attrs.uploadId);
|
|
170
171
|
const width = normalizedVideoWidth(node.attrs.width);
|
|
@@ -172,6 +173,14 @@ export function VideoBlock({
|
|
|
172
173
|
const controlsVisible = isEditable && (isHovered || selected);
|
|
173
174
|
const options = extension.options as ContentVideoOptions;
|
|
174
175
|
|
|
176
|
+
function setSourcePanelOpen(open: boolean) {
|
|
177
|
+
updateAttributes({ sourcePanelOpen: open });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setSourceTab(tab: VideoSourceTab) {
|
|
181
|
+
updateAttributes({ sourcePanelOpen: true, sourceTab: tab });
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
useEffect(() => {
|
|
176
185
|
if (!sourcePanelOpen && !selected) return;
|
|
177
186
|
|
|
@@ -210,10 +219,9 @@ export function VideoBlock({
|
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
function openReplacePanel() {
|
|
213
|
-
setSourceTab("upload");
|
|
214
222
|
setVideoUrl("");
|
|
215
223
|
setSourcePanelDismissed(false);
|
|
216
|
-
|
|
224
|
+
updateAttributes({ sourcePanelOpen: true, sourceTab: "upload" });
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
function handleLightboxOpenChange(open: boolean) {
|
|
@@ -376,8 +384,11 @@ export function VideoBlock({
|
|
|
376
384
|
const toastId = toast.loading(t("editor.media.uploadingVideo"));
|
|
377
385
|
try {
|
|
378
386
|
const nextSrc = await uploadVideoFile(file);
|
|
379
|
-
updateAttributes({
|
|
380
|
-
|
|
387
|
+
updateAttributes({
|
|
388
|
+
src: nextSrc,
|
|
389
|
+
sourcePanelOpen: false,
|
|
390
|
+
sourceTab: "upload",
|
|
391
|
+
});
|
|
381
392
|
toast.success(t("editor.media.videoAdded"), { id: toastId });
|
|
382
393
|
} catch (error) {
|
|
383
394
|
toast.error(videoUploadErrorMessage(error), { id: toastId });
|
|
@@ -399,9 +410,12 @@ export function VideoBlock({
|
|
|
399
410
|
return;
|
|
400
411
|
}
|
|
401
412
|
|
|
402
|
-
updateAttributes({
|
|
413
|
+
updateAttributes({
|
|
414
|
+
src: nextSrc,
|
|
415
|
+
sourcePanelOpen: false,
|
|
416
|
+
sourceTab: "upload",
|
|
417
|
+
});
|
|
403
418
|
setVideoUrl("");
|
|
404
|
-
setSourcePanelOpen(false);
|
|
405
419
|
}
|
|
406
420
|
|
|
407
421
|
function renderSourcePanel(replace = false) {
|
|
@@ -417,6 +431,11 @@ export function VideoBlock({
|
|
|
417
431
|
role="tab"
|
|
418
432
|
aria-selected={sourceTab === "upload"}
|
|
419
433
|
className="media-source-panel__tab"
|
|
434
|
+
onPointerDown={(event) => {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
event.stopPropagation();
|
|
437
|
+
setSourceTab("upload");
|
|
438
|
+
}}
|
|
420
439
|
onClick={() => setSourceTab("upload")}
|
|
421
440
|
>
|
|
422
441
|
{t("editor.media.upload")}
|
|
@@ -426,6 +445,11 @@ export function VideoBlock({
|
|
|
426
445
|
role="tab"
|
|
427
446
|
aria-selected={sourceTab === "link"}
|
|
428
447
|
className="media-source-panel__tab"
|
|
448
|
+
onPointerDown={(event) => {
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
event.stopPropagation();
|
|
451
|
+
setSourceTab("link");
|
|
452
|
+
}}
|
|
429
453
|
onClick={() => setSourceTab("link")}
|
|
430
454
|
>
|
|
431
455
|
{t("editor.media.link")}
|
|
@@ -114,6 +114,20 @@ export const VideoNode = Node.create<ContentVideoOptions>({
|
|
|
114
114
|
parseHTML: () => null,
|
|
115
115
|
renderHTML: () => ({}),
|
|
116
116
|
},
|
|
117
|
+
// UI-only state. Keeping the source panel on the ProseMirror node makes
|
|
118
|
+
// it survive React node-view remounts while the empty video draft is
|
|
119
|
+
// being autosaved/reconciled. It is deliberately omitted from HTML and
|
|
120
|
+
// Markdown serialization.
|
|
121
|
+
sourcePanelOpen: {
|
|
122
|
+
default: false,
|
|
123
|
+
parseHTML: () => false,
|
|
124
|
+
renderHTML: () => ({}),
|
|
125
|
+
},
|
|
126
|
+
sourceTab: {
|
|
127
|
+
default: "upload",
|
|
128
|
+
parseHTML: () => "upload",
|
|
129
|
+
renderHTML: () => ({}),
|
|
130
|
+
},
|
|
117
131
|
};
|
|
118
132
|
},
|
|
119
133
|
|
|
@@ -104,10 +104,13 @@ export function buildRegistrySlashItems(
|
|
|
104
104
|
// not a content-authoring block.
|
|
105
105
|
// - `inline-database` is registered in Phase 2 for render/round-trip
|
|
106
106
|
// compatibility; Phase 3 owns the `/database` insertion flow.
|
|
107
|
+
// - `callout` duplicates Content's native Notion-style Callout command.
|
|
108
|
+
// Keep one unambiguous authoring choice; the registry remains available
|
|
109
|
+
// for rendering and round-tripping saved `<Callout>` blocks.
|
|
107
110
|
// - `source-component` is emitted by provider hydration to preserve
|
|
108
111
|
// source-native blocks; users should not author raw preservation
|
|
109
112
|
// markers by hand.
|
|
110
|
-
// The genuinely document-friendly rich blocks (
|
|
113
|
+
// The genuinely document-friendly rich blocks (decision, diagram,
|
|
111
114
|
// wireframe, and the dev-doc/structured set) ARE offered.
|
|
112
115
|
includeSpec: (spec) =>
|
|
113
116
|
![
|
|
@@ -115,6 +118,7 @@ export function buildRegistrySlashItems(
|
|
|
115
118
|
"question-form",
|
|
116
119
|
"visual-questions",
|
|
117
120
|
"inline-database",
|
|
121
|
+
"callout",
|
|
118
122
|
"source-component",
|
|
119
123
|
].includes(spec.type),
|
|
120
124
|
toItem: (spec, insert) => ({
|
|
@@ -170,6 +170,57 @@ export function writeContentDatabaseResponseToCache(
|
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
export function applyOptimisticBuilderWriteMode(
|
|
174
|
+
current: ContentDatabaseResponse | undefined,
|
|
175
|
+
request: SetContentDatabaseSourceWriteModeRequest,
|
|
176
|
+
) {
|
|
177
|
+
if (!current || !request.writeMode) return current;
|
|
178
|
+
const sourceId = request.sourceId ?? current.source?.id;
|
|
179
|
+
if (!sourceId) return current;
|
|
180
|
+
const writeMode = request.writeMode;
|
|
181
|
+
const liveWritesEnabled = writeMode !== "read_only";
|
|
182
|
+
const allowPublicationTransitions =
|
|
183
|
+
writeMode === "publish_updates" &&
|
|
184
|
+
request.allowPublicationTransitions === true;
|
|
185
|
+
const allowedWriteModes =
|
|
186
|
+
writeMode === "publish_updates"
|
|
187
|
+
? (["autosave", "publish"] as const)
|
|
188
|
+
: writeMode === "stage_only"
|
|
189
|
+
? (["autosave"] as const)
|
|
190
|
+
: ([] as const);
|
|
191
|
+
const patchSource = (
|
|
192
|
+
source: ContentDatabaseResponse["source"],
|
|
193
|
+
): ContentDatabaseResponse["source"] =>
|
|
194
|
+
source?.id === sourceId
|
|
195
|
+
? {
|
|
196
|
+
...source,
|
|
197
|
+
capabilities: {
|
|
198
|
+
...source.capabilities,
|
|
199
|
+
liveWritesEnabled,
|
|
200
|
+
},
|
|
201
|
+
metadata: {
|
|
202
|
+
...source.metadata,
|
|
203
|
+
writeMode,
|
|
204
|
+
allowPublicationTransitions,
|
|
205
|
+
allowedWriteModes: [...allowedWriteModes],
|
|
206
|
+
allowDraftWrites: false,
|
|
207
|
+
allowPublishWrites: writeMode === "publish_updates",
|
|
208
|
+
pushMode:
|
|
209
|
+
writeMode === "publish_updates"
|
|
210
|
+
? "publish"
|
|
211
|
+
: writeMode === "stage_only"
|
|
212
|
+
? "autosave"
|
|
213
|
+
: "none",
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
: source;
|
|
217
|
+
return {
|
|
218
|
+
...current,
|
|
219
|
+
source: patchSource(current.source),
|
|
220
|
+
sources: current.sources?.map((source) => patchSource(source)!),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
173
224
|
export function applyDocumentPropertyValueToDatabaseResponse(
|
|
174
225
|
current: ContentDatabaseResponse | undefined,
|
|
175
226
|
patch: {
|
|
@@ -679,7 +730,12 @@ export function useAddDatabaseItem(documentId: string) {
|
|
|
679
730
|
return useActionMutation<ContentDatabaseResponse, AddDatabaseItemRequest>(
|
|
680
731
|
"add-database-item",
|
|
681
732
|
{
|
|
682
|
-
onSuccess: () => {
|
|
733
|
+
onSuccess: (data) => {
|
|
734
|
+
// The action returns the committed row and full database snapshot.
|
|
735
|
+
// Seed every active pagination key before invalidating so navigating
|
|
736
|
+
// away from the creation side-peek cannot briefly lose an appended row
|
|
737
|
+
// behind an older 100/200-row response.
|
|
738
|
+
writeContentDatabaseResponseToCache(queryClient, documentId, data);
|
|
683
739
|
queryClient.invalidateQueries({
|
|
684
740
|
queryKey: contentDatabaseQueryKey(documentId),
|
|
685
741
|
});
|
|
@@ -1218,7 +1274,8 @@ export function useSetContentDatabaseSourceWriteMode(documentId: string) {
|
|
|
1218
1274
|
ContentDatabaseResponse,
|
|
1219
1275
|
SetContentDatabaseSourceWriteModeRequest
|
|
1220
1276
|
>("set-content-database-source-write-mode", {
|
|
1221
|
-
onSuccess: () => {
|
|
1277
|
+
onSuccess: (data) => {
|
|
1278
|
+
writeContentDatabaseResponseToCache(queryClient, documentId, data);
|
|
1222
1279
|
queryClient.invalidateQueries({
|
|
1223
1280
|
queryKey: contentDatabaseQueryKey(documentId),
|
|
1224
1281
|
});
|
|
@@ -18,9 +18,6 @@ import type { QueryClient } from "@tanstack/react-query";
|
|
|
18
18
|
import { useQueryClient } from "@tanstack/react-query";
|
|
19
19
|
import { toast } from "sonner";
|
|
20
20
|
|
|
21
|
-
import { databaseItemBodyHydrationIsPending } from "@/components/editor/body-hydration";
|
|
22
|
-
import { isEffectivelyEmptyDocumentContent } from "@/components/editor/body-hydration";
|
|
23
|
-
|
|
24
21
|
import type { DocumentUpdateConflictResponse } from "../../actions/update-document";
|
|
25
22
|
import {
|
|
26
23
|
removeOptimisticItemFromContentDatabase,
|
|
@@ -243,28 +240,12 @@ export function seedDatabaseItemDocumentCaches(
|
|
|
243
240
|
queryClient: Pick<QueryClient, "getQueryData" | "setQueryData">,
|
|
244
241
|
item: ContentDatabaseItem,
|
|
245
242
|
) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// refetch for the whole staleTime window. Source-backed rows are never seeded:
|
|
253
|
-
// list snapshots are not authoritative enough to unlock the body editor, even
|
|
254
|
-
// when they happen to contain non-empty content. The dedicated get-document
|
|
255
|
-
// response owns that decision and prevents an edit from racing hydration.
|
|
256
|
-
if (
|
|
257
|
-
!databaseItemBodyHydrationIsPending(item) &&
|
|
258
|
-
!sourceBackedEmptyBody &&
|
|
259
|
-
!item.bodyHydration &&
|
|
260
|
-
!item.document.databaseMembership?.sourceId &&
|
|
261
|
-
queryClient.getQueryData(documentQueryKey(item.document.id)) === undefined
|
|
262
|
-
) {
|
|
263
|
-
queryClient.setQueryData<Document>(documentQueryKey(item.document.id), {
|
|
264
|
-
...item.document,
|
|
265
|
-
properties: item.properties,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
243
|
+
// Database table responses are list snapshots, not authoritative editable
|
|
244
|
+
// bodies. Even a cold cache can race a just-saved collaborative edit: seeding
|
|
245
|
+
// it marks the row snapshot fresh and can mount ProseMirror before the
|
|
246
|
+
// dedicated get-document request returns. Keep document bodies exclusively
|
|
247
|
+
// owned by get-document; the table may still warm the separately scoped
|
|
248
|
+
// property cache below.
|
|
268
249
|
if (
|
|
269
250
|
queryClient.getQueryData(documentPropertiesQueryKey(item.document.id)) ===
|
|
270
251
|
undefined
|
|
@@ -289,12 +270,23 @@ export function useDocuments() {
|
|
|
289
270
|
});
|
|
290
271
|
}
|
|
291
272
|
|
|
273
|
+
export const DOCUMENT_QUERY_FRESHNESS_OPTIONS = {
|
|
274
|
+
// Database/list snapshots may seed this cache before the page opens. Their
|
|
275
|
+
// body can lag a just-saved collaborative edit, so never treat that seed as
|
|
276
|
+
// authoritative for mounting the editor. The dedicated get-document action
|
|
277
|
+
// must win once per page mount; subsequent background refetches can keep the
|
|
278
|
+
// already-mounted editor current without remounting it.
|
|
279
|
+
staleTime: 0,
|
|
280
|
+
refetchOnMount: "always" as const,
|
|
281
|
+
retry: false,
|
|
282
|
+
};
|
|
283
|
+
|
|
292
284
|
export function useDocument(id: string | null) {
|
|
293
285
|
return useActionQuery<Document>("get-document", id ? { id } : undefined, {
|
|
294
286
|
enabled: !!id,
|
|
295
287
|
// Doc-not-found / no-access errors are deterministic — retrying just keeps
|
|
296
288
|
// the spinner up for ~7s before the UI can render "Not found".
|
|
297
|
-
|
|
289
|
+
...DOCUMENT_QUERY_FRESHNESS_OPTIONS,
|
|
298
290
|
});
|
|
299
291
|
}
|
|
300
292
|
|
|
@@ -566,12 +566,7 @@ export default function Root() {
|
|
|
566
566
|
const [cmdkOpen, setCmdkOpen] = useState(false);
|
|
567
567
|
const location = useLocation();
|
|
568
568
|
const loaderData = useLoaderData<typeof loader>();
|
|
569
|
-
useCommandMenuShortcut(
|
|
570
|
-
useCallback(() => setCmdkOpen(true), []),
|
|
571
|
-
{
|
|
572
|
-
allowContentEditable: true,
|
|
573
|
-
},
|
|
574
|
-
);
|
|
569
|
+
useCommandMenuShortcut(useCallback(() => setCmdkOpen(true), []));
|
|
575
570
|
|
|
576
571
|
// Public document paths (/p/*) SSR real content without the ClientOnly gate
|
|
577
572
|
// so crawlers and unauthenticated visitors receive full markup on first visit.
|