@agent-native/core 0.70.0 → 0.70.1
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/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +61 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/cli/skills.ts +2 -2
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +86 -2
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/core-routes-plugin.ts +78 -0
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +97 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +36 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +2 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/styles.css +70 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +73 -0
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +34 -5
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/skills.js +2 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +86 -2
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +63 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- package/corpus/templates/analytics/app/routes/about.tsx +0 -9
|
@@ -138,6 +138,13 @@ import {
|
|
|
138
138
|
} from "./DocumentProperties";
|
|
139
139
|
import { EmojiPicker } from "./EmojiPicker";
|
|
140
140
|
import { VisualEditor } from "./VisualEditor";
|
|
141
|
+
import { DocumentBlockFields } from "./DocumentBlockFields";
|
|
142
|
+
import { createPreviewDocumentSaveController } from "./previewDocumentSaveController";
|
|
143
|
+
import {
|
|
144
|
+
acquirePreviewDocumentSaveController,
|
|
145
|
+
peekPreviewDocumentSaveController,
|
|
146
|
+
releasePreviewDocumentSaveController,
|
|
147
|
+
} from "./previewDocumentSaveRegistry";
|
|
141
148
|
import { BuilderSourceReviewDialog } from "./database-sources/BuilderSourceReviewDialog";
|
|
142
149
|
import {
|
|
143
150
|
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
@@ -168,9 +175,11 @@ import {
|
|
|
168
175
|
} from "@shared/api";
|
|
169
176
|
import {
|
|
170
177
|
type DocumentPropertyOptionColor,
|
|
178
|
+
countWords,
|
|
171
179
|
documentPropertyDateKey,
|
|
172
180
|
documentPropertyDatePart,
|
|
173
181
|
evaluateNormalizationFormula,
|
|
182
|
+
formatWordCount,
|
|
174
183
|
formulaValueText,
|
|
175
184
|
isComputedPropertyType,
|
|
176
185
|
isEmptyPropertyValue,
|
|
@@ -1969,6 +1978,26 @@ function DatabaseItemPreviewSheet({
|
|
|
1969
1978
|
);
|
|
1970
1979
|
}
|
|
1971
1980
|
|
|
1981
|
+
function previewPayloadsEqual(
|
|
1982
|
+
a: { title: string; content: string },
|
|
1983
|
+
b: { title: string; content: string },
|
|
1984
|
+
) {
|
|
1985
|
+
return a.title === b.title && a.content === b.content;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function retainedPreviewPayload(
|
|
1989
|
+
documentId: string,
|
|
1990
|
+
serverPayload: { title: string; content: string },
|
|
1991
|
+
) {
|
|
1992
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
1993
|
+
if (!controller) return null;
|
|
1994
|
+
const dirty = !previewPayloadsEqual(controller.pending, controller.lastSaved);
|
|
1995
|
+
const savedAheadOfServer =
|
|
1996
|
+
controller.hasSavedLocally &&
|
|
1997
|
+
!previewPayloadsEqual(controller.lastSaved, serverPayload);
|
|
1998
|
+
return dirty || savedAheadOfServer ? controller.pending : null;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1972
2001
|
function DatabaseItemPreview({
|
|
1973
2002
|
item,
|
|
1974
2003
|
previousItem,
|
|
@@ -2000,47 +2029,161 @@ function DatabaseItemPreview({
|
|
|
2000
2029
|
const previewTitle = databaseItemPreviewTitle(item);
|
|
2001
2030
|
const canEdit = document?.canEdit ?? item.document.canEdit ?? true;
|
|
2002
2031
|
const canManage = document?.canManage ?? item.document.canManage ?? false;
|
|
2003
|
-
|
|
2004
|
-
|
|
2032
|
+
// Seed the displayed title/content from a RETAINED dirty controller's pending
|
|
2033
|
+
// edit if one exists for this doc (reopen-before-evict), so an unsaved peek
|
|
2034
|
+
// edit is restored on remount instead of showing stale server content; else
|
|
2035
|
+
// from the server/item value.
|
|
2036
|
+
const [localTitle, setLocalTitle] = useState(() => {
|
|
2037
|
+
const retained = retainedPreviewPayload(item.document.id, {
|
|
2038
|
+
title: item.document.title,
|
|
2039
|
+
content: item.document.content,
|
|
2040
|
+
});
|
|
2041
|
+
return retained?.title ?? item.document.title;
|
|
2042
|
+
});
|
|
2043
|
+
const [localContent, setLocalContent] = useState(() => {
|
|
2044
|
+
const retained = retainedPreviewPayload(item.document.id, {
|
|
2045
|
+
title: item.document.title,
|
|
2046
|
+
content: item.document.content,
|
|
2047
|
+
});
|
|
2048
|
+
return retained?.content ?? item.document.content;
|
|
2049
|
+
});
|
|
2005
2050
|
const [localIcon, setLocalIcon] = useState(item.document.icon);
|
|
2006
2051
|
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
|
|
2007
2052
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
2008
|
-
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
2009
2053
|
const titleInputRef = useRef<HTMLTextAreaElement>(null);
|
|
2010
|
-
const lastSavedRef = useRef({
|
|
2011
|
-
title: item.document.title,
|
|
2012
|
-
content: item.document.content,
|
|
2013
|
-
});
|
|
2014
2054
|
|
|
2055
|
+
// The peek's primary title+body save runs through a flush-on-release controller
|
|
2056
|
+
// so a pending debounced edit is PERSISTED — not dropped — when the row
|
|
2057
|
+
// switches, the editor unmounts, or the sheet closes / Open-page navigates.
|
|
2058
|
+
//
|
|
2059
|
+
// ONE CONTROLLER PER DOCUMENT ID (mirrors the additional Blocks fields): the
|
|
2060
|
+
// peek is a SINGLE component instance whose `item` prop changes on row-switch.
|
|
2061
|
+
// Rather than rebasing one controller's target id across rows (which produced a
|
|
2062
|
+
// class of timing races), we ACQUIRE a per-doc controller for the current row
|
|
2063
|
+
// and RELEASE it on switch. A controller's doc id is fixed for its life, so a
|
|
2064
|
+
// flush always lands on the correct document and a stale completion can only
|
|
2065
|
+
// ever touch its own row's state. See previewDocumentSaveRegistry.
|
|
2066
|
+
const updateDocumentRef = useRef(updateDocument);
|
|
2067
|
+
updateDocumentRef.current = updateDocument;
|
|
2068
|
+
const queryClientRef = useRef(queryClient);
|
|
2069
|
+
queryClientRef.current = queryClient;
|
|
2070
|
+
// Doc ids that have been deleted in this peek's lifetime. A pending save must
|
|
2071
|
+
// never resurrect a deleted document, so dispatch is suppressed for these.
|
|
2072
|
+
const deletedIdsRef = useRef<Set<string>>(new Set());
|
|
2073
|
+
|
|
2074
|
+
const documentId = item.document.id;
|
|
2075
|
+
|
|
2076
|
+
// Build the factory for THIS row's controller. It closes over the stable
|
|
2077
|
+
// component-scoped refs (updateDocument, queryClient, deletedIds), so the
|
|
2078
|
+
// freshest mutation impl is always used while the controller's save TARGET
|
|
2079
|
+
// (`documentId`) is fixed by the registry key.
|
|
2080
|
+
const makeController = () =>
|
|
2081
|
+
createPreviewDocumentSaveController({
|
|
2082
|
+
documentId,
|
|
2083
|
+
initial: {
|
|
2084
|
+
title: item.document.title,
|
|
2085
|
+
content: item.document.content,
|
|
2086
|
+
},
|
|
2087
|
+
save: (id, payload) =>
|
|
2088
|
+
new Promise((resolve, reject) => {
|
|
2089
|
+
// A just-deleted doc must not be re-dispatched (resurrection guard).
|
|
2090
|
+
if (deletedIdsRef.current.has(id)) {
|
|
2091
|
+
resolve(undefined);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
updateDocumentRef.current.mutate(
|
|
2095
|
+
{ id, title: payload.title, content: payload.content },
|
|
2096
|
+
{ onSuccess: () => resolve(undefined), onError: reject },
|
|
2097
|
+
);
|
|
2098
|
+
}),
|
|
2099
|
+
onSaved: () => {
|
|
2100
|
+
void queryClientRef.current.invalidateQueries({
|
|
2101
|
+
queryKey: ["action", "get-content-database"],
|
|
2102
|
+
});
|
|
2103
|
+
void queryClientRef.current.invalidateQueries({
|
|
2104
|
+
queryKey: ["action", "list-documents"],
|
|
2105
|
+
});
|
|
2106
|
+
},
|
|
2107
|
+
onError: (err) => {
|
|
2108
|
+
toast.error("Failed to save page preview", {
|
|
2109
|
+
description:
|
|
2110
|
+
err instanceof Error ? err.message : "Something went wrong",
|
|
2111
|
+
});
|
|
2112
|
+
},
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
// Acquire the controller for the current row, and release it on row-switch /
|
|
2116
|
+
// unmount. Release flush-then-evicts: the OLD row's latest dirty payload is
|
|
2117
|
+
// dispatched SYNCHRONOUSLY (bound to the OLD doc id) before the new row's
|
|
2118
|
+
// controller takes over, so a pending edit is persisted, not dropped, and never
|
|
2119
|
+
// retargeted. A quick reopen before the flush settles reuses the live instance.
|
|
2120
|
+
// The current controller is held in a ref so the change handlers reach it
|
|
2121
|
+
// synchronously.
|
|
2122
|
+
const saveControllerRef = useRef<ReturnType<typeof makeController> | null>(
|
|
2123
|
+
null,
|
|
2124
|
+
);
|
|
2125
|
+
useEffect(() => {
|
|
2126
|
+
saveControllerRef.current = acquirePreviewDocumentSaveController(
|
|
2127
|
+
documentId,
|
|
2128
|
+
makeController,
|
|
2129
|
+
);
|
|
2130
|
+
return () => {
|
|
2131
|
+
saveControllerRef.current = null;
|
|
2132
|
+
releasePreviewDocumentSaveController(documentId);
|
|
2133
|
+
};
|
|
2134
|
+
// makeController is rebuilt every render but intentionally not a dep: the
|
|
2135
|
+
// registry only invokes it on the FIRST acquire of a doc id, and the doc id
|
|
2136
|
+
// (the registry key) is the only thing that should drive re-acquire.
|
|
2137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2138
|
+
}, [documentId]);
|
|
2139
|
+
|
|
2140
|
+
// Sync displayed state to the current row, and adopt fresh server content
|
|
2141
|
+
// (e.g. an agent edit) as the controller's new confirmed baseline. mark()
|
|
2142
|
+
// touches only THIS row's controller — never another row's — because the
|
|
2143
|
+
// controller's doc id is fixed. The acquire effect above runs first, so the
|
|
2144
|
+
// controller for `documentId` is registered before this fires.
|
|
2015
2145
|
useEffect(() => {
|
|
2016
2146
|
const nextTitle = document?.title ?? item.document.title;
|
|
2017
2147
|
const nextContent = document?.content ?? item.document.content;
|
|
2018
2148
|
const nextIcon = document?.icon ?? item.document.icon;
|
|
2019
|
-
|
|
2020
|
-
|
|
2149
|
+
// Icon isn't tracked by the title/content save controller, so it can always
|
|
2150
|
+
// follow the server.
|
|
2021
2151
|
setLocalIcon(nextIcon);
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2152
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
2153
|
+
const serverPayload = { title: nextTitle, content: nextContent };
|
|
2154
|
+
const dirty =
|
|
2155
|
+
!!controller &&
|
|
2156
|
+
!previewPayloadsEqual(controller.pending, controller.lastSaved);
|
|
2157
|
+
const savedAheadOfServer =
|
|
2158
|
+
!!controller &&
|
|
2159
|
+
controller.hasSavedLocally &&
|
|
2160
|
+
!previewPayloadsEqual(controller.lastSaved, serverPayload);
|
|
2161
|
+
// Only adopt the server's title/content — into BOTH the displayed editor
|
|
2162
|
+
// state and the controller baseline — when the user hasn't typed something
|
|
2163
|
+
// newer on this row. If a dirty in-progress edit exists, preserve it: don't
|
|
2164
|
+
// clobber the visible text (the controller already holds the unsaved edit,
|
|
2165
|
+
// so nothing is lost, but the editor must keep showing what the user typed).
|
|
2166
|
+
if (!dirty && !savedAheadOfServer) {
|
|
2167
|
+
setLocalTitle(nextTitle);
|
|
2168
|
+
setLocalContent(nextContent);
|
|
2169
|
+
controller?.mark(serverPayload);
|
|
2170
|
+
} else if (controller) {
|
|
2171
|
+
// A dirty in-progress edit or a clean local save that the query has not
|
|
2172
|
+
// echoed yet is newer than stale server props.
|
|
2173
|
+
setLocalTitle(controller.pending.title);
|
|
2174
|
+
setLocalContent(controller.pending.content);
|
|
2026
2175
|
}
|
|
2027
2176
|
}, [
|
|
2177
|
+
documentId,
|
|
2028
2178
|
document?.id,
|
|
2029
2179
|
document?.title,
|
|
2030
2180
|
document?.content,
|
|
2031
2181
|
document?.icon,
|
|
2032
|
-
item.document.id,
|
|
2033
2182
|
item.document.title,
|
|
2034
2183
|
item.document.content,
|
|
2035
2184
|
item.document.icon,
|
|
2036
2185
|
]);
|
|
2037
2186
|
|
|
2038
|
-
useEffect(() => {
|
|
2039
|
-
return () => {
|
|
2040
|
-
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
2041
|
-
};
|
|
2042
|
-
}, []);
|
|
2043
|
-
|
|
2044
2187
|
useEffect(() => {
|
|
2045
2188
|
if (!focusTitle || !canEdit || isLoading || !document) return;
|
|
2046
2189
|
|
|
@@ -2053,48 +2196,16 @@ function DatabaseItemPreview({
|
|
|
2053
2196
|
return () => cancelAnimationFrame(frame);
|
|
2054
2197
|
}, [canEdit, document, focusTitle, isLoading, onTitleFocused]);
|
|
2055
2198
|
|
|
2056
|
-
function savePreviewDocument(next: { title: string; content: string }) {
|
|
2057
|
-
if (!canEdit || !document) return;
|
|
2058
|
-
if (
|
|
2059
|
-
next.title === lastSavedRef.current.title &&
|
|
2060
|
-
next.content === lastSavedRef.current.content
|
|
2061
|
-
) {
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
2066
|
-
saveTimerRef.current = setTimeout(() => {
|
|
2067
|
-
updateDocument.mutate(
|
|
2068
|
-
{ id: document.id, title: next.title, content: next.content },
|
|
2069
|
-
{
|
|
2070
|
-
onSuccess: () => {
|
|
2071
|
-
lastSavedRef.current = next;
|
|
2072
|
-
void queryClient.invalidateQueries({
|
|
2073
|
-
queryKey: ["action", "get-content-database"],
|
|
2074
|
-
});
|
|
2075
|
-
void queryClient.invalidateQueries({
|
|
2076
|
-
queryKey: ["action", "list-documents"],
|
|
2077
|
-
});
|
|
2078
|
-
},
|
|
2079
|
-
onError: (err) => {
|
|
2080
|
-
toast.error("Failed to save page preview", {
|
|
2081
|
-
description:
|
|
2082
|
-
err instanceof Error ? err.message : "Something went wrong",
|
|
2083
|
-
});
|
|
2084
|
-
},
|
|
2085
|
-
},
|
|
2086
|
-
);
|
|
2087
|
-
}, 450);
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
2199
|
function handleTitleChange(nextTitle: string) {
|
|
2091
2200
|
setLocalTitle(nextTitle);
|
|
2092
|
-
|
|
2201
|
+
if (!canEdit || !document) return;
|
|
2202
|
+
saveControllerRef.current?.changeTitle(nextTitle);
|
|
2093
2203
|
}
|
|
2094
2204
|
|
|
2095
2205
|
function handleContentChange(nextContent: string) {
|
|
2096
2206
|
setLocalContent(nextContent);
|
|
2097
|
-
|
|
2207
|
+
if (!canEdit || !document) return;
|
|
2208
|
+
saveControllerRef.current?.changeContent(nextContent);
|
|
2098
2209
|
}
|
|
2099
2210
|
|
|
2100
2211
|
function handleIconChange(nextIcon: string | null) {
|
|
@@ -2142,6 +2253,18 @@ function DatabaseItemPreview({
|
|
|
2142
2253
|
}
|
|
2143
2254
|
|
|
2144
2255
|
async function deletePreviewRow() {
|
|
2256
|
+
// Cancel (do NOT flush) before any row switch/close: we're deleting this
|
|
2257
|
+
// document, so a pending save must not resurrect it. Reset pending onto the
|
|
2258
|
+
// last-saved baseline so the controller's release flush is a no-op, and
|
|
2259
|
+
// record the id so any save already in flight is a no-op too (the
|
|
2260
|
+
// controller's save impl skips deleted ids).
|
|
2261
|
+
deletedIdsRef.current.add(item.document.id);
|
|
2262
|
+
const controller = saveControllerRef.current;
|
|
2263
|
+
if (controller) {
|
|
2264
|
+
controller.cancel();
|
|
2265
|
+
controller.mark(controller.lastSaved);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2145
2268
|
const nextPreviewItem = nextItem ?? previousItem;
|
|
2146
2269
|
if (nextPreviewItem) {
|
|
2147
2270
|
onPreviewItem?.(nextPreviewItem);
|
|
@@ -2150,10 +2273,6 @@ function DatabaseItemPreview({
|
|
|
2150
2273
|
}
|
|
2151
2274
|
|
|
2152
2275
|
try {
|
|
2153
|
-
if (saveTimerRef.current) {
|
|
2154
|
-
clearTimeout(saveTimerRef.current);
|
|
2155
|
-
saveTimerRef.current = null;
|
|
2156
|
-
}
|
|
2157
2276
|
await deleteDocument.mutateAsync({ id: item.document.id });
|
|
2158
2277
|
await queryClient.invalidateQueries({
|
|
2159
2278
|
queryKey: [
|
|
@@ -2166,6 +2285,7 @@ function DatabaseItemPreview({
|
|
|
2166
2285
|
queryKey: ["action", "list-documents"],
|
|
2167
2286
|
});
|
|
2168
2287
|
} catch (err) {
|
|
2288
|
+
deletedIdsRef.current.delete(item.document.id);
|
|
2169
2289
|
onPreviewItem?.(item);
|
|
2170
2290
|
toast.error("Failed to delete row", {
|
|
2171
2291
|
description:
|
|
@@ -2327,14 +2447,38 @@ function DatabaseItemPreview({
|
|
|
2327
2447
|
/>
|
|
2328
2448
|
) : null}
|
|
2329
2449
|
<div className="pt-6">
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2450
|
+
{(() => {
|
|
2451
|
+
// The peek's primary "Content" Blocks field is the document body.
|
|
2452
|
+
// No collab in the peek (ydoc=null), so it's a plain rich-text
|
|
2453
|
+
// editor saving through the preview document save path.
|
|
2454
|
+
const primaryEditor = (
|
|
2455
|
+
<VisualEditor
|
|
2456
|
+
key={document.id}
|
|
2457
|
+
documentId={document.id}
|
|
2458
|
+
content={localContent}
|
|
2459
|
+
onChange={handleContentChange}
|
|
2460
|
+
ydoc={null}
|
|
2461
|
+
editable={canEdit}
|
|
2462
|
+
/>
|
|
2463
|
+
);
|
|
2464
|
+
|
|
2465
|
+
// Render the peek body through the SAME component the full page
|
|
2466
|
+
// uses so ALL Blocks fields (Content + any others) appear with
|
|
2467
|
+
// identical loading/empty/solo/multi behavior — including the
|
|
2468
|
+
// empty state (no editable body when there are zero Blocks
|
|
2469
|
+
// fields). Only database rows have Blocks fields.
|
|
2470
|
+
if (document.databaseMembership) {
|
|
2471
|
+
return (
|
|
2472
|
+
<DocumentBlockFields
|
|
2473
|
+
documentId={document.id}
|
|
2474
|
+
canEdit={canEdit}
|
|
2475
|
+
primaryEditor={primaryEditor}
|
|
2476
|
+
/>
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
return primaryEditor;
|
|
2481
|
+
})()}
|
|
2338
2482
|
</div>
|
|
2339
2483
|
</div>
|
|
2340
2484
|
</div>
|
|
@@ -9120,6 +9264,16 @@ function isTablePropertyVisible(
|
|
|
9120
9264
|
}
|
|
9121
9265
|
|
|
9122
9266
|
function databaseTableCellDisplayValue(property: DocumentProperty) {
|
|
9267
|
+
// Blocks columns show a word count, never the dumped body content.
|
|
9268
|
+
if (property.definition.type === "blocks") {
|
|
9269
|
+
const content = typeof property.value === "string" ? property.value : "";
|
|
9270
|
+
const words = countWords(content);
|
|
9271
|
+
if (words === 0) return <span aria-hidden="true"> </span>;
|
|
9272
|
+
return (
|
|
9273
|
+
<span className="text-muted-foreground">{formatWordCount(content)}</span>
|
|
9274
|
+
);
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9123
9277
|
if (isEmptyPropertyValue(property.value)) {
|
|
9124
9278
|
return <span aria-hidden="true"> </span>;
|
|
9125
9279
|
}
|
|
@@ -12958,6 +13112,10 @@ function DatabaseTableRow({
|
|
|
12958
13112
|
>
|
|
12959
13113
|
{value}
|
|
12960
13114
|
</button>
|
|
13115
|
+
) : itemProperty.definition.type === "blocks" ? (
|
|
13116
|
+
// Blocks cells are a read-only word count in the table; the body
|
|
13117
|
+
// is edited on the page, not inline.
|
|
13118
|
+
value
|
|
12961
13119
|
) : canEdit && itemProperty.editable ? (
|
|
12962
13120
|
<PropertyValuePopover
|
|
12963
13121
|
property={itemProperty}
|
|
@@ -13,6 +13,7 @@ import { VisualEditor } from "./VisualEditor";
|
|
|
13
13
|
import { DocumentToolbar } from "./DocumentToolbar";
|
|
14
14
|
import { DocumentDatabase } from "./DocumentDatabase";
|
|
15
15
|
import { DocumentProperties } from "./DocumentProperties";
|
|
16
|
+
import { DocumentBlockFields } from "./DocumentBlockFields";
|
|
16
17
|
import { NotionConflictBanner } from "./NotionConflictBanner";
|
|
17
18
|
import { EmojiPicker } from "./EmojiPicker";
|
|
18
19
|
import {
|
|
@@ -958,37 +959,65 @@ function DocumentEditorBody({ documentId, document }: DocumentEditorBodyProps) {
|
|
|
958
959
|
}
|
|
959
960
|
}}
|
|
960
961
|
>
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
962
|
+
{(() => {
|
|
963
|
+
// The primary "Content" Blocks field IS the document body, with
|
|
964
|
+
// the full collaborative editor. It renders chromeless when it's
|
|
965
|
+
// the only Blocks field, or inside a header/collapsible shell
|
|
966
|
+
// when the row has multiple Blocks fields.
|
|
967
|
+
const primaryEditor = (
|
|
968
|
+
<VisualEditor
|
|
969
|
+
key={documentId}
|
|
970
|
+
documentId={documentId}
|
|
971
|
+
content={
|
|
972
|
+
isLocalFileDocument ? localContent : document.content
|
|
973
|
+
}
|
|
974
|
+
contentUpdatedAt={
|
|
975
|
+
isLocalFileDocument
|
|
976
|
+
? (localContentUpdatedAt ?? document.updatedAt)
|
|
977
|
+
: document.updatedAt
|
|
978
|
+
}
|
|
979
|
+
onChange={handleContentChange}
|
|
980
|
+
ydoc={canEdit && !isLocalFileDocument ? ydoc : null}
|
|
981
|
+
awareness={
|
|
982
|
+
canEdit && !isLocalFileDocument ? awareness : null
|
|
983
|
+
}
|
|
984
|
+
user={currentUser}
|
|
985
|
+
editable={canEdit}
|
|
986
|
+
localFileMode={isLocalFileDocument}
|
|
987
|
+
onComment={
|
|
988
|
+
canEdit && !isLocalFileDocument
|
|
989
|
+
? handleComment
|
|
990
|
+
: undefined
|
|
991
|
+
}
|
|
992
|
+
commentThreads={threads ?? []}
|
|
993
|
+
activeThreadId={activeThreadId}
|
|
994
|
+
pendingHighlight={pendingComment?.range ?? null}
|
|
995
|
+
onActivateThread={
|
|
996
|
+
canEdit && !isLocalFileDocument
|
|
997
|
+
? setActiveThreadId
|
|
998
|
+
: undefined
|
|
999
|
+
}
|
|
1000
|
+
onJoinTitle={joinFirstBodyBlockToTitle}
|
|
1001
|
+
notionPageLinks={notionPageLinks}
|
|
1002
|
+
onOpenNotionPageLink={handleOpenNotionPageLink}
|
|
1003
|
+
notionPageId={document.notionPageId}
|
|
1004
|
+
/>
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
// Only database rows have Blocks fields. Standalone pages and
|
|
1008
|
+
// local-file documents keep the plain chromeless body.
|
|
1009
|
+
if (document.databaseMembership && !isLocalFileDocument) {
|
|
1010
|
+
return (
|
|
1011
|
+
<DocumentBlockFields
|
|
1012
|
+
documentId={documentId}
|
|
1013
|
+
canEdit={canEdit}
|
|
1014
|
+
primaryEditor={primaryEditor}
|
|
1015
|
+
/>
|
|
1016
|
+
);
|
|
986
1017
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
notionPageId={document.notionPageId}
|
|
991
|
-
/>
|
|
1018
|
+
|
|
1019
|
+
return primaryEditor;
|
|
1020
|
+
})()}
|
|
992
1021
|
</div>
|
|
993
1022
|
</div>
|
|
994
1023
|
</div>
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
IconEdit,
|
|
20
20
|
IconEye,
|
|
21
21
|
IconEyeOff,
|
|
22
|
+
IconFileText,
|
|
22
23
|
IconHash,
|
|
23
24
|
IconLink,
|
|
24
25
|
IconList,
|
|
@@ -93,6 +94,7 @@ import {
|
|
|
93
94
|
documentPropertyDatePart,
|
|
94
95
|
isEmptyPropertyValue,
|
|
95
96
|
isComputedPropertyType,
|
|
97
|
+
isOnlyBlocksFieldDeletion,
|
|
96
98
|
normalizeDatePropertyValue,
|
|
97
99
|
type DocumentPropertyDateValue,
|
|
98
100
|
type DocumentPropertyOption,
|
|
@@ -132,6 +134,7 @@ export const TYPE_ICONS: Record<DocumentPropertyType, Icon> = {
|
|
|
132
134
|
email: IconAt,
|
|
133
135
|
phone: IconPhone,
|
|
134
136
|
relation: IconLink,
|
|
137
|
+
blocks: IconFileText,
|
|
135
138
|
id: IconNumber,
|
|
136
139
|
created_time: IconClockFilled,
|
|
137
140
|
created_by: IconUserCircle,
|
|
@@ -171,6 +174,7 @@ const PROPERTY_TYPE_SEARCH_ALIASES: Partial<
|
|
|
171
174
|
formula: ["calculate", "calculation", "computed", "equation"],
|
|
172
175
|
relation: ["relationship", "linked", "link", "database"],
|
|
173
176
|
rollup: ["aggregate", "aggregation", "sum", "count", "relation"],
|
|
177
|
+
blocks: ["content", "body", "rich text", "rich-text", "page", "notes"],
|
|
174
178
|
};
|
|
175
179
|
|
|
176
180
|
function slugify(value: string) {
|
|
@@ -642,7 +646,11 @@ export function DocumentProperties({
|
|
|
642
646
|
popoversPortalled = true,
|
|
643
647
|
}: DocumentPropertiesProps) {
|
|
644
648
|
const { data, isLoading } = useDocumentProperties(documentId);
|
|
645
|
-
|
|
649
|
+
// Blocks fields are rendered as body content (below the database/title), not
|
|
650
|
+
// as scalar property rows in this panel — exclude them here.
|
|
651
|
+
const properties = (data?.properties ?? []).filter(
|
|
652
|
+
(property) => property.definition.type !== "blocks",
|
|
653
|
+
);
|
|
646
654
|
const databaseId = data?.databaseId ?? null;
|
|
647
655
|
const visibleProperties = properties.filter(isPropertyVisible);
|
|
648
656
|
const hiddenProperties = properties.filter(
|
|
@@ -824,6 +832,16 @@ export function PropertyManagementPopover({
|
|
|
824
832
|
const configure = useConfigureDocumentProperty(documentId);
|
|
825
833
|
const duplicate = useDuplicateDocumentProperty(documentId);
|
|
826
834
|
const remove = useDeleteDocumentProperty(documentId);
|
|
835
|
+
const { data: propertiesData } = useDocumentProperties(documentId);
|
|
836
|
+
// Whether deleting THIS property removes the last Blocks field of the type —
|
|
837
|
+
// i.e. the body. Drives the yellow warning in the delete dialog.
|
|
838
|
+
const blocksFieldCount = (propertiesData?.properties ?? []).filter(
|
|
839
|
+
(item) => item.definition.type === "blocks",
|
|
840
|
+
).length;
|
|
841
|
+
const isOnlyBlocksField = isOnlyBlocksFieldDeletion({
|
|
842
|
+
type: property.definition.type,
|
|
843
|
+
blocksFieldCount,
|
|
844
|
+
});
|
|
827
845
|
const [open, setOpen] = useState(false);
|
|
828
846
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
829
847
|
const [name, setName] = useState(property.definition.name);
|
|
@@ -1173,6 +1191,12 @@ export function PropertyManagementPopover({
|
|
|
1173
1191
|
every document in this workspace.
|
|
1174
1192
|
</AlertDialogDescription>
|
|
1175
1193
|
</AlertDialogHeader>
|
|
1194
|
+
{isOnlyBlocksField ? (
|
|
1195
|
+
<div className="rounded-md border border-yellow-500/40 bg-yellow-500/10 px-3 py-2 text-sm text-yellow-800 dark:text-yellow-200">
|
|
1196
|
+
This is the only blocks property present in this object type. This
|
|
1197
|
+
will delete the main content (body) for all objects of this type.
|
|
1198
|
+
</div>
|
|
1199
|
+
) : null}
|
|
1176
1200
|
<AlertDialogFooter>
|
|
1177
1201
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
1178
1202
|
<AlertDialogAction
|