@agent-native/core 0.101.6 → 0.101.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +255 -12
- package/corpus/core/src/agent/thread-data-builder.ts +84 -11
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
- package/corpus/core/src/integrations/adapters/discord.ts +3 -1
- package/corpus/core/src/integrations/adapters/email.ts +4 -1
- package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
- package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
- package/corpus/core/src/integrations/adapters/slack.ts +38 -9
- package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
- package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
- package/corpus/core/src/integrations/index.ts +1 -0
- package/corpus/core/src/integrations/types.ts +9 -2
- package/corpus/core/src/integrations/webhook-handler.ts +69 -45
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
- package/corpus/templates/content/AGENTS.md +32 -9
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
- package/corpus/templates/content/actions/_database-utils.ts +104 -13
- package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/_property-utils.ts +7 -1
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/configure-document-property.ts +11 -0
- package/corpus/templates/content/actions/create-content-database.ts +11 -0
- package/corpus/templates/content/actions/create-document.ts +9 -0
- package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/export-content-source.ts +1 -0
- package/corpus/templates/content/actions/get-content-database.ts +13 -1
- package/corpus/templates/content/actions/get-document.ts +12 -2
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/import-content-source.ts +6 -0
- package/corpus/templates/content/actions/list-documents.ts +3 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/pull-document.ts +2 -0
- package/corpus/templates/content/actions/search-documents.ts +5 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-document.ts +16 -1
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/actions/view-screen.ts +15 -2
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
- package/corpus/templates/content/app/i18n-data.ts +339 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +51 -0
- package/corpus/templates/content/server/lib/document-context.ts +76 -0
- package/corpus/templates/content/server/lib/public-documents.ts +3 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +59 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
- package/corpus/templates/content/shared/api.ts +87 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/content-source.ts +7 -0
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/content/shared/properties.ts +2 -0
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/a2a/artifact-response.d.ts +15 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +204 -5
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +6 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +71 -8
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +72 -6
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/adapters/discord.d.ts.map +1 -1
- package/dist/integrations/adapters/discord.js +1 -0
- package/dist/integrations/adapters/discord.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +2 -0
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
- package/dist/integrations/adapters/google-docs.js +2 -0
- package/dist/integrations/adapters/google-docs.js.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.js +1 -0
- package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +21 -6
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/adapters/telegram.d.ts.map +1 -1
- package/dist/integrations/adapters/telegram.js +11 -2
- package/dist/integrations/adapters/telegram.js.map +1 -1
- package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
- package/dist/integrations/adapters/whatsapp.js +3 -0
- package/dist/integrations/adapters/whatsapp.js.map +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/types.d.ts +8 -2
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +40 -43
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +1 -0
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useBuilderStatus,
|
|
6
6
|
useCodeMode,
|
|
7
7
|
useSession,
|
|
8
|
+
useT,
|
|
8
9
|
} from "@agent-native/core/client";
|
|
9
10
|
import {
|
|
10
11
|
AlertDialog,
|
|
@@ -48,7 +49,6 @@ import {
|
|
|
48
49
|
TooltipTrigger,
|
|
49
50
|
} from "@agent-native/toolkit/ui/tooltip";
|
|
50
51
|
import {
|
|
51
|
-
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
52
52
|
type BuilderCmsModelSummary,
|
|
53
53
|
type ContentDatabaseItem,
|
|
54
54
|
type ContentDatabaseResponse,
|
|
@@ -56,6 +56,8 @@ import {
|
|
|
56
56
|
type ContentDatabaseSourceChangeSet,
|
|
57
57
|
type ContentDatabaseSourceJoinRequest,
|
|
58
58
|
type ContentDatabaseSourceReviewPayload,
|
|
59
|
+
type ContentDatabaseSourceWriteMode,
|
|
60
|
+
type ProcessBuilderBodyHydrationResponse,
|
|
59
61
|
type SourceJoinSuggestion,
|
|
60
62
|
type ContentDatabasePersonalViewOverrides,
|
|
61
63
|
type ContentDatabaseView,
|
|
@@ -131,6 +133,7 @@ import {
|
|
|
131
133
|
import { useQueryClient } from "@tanstack/react-query";
|
|
132
134
|
import {
|
|
133
135
|
useEffect,
|
|
136
|
+
useCallback,
|
|
134
137
|
useMemo,
|
|
135
138
|
useRef,
|
|
136
139
|
useState,
|
|
@@ -148,6 +151,7 @@ import {
|
|
|
148
151
|
useAddContentDatabaseSourceFieldProperty,
|
|
149
152
|
useAttachContentDatabaseSource,
|
|
150
153
|
useBuilderCmsModels,
|
|
154
|
+
useCancelPreparedBuilderSourceUpdate,
|
|
151
155
|
useChangeContentDatabaseSourceRole,
|
|
152
156
|
useContentDatabase,
|
|
153
157
|
useContentDatabasePersonalView,
|
|
@@ -159,8 +163,10 @@ import {
|
|
|
159
163
|
useDuplicateDatabaseItems,
|
|
160
164
|
useExecuteBuilderSourceExecution,
|
|
161
165
|
useMoveDatabaseItem,
|
|
166
|
+
useMaterializeBuilderRequiredFields,
|
|
162
167
|
useNotionDatabaseSources,
|
|
163
168
|
usePrepareBuilderSourceReview,
|
|
169
|
+
usePreviewBuilderSourceReview,
|
|
164
170
|
useProcessBuilderBodyHydration,
|
|
165
171
|
useRefreshContentDatabaseSource,
|
|
166
172
|
useSetContentDatabaseSourceWriteMode,
|
|
@@ -173,9 +179,12 @@ import {
|
|
|
173
179
|
useSetDocumentProperty,
|
|
174
180
|
} from "@/hooks/use-document-properties";
|
|
175
181
|
import {
|
|
182
|
+
isDocumentUpdateConflict,
|
|
176
183
|
useDeleteDocument,
|
|
177
184
|
useDocument,
|
|
178
185
|
seedDatabaseItemDocumentCaches,
|
|
186
|
+
usePreviewDocumentDraft,
|
|
187
|
+
useUpdatePreviewDocumentDraft,
|
|
179
188
|
useUpdateDocument,
|
|
180
189
|
} from "@/hooks/use-documents";
|
|
181
190
|
import { messagesByLocale } from "@/i18n-data";
|
|
@@ -188,6 +197,7 @@ import {
|
|
|
188
197
|
isEffectivelyEmptyDocumentContent,
|
|
189
198
|
previewBodyHydrationIsPending,
|
|
190
199
|
previewBodyHydrationIsTerminalError,
|
|
200
|
+
previewDraftConflictsWithHydratedBody,
|
|
191
201
|
shouldIgnorePreviewEmptyNormalization,
|
|
192
202
|
} from "../body-hydration";
|
|
193
203
|
import {
|
|
@@ -200,6 +210,7 @@ import {
|
|
|
200
210
|
BuilderSourceReviewDialog,
|
|
201
211
|
type BuilderReviewPublicationTransitions,
|
|
202
212
|
} from "../database-sources/BuilderSourceReviewDialog";
|
|
213
|
+
import { DescriptionField } from "../DescriptionField";
|
|
203
214
|
import { DocumentBlockFields } from "../DocumentBlockFields";
|
|
204
215
|
import {
|
|
205
216
|
AddProperty,
|
|
@@ -225,7 +236,8 @@ import {
|
|
|
225
236
|
import { EmojiPicker } from "../EmojiPicker";
|
|
226
237
|
import {
|
|
227
238
|
createPreviewDocumentSaveController,
|
|
228
|
-
|
|
239
|
+
deferredPreviewDocumentSave,
|
|
240
|
+
type PreviewDocumentSaveAdapter,
|
|
229
241
|
} from "../previewDocumentSaveController";
|
|
230
242
|
import {
|
|
231
243
|
acquirePreviewDocumentSaveController,
|
|
@@ -250,6 +262,26 @@ export interface DatabaseViewProps {
|
|
|
250
262
|
const CONTENT_DATABASE_PAGE_SIZE = 100;
|
|
251
263
|
const CONTENT_DATABASE_MAX_ITEM_LIMIT = 5_000;
|
|
252
264
|
|
|
265
|
+
export function databaseSearchExpandedItemLimit(
|
|
266
|
+
searchQuery: string,
|
|
267
|
+
currentLimit: number,
|
|
268
|
+
totalItemCount: number,
|
|
269
|
+
) {
|
|
270
|
+
if (!searchQuery.trim()) return currentLimit;
|
|
271
|
+
return Math.max(
|
|
272
|
+
currentLimit,
|
|
273
|
+
Math.min(totalItemCount, CONTENT_DATABASE_MAX_ITEM_LIMIT),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function databaseSearchExpansionIsPending(
|
|
278
|
+
searchQuery: string,
|
|
279
|
+
requestedLimit: number,
|
|
280
|
+
responseLimit: number,
|
|
281
|
+
) {
|
|
282
|
+
return !!searchQuery.trim() && responseLimit < requestedLimit;
|
|
283
|
+
}
|
|
284
|
+
|
|
253
285
|
export type SortDirection = ContentDatabaseSortDirection;
|
|
254
286
|
export type DatabaseSort = ContentDatabaseSort;
|
|
255
287
|
export type FilterOperator = ContentDatabaseFilterOperator;
|
|
@@ -289,6 +321,32 @@ export const BUILDER_SOURCE_CONTINUATION_MAX_BACKOFF_MS = 30_000;
|
|
|
289
321
|
export type PersonalDatabaseViewOverrides =
|
|
290
322
|
ContentDatabasePersonalViewOverrides;
|
|
291
323
|
|
|
324
|
+
type BuilderSourceWriteSettingsInput = {
|
|
325
|
+
sourceId: string;
|
|
326
|
+
writeMode: ContentDatabaseSourceWriteMode;
|
|
327
|
+
allowPublicationTransitions?: boolean;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export function previewDraftNeedsConflict(args: {
|
|
331
|
+
returnedDraft: { title: string; content: string };
|
|
332
|
+
pending: { title: string; content: string };
|
|
333
|
+
}) {
|
|
334
|
+
return (
|
|
335
|
+
args.returnedDraft.title !== args.pending.title ||
|
|
336
|
+
args.returnedDraft.content !== args.pending.content
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function previewDraftMissingCasRecovery(args: {
|
|
341
|
+
operation: "upsert" | "delete";
|
|
342
|
+
allowCreateRetry: boolean;
|
|
343
|
+
}) {
|
|
344
|
+
if (args.operation === "delete") return "converged" as const;
|
|
345
|
+
return args.allowCreateRetry
|
|
346
|
+
? ("retry-create" as const)
|
|
347
|
+
: ("failed" as const);
|
|
348
|
+
}
|
|
349
|
+
|
|
292
350
|
type DatabaseMessageKey = keyof (typeof messagesByLocale)["en-US"]["database"];
|
|
293
351
|
|
|
294
352
|
export function dbText(
|
|
@@ -313,6 +371,96 @@ export function dbText(
|
|
|
313
371
|
export type CreateDatabaseRowHandler = (
|
|
314
372
|
title?: string,
|
|
315
373
|
) => Promise<ContentDatabaseItem | null>;
|
|
374
|
+
|
|
375
|
+
export function databaseCreatedItemForImmediatePreview(
|
|
376
|
+
response: ContentDatabaseResponse,
|
|
377
|
+
args: {
|
|
378
|
+
databaseId: string;
|
|
379
|
+
parentDocument: Document;
|
|
380
|
+
title: string;
|
|
381
|
+
propertyValues: Record<string, DocumentPropertyValue>;
|
|
382
|
+
now?: string;
|
|
383
|
+
},
|
|
384
|
+
): ContentDatabaseItem | null {
|
|
385
|
+
const returnedItem = response.items.find(
|
|
386
|
+
(item) => item.id === response.createdItemId,
|
|
387
|
+
);
|
|
388
|
+
if (returnedItem) return returnedItem;
|
|
389
|
+
if (!response.createdItemId || !response.createdDocumentId) return null;
|
|
390
|
+
|
|
391
|
+
const now = args.now ?? new Date().toISOString();
|
|
392
|
+
const position = Math.max(
|
|
393
|
+
0,
|
|
394
|
+
(response.pagination?.totalItems ?? response.items.length + 1) - 1,
|
|
395
|
+
);
|
|
396
|
+
return {
|
|
397
|
+
id: response.createdItemId,
|
|
398
|
+
databaseId: args.databaseId,
|
|
399
|
+
position,
|
|
400
|
+
properties: response.properties.map((property) => ({
|
|
401
|
+
...property,
|
|
402
|
+
value: Object.prototype.hasOwnProperty.call(
|
|
403
|
+
args.propertyValues,
|
|
404
|
+
property.definition.id,
|
|
405
|
+
)
|
|
406
|
+
? args.propertyValues[property.definition.id]
|
|
407
|
+
: null,
|
|
408
|
+
})),
|
|
409
|
+
document: {
|
|
410
|
+
id: response.createdDocumentId,
|
|
411
|
+
parentId: args.parentDocument.id,
|
|
412
|
+
title: args.title.trim(),
|
|
413
|
+
content: "",
|
|
414
|
+
icon: null,
|
|
415
|
+
position,
|
|
416
|
+
isFavorite: false,
|
|
417
|
+
hideFromSearch: args.parentDocument.hideFromSearch,
|
|
418
|
+
visibility: args.parentDocument.visibility,
|
|
419
|
+
accessRole: args.parentDocument.accessRole,
|
|
420
|
+
canEdit: true,
|
|
421
|
+
canManage: args.parentDocument.canManage,
|
|
422
|
+
createdAt: now,
|
|
423
|
+
updatedAt: now,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function databaseBuilderHydrationSourceForItem(
|
|
429
|
+
item: ContentDatabaseItem,
|
|
430
|
+
sources: ContentDatabaseSource[],
|
|
431
|
+
) {
|
|
432
|
+
const sourceId = item.document.databaseMembership?.sourceId;
|
|
433
|
+
if (!sourceId) return null;
|
|
434
|
+
const itemSource = sources.find((candidate) => candidate.id === sourceId);
|
|
435
|
+
return itemSource?.sourceType === "builder-cms" ? itemSource : null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function databasePreviewItem(
|
|
439
|
+
items: ContentDatabaseItem[],
|
|
440
|
+
previewDocumentId: string | null,
|
|
441
|
+
createdPreviewItem: ContentDatabaseItem | null,
|
|
442
|
+
) {
|
|
443
|
+
return (
|
|
444
|
+
items.find((item) => item.document.id === previewDocumentId) ??
|
|
445
|
+
(createdPreviewItem?.document.id === previewDocumentId
|
|
446
|
+
? createdPreviewItem
|
|
447
|
+
: null)
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function databaseCreatedItemNeedsPreview(
|
|
452
|
+
items: ContentDatabaseItem[],
|
|
453
|
+
createdItem: ContentDatabaseItem,
|
|
454
|
+
options: {
|
|
455
|
+
openAfterCreate?: boolean;
|
|
456
|
+
focusInlineTitle?: boolean;
|
|
457
|
+
},
|
|
458
|
+
) {
|
|
459
|
+
if (options.openAfterCreate !== false) return true;
|
|
460
|
+
if (!options.focusInlineTitle) return false;
|
|
461
|
+
return !items.some((item) => item.id === createdItem.id);
|
|
462
|
+
}
|
|
463
|
+
|
|
316
464
|
type DatabaseDragPreviewState =
|
|
317
465
|
| {
|
|
318
466
|
kind: "view";
|
|
@@ -514,6 +662,7 @@ function DatabaseTable({
|
|
|
514
662
|
canEdit: boolean;
|
|
515
663
|
isActive: boolean;
|
|
516
664
|
}) {
|
|
665
|
+
const t = useT();
|
|
517
666
|
const navigate = useNavigate();
|
|
518
667
|
const queryClient = useQueryClient();
|
|
519
668
|
const [databaseItemLimit, setDatabaseItemLimit] = useState(
|
|
@@ -528,8 +677,12 @@ function DatabaseTable({
|
|
|
528
677
|
const processBuilderBodies = useProcessBuilderBodyHydration(document.id);
|
|
529
678
|
const prepareBuilderReview = usePrepareBuilderSourceReview(document.id);
|
|
530
679
|
const executeBuilderExecution = useExecuteBuilderSourceExecution(document.id);
|
|
680
|
+
const cancelPreparedBuilderUpdate = useCancelPreparedBuilderSourceUpdate(
|
|
681
|
+
document.id,
|
|
682
|
+
);
|
|
531
683
|
const setSourceWriteMode = useSetContentDatabaseSourceWriteMode(document.id);
|
|
532
684
|
const setProperty = useSetDocumentProperty(document.id, document.id);
|
|
685
|
+
const updateDatabaseDocument = useUpdateDocument();
|
|
533
686
|
const updateView = useUpdateContentDatabaseView(document.id);
|
|
534
687
|
// A deleted/missing database resolves to the unavailable union (no
|
|
535
688
|
// `database` field) — treat it as no data; the inline-block wrapper owns
|
|
@@ -550,10 +703,19 @@ function DatabaseTable({
|
|
|
550
703
|
const personalView = useContentDatabasePersonalView(databaseId);
|
|
551
704
|
const updatePersonalView = useUpdateContentDatabasePersonalView(databaseId);
|
|
552
705
|
const source = data?.source ?? null;
|
|
553
|
-
const sources =
|
|
706
|
+
const sources = useMemo(
|
|
707
|
+
() => databaseAttachedSources(data?.sources, source),
|
|
708
|
+
[data?.sources, source],
|
|
709
|
+
);
|
|
710
|
+
const builderSources = useMemo(
|
|
711
|
+
() => databaseAttachedBuilderSources(sources, source),
|
|
712
|
+
[sources, source],
|
|
713
|
+
);
|
|
554
714
|
const [previewDocumentId, setPreviewDocumentId] = useState<string | null>(
|
|
555
715
|
null,
|
|
556
716
|
);
|
|
717
|
+
const [createdPreviewItem, setCreatedPreviewItem] =
|
|
718
|
+
useState<ContentDatabaseItem | null>(null);
|
|
557
719
|
const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
|
|
558
720
|
const [previewTitleFocusDocumentId, setPreviewTitleFocusDocumentId] =
|
|
559
721
|
useState<string | null>(null);
|
|
@@ -562,6 +724,18 @@ function DatabaseTable({
|
|
|
562
724
|
>(null);
|
|
563
725
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
564
726
|
const [searchQuery, setSearchQuery] = useState("");
|
|
727
|
+
const searchExpandedItemLimit = databaseSearchExpandedItemLimit(
|
|
728
|
+
searchQuery,
|
|
729
|
+
databaseItemLimit,
|
|
730
|
+
totalItemCount,
|
|
731
|
+
);
|
|
732
|
+
const isSearchExpansionPending = databaseSearchExpansionIsPending(
|
|
733
|
+
searchQuery,
|
|
734
|
+
searchExpandedItemLimit,
|
|
735
|
+
data?.pagination?.limit ?? items.length,
|
|
736
|
+
);
|
|
737
|
+
const isDatabaseViewLoading =
|
|
738
|
+
isDatabaseInitialLoading || isSearchExpansionPending;
|
|
565
739
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
566
740
|
const [inlineFilterControlsOpen, setInlineFilterControlsOpen] =
|
|
567
741
|
useState(false);
|
|
@@ -576,11 +750,28 @@ function DatabaseTable({
|
|
|
576
750
|
null,
|
|
577
751
|
);
|
|
578
752
|
const [builderReviewOpen, setBuilderReviewOpen] = useState(false);
|
|
753
|
+
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
if (searchExpandedItemLimit === databaseItemLimit) return;
|
|
756
|
+
setDatabaseItemLimit(searchExpandedItemLimit);
|
|
757
|
+
}, [databaseItemLimit, searchExpandedItemLimit]);
|
|
758
|
+
const [builderReviewSourceId, setBuilderReviewSourceId] = useState<
|
|
759
|
+
string | null
|
|
760
|
+
>(null);
|
|
579
761
|
const [builderReviewResult, setBuilderReviewResult] =
|
|
580
762
|
useState<ContentDatabaseSourceReviewPayload | null>(null);
|
|
581
763
|
const [builderReviewCheckedAt, setBuilderReviewCheckedAt] = useState<
|
|
582
764
|
string | null
|
|
583
765
|
>(null);
|
|
766
|
+
const [builderReviewError, setBuilderReviewError] = useState<string | null>(
|
|
767
|
+
null,
|
|
768
|
+
);
|
|
769
|
+
const [
|
|
770
|
+
preparedBuilderReviewConfirmation,
|
|
771
|
+
setPreparedBuilderReviewConfirmation,
|
|
772
|
+
] = useState<PreparedBuilderReviewConfirmation | null>(null);
|
|
773
|
+
const [builderReviewSelectionRemap, setBuilderReviewSelectionRemap] =
|
|
774
|
+
useState<Record<string, string> | null>(null);
|
|
584
775
|
const [settingsPanel, setSettingsPanel] =
|
|
585
776
|
useState<DatabaseSettingsPanel>("main");
|
|
586
777
|
const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
|
|
@@ -625,29 +816,22 @@ function DatabaseTable({
|
|
|
625
816
|
const personalViewSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
626
817
|
null,
|
|
627
818
|
);
|
|
628
|
-
const autoContinueBuilderSourceRef = useRef<string
|
|
629
|
-
const builderContinuationWatchdogRef = useRef<
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
819
|
+
const autoContinueBuilderSourceRef = useRef<Set<string>>(new Set());
|
|
820
|
+
const builderContinuationWatchdogRef = useRef<Map<string, number>>(new Map());
|
|
821
|
+
const refreshSourceInFlightRef = useRef<string | null>(null);
|
|
822
|
+
const hydrationSourceInFlightRef = useRef<string | null>(null);
|
|
823
|
+
const builderReviewGenerationRef = useRef(0);
|
|
824
|
+
const builderReviewSessionRef = useRef<BuilderReviewSession | null>(null);
|
|
633
825
|
const [
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
] = useState<string
|
|
637
|
-
const autoPumpBuilderBodiesRef = useRef<string
|
|
638
|
-
const [
|
|
639
|
-
useState<string
|
|
640
|
-
const [builderProgressHighWater, setBuilderProgressHighWater] = useState<
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
hydratedCount: number;
|
|
644
|
-
rowsComplete: boolean;
|
|
645
|
-
}>({
|
|
646
|
-
sourceId: null,
|
|
647
|
-
fetchedCount: 0,
|
|
648
|
-
hydratedCount: 0,
|
|
649
|
-
rowsComplete: false,
|
|
650
|
-
});
|
|
826
|
+
builderContinuationClientErrorKeys,
|
|
827
|
+
setBuilderContinuationClientErrorKeys,
|
|
828
|
+
] = useState<string[]>([]);
|
|
829
|
+
const autoPumpBuilderBodiesRef = useRef<Set<string>>(new Set());
|
|
830
|
+
const [builderHydrationClientErrorKeys, setBuilderHydrationClientErrorKeys] =
|
|
831
|
+
useState<string[]>([]);
|
|
832
|
+
const [builderProgressHighWater, setBuilderProgressHighWater] = useState<
|
|
833
|
+
Record<string, BuilderSourceProgressHighWater>
|
|
834
|
+
>({});
|
|
651
835
|
const previewStateRef = useRef<{
|
|
652
836
|
documentId: string | null;
|
|
653
837
|
visibleItems: ContentDatabaseItem[];
|
|
@@ -704,8 +888,11 @@ function DatabaseTable({
|
|
|
704
888
|
activeFilters.length === 0 &&
|
|
705
889
|
!databaseGroupProperty;
|
|
706
890
|
const hasResultConstraints = !!searchQuery.trim() || activeFilters.length > 0;
|
|
707
|
-
const previewItem =
|
|
708
|
-
items
|
|
891
|
+
const previewItem = databasePreviewItem(
|
|
892
|
+
items,
|
|
893
|
+
previewDocumentId,
|
|
894
|
+
createdPreviewItem,
|
|
895
|
+
);
|
|
709
896
|
const previousPreviewItem = previewItem
|
|
710
897
|
? databaseItemPreviewNeighbor(
|
|
711
898
|
screenVisibleItems,
|
|
@@ -727,18 +914,136 @@ function DatabaseTable({
|
|
|
727
914
|
() => databaseSelectedItems(visibleItems, selectedItemIds),
|
|
728
915
|
[visibleItems, selectedItemIds],
|
|
729
916
|
);
|
|
917
|
+
const builderReviewSource = databaseBuilderReviewSource(
|
|
918
|
+
sources,
|
|
919
|
+
source,
|
|
920
|
+
builderReviewSourceId,
|
|
921
|
+
);
|
|
922
|
+
const builderReviewPreviewQuery = usePreviewBuilderSourceReview({
|
|
923
|
+
documentId: document.id,
|
|
924
|
+
sourceId: builderReviewSourceId,
|
|
925
|
+
scope: selectedItemIds.length > 0 ? "selected" : "all",
|
|
926
|
+
documentIds: selectedItems.map((item) => item.document.id),
|
|
927
|
+
enabled: builderReviewOpen,
|
|
928
|
+
});
|
|
929
|
+
const completeBuilderReviewPreview =
|
|
930
|
+
builderReviewPreviewQuery.data &&
|
|
931
|
+
builderReviewSource &&
|
|
932
|
+
builderReviewPreviewQuery.data.sourceId === builderReviewSource.id &&
|
|
933
|
+
builderReviewPreviewQuery.data.sourceTable ===
|
|
934
|
+
builderReviewSource.sourceTable
|
|
935
|
+
? builderReviewPreviewQuery.data
|
|
936
|
+
: null;
|
|
730
937
|
const builderReviewChangeSets = useMemo(
|
|
731
|
-
() => builderReviewableChangeSets(
|
|
732
|
-
[
|
|
938
|
+
() => builderReviewableChangeSets(builderReviewSource),
|
|
939
|
+
[builderReviewSource],
|
|
733
940
|
);
|
|
734
941
|
const builderReviewPreview = useMemo(
|
|
735
942
|
() =>
|
|
736
|
-
|
|
737
|
-
? buildClientBuilderReviewPayload(
|
|
943
|
+
builderReviewSource && builderReviewChangeSets.length > 0
|
|
944
|
+
? buildClientBuilderReviewPayload(
|
|
945
|
+
builderReviewSource,
|
|
946
|
+
builderReviewChangeSets,
|
|
947
|
+
)
|
|
738
948
|
: null,
|
|
739
|
-
[builderReviewChangeSets,
|
|
949
|
+
[builderReviewChangeSets, builderReviewSource],
|
|
950
|
+
);
|
|
951
|
+
const activeBuilderReview =
|
|
952
|
+
builderReviewResult ??
|
|
953
|
+
(builderReviewOpen
|
|
954
|
+
? (completeBuilderReviewPreview?.review ?? null)
|
|
955
|
+
: builderReviewPreview);
|
|
956
|
+
const sourcePendingOperations: DatabaseSourcePendingOperations = {
|
|
957
|
+
attach: attachSource.isPending,
|
|
958
|
+
changeRole: changeSourceRole.isPending,
|
|
959
|
+
refresh:
|
|
960
|
+
refreshSource.isPending || refreshSourceInFlightRef.current !== null,
|
|
961
|
+
hydration:
|
|
962
|
+
processBuilderBodies.isPending ||
|
|
963
|
+
hydrationSourceInFlightRef.current !== null,
|
|
964
|
+
disconnect: disconnectSource.isPending,
|
|
965
|
+
review: prepareBuilderReview.isPending,
|
|
966
|
+
execute: executeBuilderExecution.isPending,
|
|
967
|
+
writeMode: setSourceWriteMode.isPending,
|
|
968
|
+
changeRoleSourceId: pendingMutationSourceId(
|
|
969
|
+
changeSourceRole.isPending,
|
|
970
|
+
changeSourceRole.variables,
|
|
971
|
+
),
|
|
972
|
+
refreshSourceId:
|
|
973
|
+
refreshSourceInFlightRef.current ??
|
|
974
|
+
pendingMutationSourceId(refreshSource.isPending, refreshSource.variables),
|
|
975
|
+
hydrationSourceId:
|
|
976
|
+
hydrationSourceInFlightRef.current ??
|
|
977
|
+
pendingMutationSourceId(
|
|
978
|
+
processBuilderBodies.isPending,
|
|
979
|
+
processBuilderBodies.variables,
|
|
980
|
+
),
|
|
981
|
+
disconnectSourceId: pendingMutationSourceId(
|
|
982
|
+
disconnectSource.isPending,
|
|
983
|
+
disconnectSource.variables,
|
|
984
|
+
),
|
|
985
|
+
reviewSourceId: pendingMutationSourceId(
|
|
986
|
+
prepareBuilderReview.isPending,
|
|
987
|
+
prepareBuilderReview.variables,
|
|
988
|
+
),
|
|
989
|
+
executeSourceId: pendingMutationSourceId(
|
|
990
|
+
executeBuilderExecution.isPending,
|
|
991
|
+
executeBuilderExecution.variables,
|
|
992
|
+
),
|
|
993
|
+
writeModeSourceId: pendingMutationSourceId(
|
|
994
|
+
setSourceWriteMode.isPending,
|
|
995
|
+
setSourceWriteMode.variables,
|
|
996
|
+
),
|
|
997
|
+
};
|
|
998
|
+
const runSourceRefresh = useCallback(
|
|
999
|
+
(sourceId: string, onError?: () => void) => {
|
|
1000
|
+
if (!acquireDatabaseSourceOperation(refreshSourceInFlightRef, sourceId)) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
refreshSource.mutate(
|
|
1004
|
+
{ documentId: document.id, sourceId },
|
|
1005
|
+
{
|
|
1006
|
+
onError,
|
|
1007
|
+
onSettled: () => {
|
|
1008
|
+
releaseDatabaseSourceOperation(refreshSourceInFlightRef, sourceId);
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
);
|
|
1012
|
+
return true;
|
|
1013
|
+
},
|
|
1014
|
+
[document.id, refreshSource.mutate],
|
|
1015
|
+
);
|
|
1016
|
+
const runBuilderHydration = useCallback(
|
|
1017
|
+
(
|
|
1018
|
+
sourceId: string,
|
|
1019
|
+
options: {
|
|
1020
|
+
onSuccess?: (result: ProcessBuilderBodyHydrationResponse) => void;
|
|
1021
|
+
onError?: () => void;
|
|
1022
|
+
} = {},
|
|
1023
|
+
request: { documentId?: string; limit?: number } = {},
|
|
1024
|
+
) => {
|
|
1025
|
+
if (
|
|
1026
|
+
!acquireDatabaseSourceOperation(hydrationSourceInFlightRef, sourceId)
|
|
1027
|
+
) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
processBuilderBodies.mutate(
|
|
1031
|
+
{ sourceId, ...request },
|
|
1032
|
+
{
|
|
1033
|
+
onSuccess: options.onSuccess,
|
|
1034
|
+
onError: options.onError,
|
|
1035
|
+
onSettled: () => {
|
|
1036
|
+
releaseDatabaseSourceOperation(
|
|
1037
|
+
hydrationSourceInFlightRef,
|
|
1038
|
+
sourceId,
|
|
1039
|
+
);
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
);
|
|
1043
|
+
return true;
|
|
1044
|
+
},
|
|
1045
|
+
[processBuilderBodies.mutate],
|
|
740
1046
|
);
|
|
741
|
-
const activeBuilderReview = builderReviewResult ?? builderReviewPreview;
|
|
742
1047
|
|
|
743
1048
|
useEffect(() => {
|
|
744
1049
|
previewStateRef.current = {
|
|
@@ -754,191 +1059,125 @@ function DatabaseTable({
|
|
|
754
1059
|
}, [visibleItems]);
|
|
755
1060
|
|
|
756
1061
|
useEffect(() => {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
sourceId: null,
|
|
762
|
-
fetchedCount: 0,
|
|
763
|
-
hydratedCount: 0,
|
|
764
|
-
rowsComplete: false,
|
|
765
|
-
});
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
const rawFetchedCount =
|
|
769
|
-
typeof source.metadata.lastReadFetchedEntryCount === "number"
|
|
770
|
-
? source.metadata.lastReadFetchedEntryCount
|
|
771
|
-
: typeof source.metadata.lastReadEntryCount === "number"
|
|
772
|
-
? source.metadata.lastReadEntryCount
|
|
773
|
-
: 0;
|
|
774
|
-
const fetchedCount =
|
|
775
|
-
source.metadata.lastReadHasMore === false
|
|
776
|
-
? Math.max(rawFetchedCount, source.bodyHydration?.total ?? 0)
|
|
777
|
-
: rawFetchedCount;
|
|
778
|
-
const hydratedCount = source.bodyHydration?.hydrated ?? 0;
|
|
779
|
-
const rowsComplete = source.metadata.lastReadHasMore === false;
|
|
780
|
-
setBuilderProgressHighWater((current) => {
|
|
781
|
-
if (current.sourceId !== source.id) {
|
|
782
|
-
return {
|
|
783
|
-
sourceId: source.id,
|
|
784
|
-
fetchedCount,
|
|
785
|
-
hydratedCount,
|
|
786
|
-
rowsComplete,
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
const next = {
|
|
790
|
-
sourceId: source.id,
|
|
791
|
-
fetchedCount: Math.max(current.fetchedCount, fetchedCount),
|
|
792
|
-
hydratedCount: Math.max(current.hydratedCount, hydratedCount),
|
|
793
|
-
rowsComplete: current.rowsComplete || rowsComplete,
|
|
794
|
-
};
|
|
795
|
-
return next.fetchedCount === current.fetchedCount &&
|
|
796
|
-
next.hydratedCount === current.hydratedCount &&
|
|
797
|
-
next.rowsComplete === current.rowsComplete
|
|
798
|
-
? current
|
|
799
|
-
: next;
|
|
800
|
-
});
|
|
801
|
-
}, [source]);
|
|
1062
|
+
setBuilderProgressHighWater((current) =>
|
|
1063
|
+
databaseBuilderProgressHighWater(builderSources, current),
|
|
1064
|
+
);
|
|
1065
|
+
}, [builderSources]);
|
|
802
1066
|
|
|
803
1067
|
useEffect(() => {
|
|
1068
|
+
if (!isActive || !canEdit || refreshSource.isPending) return;
|
|
1069
|
+
const candidate = databaseNextBuilderContinuationSource(builderSources, {
|
|
1070
|
+
attemptedKeys: autoContinueBuilderSourceRef.current,
|
|
1071
|
+
errorKeys: new Set(builderContinuationClientErrorKeys),
|
|
1072
|
+
});
|
|
1073
|
+
if (!candidate) return;
|
|
1074
|
+
const continuationKey = builderSourceContinuationKey(candidate);
|
|
1075
|
+
if (!continuationKey) return;
|
|
1076
|
+
databaseRecordBuilderContinuationAttempt(
|
|
1077
|
+
autoContinueBuilderSourceRef.current,
|
|
1078
|
+
continuationKey,
|
|
1079
|
+
);
|
|
804
1080
|
if (
|
|
805
|
-
!
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
refreshSource.isPending
|
|
811
|
-
) {
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
const sourceStatus = builderSourceRowFetchStatus(source);
|
|
815
|
-
if (
|
|
816
|
-
sourceStatus !== "fetching" ||
|
|
817
|
-
source.metadata.lastReadHasMore !== true
|
|
1081
|
+
!runSourceRefresh(candidate.id, () =>
|
|
1082
|
+
setBuilderContinuationClientErrorKeys((current) =>
|
|
1083
|
+
addUniqueKey(current, continuationKey),
|
|
1084
|
+
),
|
|
1085
|
+
)
|
|
818
1086
|
) {
|
|
819
|
-
|
|
1087
|
+
autoContinueBuilderSourceRef.current.delete(continuationKey);
|
|
820
1088
|
}
|
|
821
|
-
const continuationKey = builderSourceContinuationKey(source);
|
|
822
|
-
if (!continuationKey) return;
|
|
823
|
-
if (builderContinuationClientErrorKey === continuationKey) return;
|
|
824
|
-
if (autoContinueBuilderSourceRef.current === continuationKey) return;
|
|
825
|
-
autoContinueBuilderSourceRef.current = continuationKey;
|
|
826
|
-
refreshSource.mutate(
|
|
827
|
-
{
|
|
828
|
-
documentId: document.id,
|
|
829
|
-
sourceId: source.id,
|
|
830
|
-
},
|
|
831
|
-
{
|
|
832
|
-
onError: () => setBuilderContinuationClientErrorKey(continuationKey),
|
|
833
|
-
},
|
|
834
|
-
);
|
|
835
1089
|
}, [
|
|
836
|
-
|
|
1090
|
+
builderContinuationClientErrorKeys,
|
|
1091
|
+
builderSources,
|
|
837
1092
|
canEdit,
|
|
838
|
-
document.id,
|
|
839
1093
|
isActive,
|
|
840
|
-
refreshSource.mutate,
|
|
841
1094
|
refreshSource.isPending,
|
|
842
|
-
|
|
1095
|
+
runSourceRefresh,
|
|
843
1096
|
]);
|
|
844
1097
|
|
|
845
1098
|
useEffect(() => {
|
|
1099
|
+
if (!isActive || !canEdit || processBuilderBodies.isPending) return;
|
|
1100
|
+
const candidate = databaseNextBuilderHydrationSource(builderSources, {
|
|
1101
|
+
attemptedKeys: autoPumpBuilderBodiesRef.current,
|
|
1102
|
+
errorKeys: new Set(builderHydrationClientErrorKeys),
|
|
1103
|
+
});
|
|
1104
|
+
if (!candidate) return;
|
|
1105
|
+
const hydrationKey = builderBodyHydrationPumpKey(candidate);
|
|
1106
|
+
if (!hydrationKey) return;
|
|
1107
|
+
autoPumpBuilderBodiesRef.current.add(hydrationKey);
|
|
846
1108
|
if (
|
|
847
|
-
!
|
|
848
|
-
!canEdit ||
|
|
849
|
-
!source ||
|
|
850
|
-
!shouldPumpBuilderBodyHydration(
|
|
851
|
-
source,
|
|
852
|
-
processBuilderBodies.isPending,
|
|
853
|
-
builderHydrationClientErrorKey,
|
|
854
|
-
)
|
|
855
|
-
) {
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
const hydrationKey = builderBodyHydrationPumpKey(source);
|
|
859
|
-
if (!hydrationKey || autoPumpBuilderBodiesRef.current === hydrationKey) {
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
autoPumpBuilderBodiesRef.current = hydrationKey;
|
|
863
|
-
processBuilderBodies.mutate(
|
|
864
|
-
{ sourceId: source.id },
|
|
865
|
-
{
|
|
1109
|
+
!runBuilderHydration(candidate.id, {
|
|
866
1110
|
onSuccess: (result) => {
|
|
867
1111
|
if (!builderBodyHydrationMutationMadeProgress(result)) {
|
|
868
|
-
|
|
1112
|
+
setBuilderHydrationClientErrorKeys((current) =>
|
|
1113
|
+
addUniqueKey(current, hydrationKey),
|
|
1114
|
+
);
|
|
869
1115
|
}
|
|
870
1116
|
},
|
|
871
|
-
onError: () =>
|
|
872
|
-
|
|
873
|
-
|
|
1117
|
+
onError: () =>
|
|
1118
|
+
setBuilderHydrationClientErrorKeys((current) =>
|
|
1119
|
+
addUniqueKey(current, hydrationKey),
|
|
1120
|
+
),
|
|
1121
|
+
})
|
|
1122
|
+
) {
|
|
1123
|
+
autoPumpBuilderBodiesRef.current.delete(hydrationKey);
|
|
1124
|
+
}
|
|
874
1125
|
}, [
|
|
875
|
-
|
|
1126
|
+
builderHydrationClientErrorKeys,
|
|
1127
|
+
builderSources,
|
|
876
1128
|
canEdit,
|
|
877
1129
|
isActive,
|
|
878
1130
|
processBuilderBodies.isPending,
|
|
879
|
-
|
|
880
|
-
source,
|
|
1131
|
+
runBuilderHydration,
|
|
881
1132
|
]);
|
|
882
1133
|
|
|
883
1134
|
useEffect(() => {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
source.sourceType !== "builder-cms" ||
|
|
893
|
-
sourceAddsDetails(source) ||
|
|
894
|
-
refreshSource.isPending ||
|
|
895
|
-
builderSourceRowFetchStatus(source) !== "fetching" ||
|
|
896
|
-
source.metadata.lastReadHasMore !== true ||
|
|
897
|
-
!continuationKey ||
|
|
898
|
-
builderContinuationClientErrorKey === continuationKey
|
|
899
|
-
) {
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (builderContinuationWatchdogRef.current.key !== continuationKey) {
|
|
903
|
-
builderContinuationWatchdogRef.current = {
|
|
904
|
-
key: continuationKey,
|
|
905
|
-
refires: 0,
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
const refireDelay = builderSourceContinuationWatchdogDelay(
|
|
909
|
-
builderContinuationWatchdogRef.current.refires,
|
|
1135
|
+
if (!isActive || !canEdit || refreshSource.isPending) return;
|
|
1136
|
+
const candidate = databaseNextBuilderContinuationWatchdogSource(
|
|
1137
|
+
builderSources,
|
|
1138
|
+
{
|
|
1139
|
+
attemptedKeys: autoContinueBuilderSourceRef.current,
|
|
1140
|
+
errorKeys: new Set(builderContinuationClientErrorKeys),
|
|
1141
|
+
refiresByKey: builderContinuationWatchdogRef.current,
|
|
1142
|
+
},
|
|
910
1143
|
);
|
|
1144
|
+
if (!candidate) return;
|
|
1145
|
+
const continuationKey = builderSourceContinuationKey(candidate);
|
|
1146
|
+
if (!continuationKey) return;
|
|
1147
|
+
const refires =
|
|
1148
|
+
builderContinuationWatchdogRef.current.get(continuationKey) ?? 0;
|
|
911
1149
|
const timer = window.setTimeout(() => {
|
|
912
|
-
const watchdog = builderContinuationWatchdogRef.current;
|
|
913
|
-
if (watchdog.key !== continuationKey) return;
|
|
914
1150
|
if (
|
|
915
|
-
builderSourceContinuationWatchdogDecision(
|
|
916
|
-
|
|
1151
|
+
builderSourceContinuationWatchdogDecision(refires) !== "refire" ||
|
|
1152
|
+
refreshSource.isPending
|
|
1153
|
+
) {
|
|
917
1154
|
return;
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1155
|
+
}
|
|
1156
|
+
if (
|
|
1157
|
+
runSourceRefresh(candidate.id, () =>
|
|
1158
|
+
setBuilderContinuationClientErrorKeys((current) =>
|
|
1159
|
+
addUniqueKey(current, continuationKey),
|
|
1160
|
+
),
|
|
1161
|
+
)
|
|
1162
|
+
) {
|
|
1163
|
+
builderContinuationWatchdogRef.current.set(
|
|
1164
|
+
continuationKey,
|
|
1165
|
+
refires + 1,
|
|
1166
|
+
);
|
|
1167
|
+
databaseRecordBuilderContinuationAttempt(
|
|
1168
|
+
autoContinueBuilderSourceRef.current,
|
|
1169
|
+
continuationKey,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}, builderSourceContinuationWatchdogDelay(refires));
|
|
933
1173
|
return () => window.clearTimeout(timer);
|
|
934
1174
|
}, [
|
|
935
|
-
|
|
1175
|
+
builderContinuationClientErrorKeys,
|
|
1176
|
+
builderSources,
|
|
936
1177
|
canEdit,
|
|
937
|
-
document.id,
|
|
938
1178
|
isActive,
|
|
939
|
-
refreshSource.mutate,
|
|
940
1179
|
refreshSource.isPending,
|
|
941
|
-
|
|
1180
|
+
runSourceRefresh,
|
|
942
1181
|
]);
|
|
943
1182
|
|
|
944
1183
|
useEffect(() => {
|
|
@@ -1043,19 +1282,20 @@ function DatabaseTable({
|
|
|
1043
1282
|
}
|
|
1044
1283
|
|
|
1045
1284
|
function prioritizeBuilderBodyHydrationForItem(item: ContentDatabaseItem) {
|
|
1046
|
-
const
|
|
1047
|
-
if (!
|
|
1048
|
-
const
|
|
1049
|
-
sources.find((candidate) => candidate.id === sourceId) ?? source;
|
|
1050
|
-
if (builderSource?.sourceType !== "builder-cms") return;
|
|
1285
|
+
const builderSource = databaseBuilderHydrationSourceForItem(item, sources);
|
|
1286
|
+
if (!builderSource) return;
|
|
1287
|
+
const sourceId = builderSource.id;
|
|
1051
1288
|
const hydration =
|
|
1052
1289
|
item.bodyHydration ?? item.document.databaseMembership?.bodyHydration;
|
|
1053
1290
|
if (hydration && !databaseItemBodyHydrationIsPending(item)) return;
|
|
1054
|
-
|
|
1291
|
+
runBuilderHydration(
|
|
1055
1292
|
sourceId,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1293
|
+
{},
|
|
1294
|
+
{
|
|
1295
|
+
documentId: item.document.id,
|
|
1296
|
+
limit: 1,
|
|
1297
|
+
},
|
|
1298
|
+
);
|
|
1059
1299
|
}
|
|
1060
1300
|
|
|
1061
1301
|
async function createRow(
|
|
@@ -1086,14 +1326,21 @@ function DatabaseTable({
|
|
|
1086
1326
|
});
|
|
1087
1327
|
return null;
|
|
1088
1328
|
}
|
|
1089
|
-
const createdItem = response
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1329
|
+
const createdItem = databaseCreatedItemForImmediatePreview(response, {
|
|
1330
|
+
databaseId,
|
|
1331
|
+
parentDocument: document,
|
|
1332
|
+
title,
|
|
1333
|
+
propertyValues,
|
|
1334
|
+
});
|
|
1335
|
+
const needsPreview =
|
|
1336
|
+
!!createdItem &&
|
|
1337
|
+
databaseCreatedItemNeedsPreview(items, createdItem, options);
|
|
1338
|
+
if (createdItem && needsPreview) {
|
|
1339
|
+
setCreatedPreviewItem(createdItem);
|
|
1093
1340
|
setPreviewDocumentId(createdItem.document.id);
|
|
1094
1341
|
setPreviewTitleFocusDocumentId(createdItem.document.id);
|
|
1095
1342
|
}
|
|
1096
|
-
if (createdItem && options.focusInlineTitle) {
|
|
1343
|
+
if (createdItem && options.focusInlineTitle && !needsPreview) {
|
|
1097
1344
|
setInlineTitleFocusDocumentId(createdItem.document.id);
|
|
1098
1345
|
}
|
|
1099
1346
|
return createdItem ?? null;
|
|
@@ -1368,73 +1615,376 @@ function DatabaseTable({
|
|
|
1368
1615
|
updateActiveView((view) => ({ ...view, hideEmptyGroups }));
|
|
1369
1616
|
}
|
|
1370
1617
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1618
|
+
function openBuilderReview(sourceId: string) {
|
|
1619
|
+
if (
|
|
1620
|
+
prepareBuilderReview.isPending ||
|
|
1621
|
+
executeBuilderExecution.isPending ||
|
|
1622
|
+
cancelPreparedBuilderUpdate.isPending
|
|
1623
|
+
) {
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const session = {
|
|
1627
|
+
sourceId,
|
|
1628
|
+
generation: builderReviewGenerationRef.current + 1,
|
|
1629
|
+
};
|
|
1630
|
+
builderReviewGenerationRef.current = session.generation;
|
|
1631
|
+
builderReviewSessionRef.current = session;
|
|
1632
|
+
setBuilderReviewSourceId(sourceId);
|
|
1374
1633
|
setBuilderReviewResult(null);
|
|
1375
1634
|
setBuilderReviewCheckedAt(null);
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1635
|
+
setBuilderReviewError(null);
|
|
1636
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1637
|
+
setBuilderReviewSelectionRemap(null);
|
|
1638
|
+
setBuilderReviewOpen(true);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function closeBuilderReview(force = false) {
|
|
1642
|
+
if (
|
|
1643
|
+
!force &&
|
|
1644
|
+
(prepareBuilderReview.isPending ||
|
|
1645
|
+
executeBuilderExecution.isPending ||
|
|
1646
|
+
cancelPreparedBuilderUpdate.isPending)
|
|
1647
|
+
) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
builderReviewGenerationRef.current += 1;
|
|
1651
|
+
builderReviewSessionRef.current = null;
|
|
1652
|
+
setBuilderReviewOpen(false);
|
|
1653
|
+
setBuilderReviewSourceId(null);
|
|
1654
|
+
setBuilderReviewResult(null);
|
|
1655
|
+
setBuilderReviewCheckedAt(null);
|
|
1656
|
+
setBuilderReviewError(null);
|
|
1657
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1658
|
+
setBuilderReviewSelectionRemap(null);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
async function handleBuilderReviewPush(selection: {
|
|
1662
|
+
changeSetIds: string[];
|
|
1663
|
+
transitions: BuilderReviewPublicationTransitions;
|
|
1664
|
+
}) {
|
|
1665
|
+
const session = builderReviewSessionRef.current;
|
|
1666
|
+
const reviewSource = builderReviewSource;
|
|
1667
|
+
if (
|
|
1668
|
+
!session ||
|
|
1669
|
+
!reviewSource ||
|
|
1670
|
+
session.sourceId !== reviewSource.id ||
|
|
1671
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1672
|
+
builderReviewSessionRef.current,
|
|
1673
|
+
session,
|
|
1674
|
+
)
|
|
1675
|
+
) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (
|
|
1679
|
+
!activeBuilderReview ||
|
|
1680
|
+
!databaseBuilderReviewSelectedChangeSetIds(
|
|
1681
|
+
activeBuilderReview,
|
|
1682
|
+
reviewSource,
|
|
1683
|
+
selection.changeSetIds,
|
|
1684
|
+
!builderReviewResult
|
|
1685
|
+
? completeBuilderReviewPreview?.changeSetIds
|
|
1686
|
+
: undefined,
|
|
1687
|
+
)
|
|
1688
|
+
) {
|
|
1689
|
+
toast.error(dbText("builderUpdateFailed"), {
|
|
1690
|
+
description: dbText("needsAFreshReview"),
|
|
1386
1691
|
});
|
|
1387
|
-
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const selectedChangeSetIds = databaseBuilderReviewSelectedChangeSetIds(
|
|
1695
|
+
activeBuilderReview,
|
|
1696
|
+
reviewSource,
|
|
1697
|
+
selection.changeSetIds,
|
|
1698
|
+
!builderReviewResult
|
|
1699
|
+
? completeBuilderReviewPreview?.changeSetIds
|
|
1700
|
+
: undefined,
|
|
1701
|
+
)!;
|
|
1702
|
+
const selectedChangeSetIdSet = new Set(selectedChangeSetIds);
|
|
1703
|
+
const selectionFingerprint = builderReviewSelectionFingerprint({
|
|
1704
|
+
changeSetIds: selectedChangeSetIds,
|
|
1705
|
+
transitions: selection.transitions,
|
|
1706
|
+
});
|
|
1707
|
+
const shouldExecute = preparedBuilderReviewMatches(
|
|
1708
|
+
preparedBuilderReviewConfirmation,
|
|
1709
|
+
session,
|
|
1710
|
+
selectionFingerprint,
|
|
1711
|
+
);
|
|
1388
1712
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
)
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1713
|
+
if (!shouldExecute) {
|
|
1714
|
+
setBuilderReviewResult(null);
|
|
1715
|
+
setBuilderReviewCheckedAt(null);
|
|
1716
|
+
setBuilderReviewError(null);
|
|
1717
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1718
|
+
try {
|
|
1719
|
+
const prepared = await prepareBuilderReview.mutateAsync({
|
|
1720
|
+
documentId: document.id,
|
|
1721
|
+
sourceId: session.sourceId,
|
|
1722
|
+
changeSetIds: selectedChangeSetIds,
|
|
1723
|
+
pushModeConfirmation:
|
|
1724
|
+
activeBuilderReview.pushMode === "none"
|
|
1725
|
+
? undefined
|
|
1726
|
+
: activeBuilderReview.pushMode,
|
|
1727
|
+
transitions: selection.transitions,
|
|
1728
|
+
});
|
|
1729
|
+
if (
|
|
1730
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1731
|
+
builderReviewSessionRef.current,
|
|
1732
|
+
session,
|
|
1733
|
+
)
|
|
1734
|
+
) {
|
|
1735
|
+
return;
|
|
1406
1736
|
}
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1737
|
+
const preparedSelection = databaseBuilderPreparedReviewSelection(
|
|
1738
|
+
prepared.review,
|
|
1739
|
+
reviewSource,
|
|
1740
|
+
selectedChangeSetIds,
|
|
1741
|
+
prepared.preparedChangeSetMappings,
|
|
1742
|
+
);
|
|
1743
|
+
if (!preparedSelection) {
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
"The prepared Builder review returned changes from another source.",
|
|
1414
1746
|
);
|
|
1415
|
-
if (reviewedChangeSets.length > 0) {
|
|
1416
|
-
nextReview = buildClientBuilderReviewPayload(
|
|
1417
|
-
executedSource,
|
|
1418
|
-
reviewedChangeSets,
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
1747
|
}
|
|
1422
|
-
}
|
|
1423
1748
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1749
|
+
const preparedTransitions = Object.fromEntries(
|
|
1750
|
+
Object.entries(selection.transitions).map(
|
|
1751
|
+
([changeSetId, transition]) => [
|
|
1752
|
+
preparedSelection.changeSetIdMap[changeSetId] ?? changeSetId,
|
|
1753
|
+
transition,
|
|
1754
|
+
],
|
|
1755
|
+
),
|
|
1756
|
+
);
|
|
1757
|
+
const preparedSelectionFingerprint = builderReviewSelectionFingerprint({
|
|
1758
|
+
changeSetIds: preparedSelection.preparedChangeSetIds,
|
|
1759
|
+
transitions: preparedTransitions,
|
|
1760
|
+
});
|
|
1761
|
+
setBuilderReviewSelectionRemap(preparedSelection.changeSetIdMap);
|
|
1762
|
+
setBuilderReviewResult(prepared.review);
|
|
1763
|
+
const executableRows = builderReviewExecutableRows(
|
|
1764
|
+
prepared.review,
|
|
1765
|
+
new Set(preparedSelection.preparedChangeSetIds),
|
|
1766
|
+
);
|
|
1767
|
+
if (
|
|
1768
|
+
prepared.review.liveWritesEnabled &&
|
|
1769
|
+
executableRows.length ===
|
|
1770
|
+
preparedSelection.preparedChangeSetIds.length
|
|
1771
|
+
) {
|
|
1772
|
+
setPreparedBuilderReviewConfirmation({
|
|
1773
|
+
...session,
|
|
1774
|
+
selectionFingerprint: preparedSelectionFingerprint,
|
|
1775
|
+
});
|
|
1776
|
+
toast.info(dbText("ready"), {
|
|
1777
|
+
description: dbText("reviewBuilderUpdate"),
|
|
1778
|
+
});
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
setBuilderReviewCheckedAt(new Date().toISOString());
|
|
1783
|
+
toast.success(dbText("checkedStatus"), {
|
|
1784
|
+
description: prepared.review.result.message,
|
|
1785
|
+
});
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (
|
|
1788
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1789
|
+
builderReviewSessionRef.current,
|
|
1790
|
+
session,
|
|
1791
|
+
)
|
|
1792
|
+
) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const message =
|
|
1796
|
+
error instanceof Error ? error.message : dbText("tryAgain");
|
|
1797
|
+
setBuilderReviewError(message);
|
|
1798
|
+
toast.error(dbText("builderUpdateFailed"), {
|
|
1799
|
+
description: message,
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const preparedReview = builderReviewResult;
|
|
1806
|
+
if (
|
|
1807
|
+
!preparedReview ||
|
|
1808
|
+
!databaseBuilderReviewExactSelectionIsValid(
|
|
1809
|
+
preparedReview,
|
|
1810
|
+
selectedChangeSetIds,
|
|
1811
|
+
)
|
|
1812
|
+
) {
|
|
1813
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1814
|
+
setBuilderReviewError(dbText("needsAFreshReview"));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
setBuilderReviewCheckedAt(null);
|
|
1819
|
+
setBuilderReviewError(null);
|
|
1820
|
+
let preparedReviewForError = activeBuilderReview;
|
|
1821
|
+
try {
|
|
1822
|
+
let nextReview = preparedReview;
|
|
1823
|
+
preparedReviewForError = nextReview;
|
|
1824
|
+
|
|
1825
|
+
const executableRows = builderReviewExecutableRows(
|
|
1826
|
+
nextReview,
|
|
1827
|
+
selectedChangeSetIdSet,
|
|
1828
|
+
);
|
|
1829
|
+
let executedResponse: ContentDatabaseResponse | null = null;
|
|
1830
|
+
for (const row of executableRows) {
|
|
1831
|
+
if (!row.execution?.idempotencyKey) continue;
|
|
1832
|
+
if (
|
|
1833
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1834
|
+
builderReviewSessionRef.current,
|
|
1835
|
+
session,
|
|
1836
|
+
)
|
|
1837
|
+
) {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const transition = selection.transitions[row.changeSetId];
|
|
1841
|
+
executedResponse = await executeBuilderExecution.mutateAsync({
|
|
1842
|
+
documentId: document.id,
|
|
1843
|
+
sourceId: session.sourceId,
|
|
1844
|
+
changeSetId: row.changeSetId,
|
|
1845
|
+
idempotencyKey: row.execution.idempotencyKey,
|
|
1846
|
+
pushModeConfirmation: nextReview.pushMode,
|
|
1847
|
+
publicationTransition: transition?.publicationTransition,
|
|
1848
|
+
confirmUnpublish: transition?.confirmUnpublish,
|
|
1849
|
+
});
|
|
1850
|
+
if (
|
|
1851
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1852
|
+
builderReviewSessionRef.current,
|
|
1853
|
+
session,
|
|
1854
|
+
)
|
|
1855
|
+
) {
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const executedSource = executedResponse
|
|
1860
|
+
? databaseAttachedBuilderSource(
|
|
1861
|
+
databaseAttachedSources(
|
|
1862
|
+
executedResponse.sources,
|
|
1863
|
+
executedResponse.source,
|
|
1864
|
+
),
|
|
1865
|
+
executedResponse.source,
|
|
1866
|
+
{ sourceId: session.sourceId },
|
|
1867
|
+
)
|
|
1868
|
+
: null;
|
|
1869
|
+
if (executedSource) {
|
|
1870
|
+
const reviewedIds = new Set(
|
|
1871
|
+
nextReview.rows.map((row) => row.changeSetId),
|
|
1872
|
+
);
|
|
1873
|
+
const reviewedChangeSets = executedSource.changeSets.filter(
|
|
1874
|
+
(changeSet) => reviewedIds.has(changeSet.id),
|
|
1875
|
+
);
|
|
1876
|
+
if (reviewedChangeSets.length > 0) {
|
|
1877
|
+
nextReview = buildClientBuilderReviewPayload(
|
|
1878
|
+
executedSource,
|
|
1879
|
+
reviewedChangeSets,
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (
|
|
1885
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1886
|
+
builderReviewSessionRef.current,
|
|
1887
|
+
session,
|
|
1888
|
+
)
|
|
1889
|
+
) {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
setBuilderReviewResult(nextReview);
|
|
1893
|
+
setBuilderReviewCheckedAt(new Date().toISOString());
|
|
1894
|
+
setBuilderReviewError(null);
|
|
1895
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1896
|
+
if (nextReview.result.status === "running") {
|
|
1897
|
+
toast.info(dbText("working"), {
|
|
1898
|
+
description: dbText("builderPushAlreadyRunning"),
|
|
1899
|
+
});
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
toast.success(
|
|
1903
|
+
nextReview.result.status === "succeeded"
|
|
1904
|
+
? "Builder update pushed"
|
|
1905
|
+
: "Builder update checked",
|
|
1906
|
+
{
|
|
1907
|
+
description: nextReview.result.message,
|
|
1432
1908
|
},
|
|
1433
1909
|
);
|
|
1434
1910
|
} catch (error) {
|
|
1911
|
+
if (
|
|
1912
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1913
|
+
builderReviewSessionRef.current,
|
|
1914
|
+
session,
|
|
1915
|
+
)
|
|
1916
|
+
) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const message =
|
|
1920
|
+
error instanceof Error ? error.message : dbText("tryAgain");
|
|
1921
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
1922
|
+
const ambiguousReview = databaseBuilderReviewAfterExecutionError(
|
|
1923
|
+
preparedReviewForError,
|
|
1924
|
+
message,
|
|
1925
|
+
);
|
|
1926
|
+
if (ambiguousReview) {
|
|
1927
|
+
setBuilderReviewResult(ambiguousReview);
|
|
1928
|
+
setBuilderReviewCheckedAt(new Date().toISOString());
|
|
1929
|
+
if (ambiguousReview.result.status === "running") {
|
|
1930
|
+
setBuilderReviewError(null);
|
|
1931
|
+
toast.info(dbText("working"), {
|
|
1932
|
+
description: ambiguousReview.result.message,
|
|
1933
|
+
});
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
setBuilderReviewError(message);
|
|
1435
1938
|
toast.error(dbText("builderUpdateFailed"), {
|
|
1436
|
-
description:
|
|
1437
|
-
|
|
1939
|
+
description: message,
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async function handleCancelPreparedBuilderUpdate(changeSetId: string) {
|
|
1945
|
+
const session = builderReviewSessionRef.current;
|
|
1946
|
+
if (
|
|
1947
|
+
!session ||
|
|
1948
|
+
!builderReviewSource ||
|
|
1949
|
+
session.sourceId !== builderReviewSource.id
|
|
1950
|
+
) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
setBuilderReviewError(null);
|
|
1954
|
+
try {
|
|
1955
|
+
const result = await cancelPreparedBuilderUpdate.mutateAsync({
|
|
1956
|
+
documentId: document.id,
|
|
1957
|
+
sourceId: session.sourceId,
|
|
1958
|
+
changeSetId,
|
|
1959
|
+
});
|
|
1960
|
+
if (
|
|
1961
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1962
|
+
builderReviewSessionRef.current,
|
|
1963
|
+
session,
|
|
1964
|
+
)
|
|
1965
|
+
) {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
toast.success(
|
|
1969
|
+
result.cancellation.status === "already_cancelled"
|
|
1970
|
+
? dbText("preparedUpdateAlreadyCancelled")
|
|
1971
|
+
: dbText("preparedUpdateCancelled"),
|
|
1972
|
+
);
|
|
1973
|
+
closeBuilderReview(true);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
if (
|
|
1976
|
+
!databaseBuilderReviewSessionIsCurrent(
|
|
1977
|
+
builderReviewSessionRef.current,
|
|
1978
|
+
session,
|
|
1979
|
+
)
|
|
1980
|
+
) {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const message =
|
|
1984
|
+
error instanceof Error ? error.message : dbText("tryAgain");
|
|
1985
|
+
setBuilderReviewError(message);
|
|
1986
|
+
toast.error(dbText("cancelPreparedUpdateFailed"), {
|
|
1987
|
+
description: message,
|
|
1438
1988
|
});
|
|
1439
1989
|
}
|
|
1440
1990
|
}
|
|
@@ -1578,6 +2128,19 @@ function DatabaseTable({
|
|
|
1578
2128
|
|
|
1579
2129
|
return (
|
|
1580
2130
|
<div className="mt-4 min-w-0 w-full max-w-[calc(100vw-var(--content-sidebar-width,0px)-1.5rem)]">
|
|
2131
|
+
<DescriptionField
|
|
2132
|
+
description={document.description}
|
|
2133
|
+
canEdit={canEdit}
|
|
2134
|
+
label={t("editor.properties.description")}
|
|
2135
|
+
placeholder={t("editor.properties.addDatabaseDescription")}
|
|
2136
|
+
className="mb-3"
|
|
2137
|
+
onSave={(description) =>
|
|
2138
|
+
updateDatabaseDocument.mutateAsync({
|
|
2139
|
+
id: document.id,
|
|
2140
|
+
description,
|
|
2141
|
+
})
|
|
2142
|
+
}
|
|
2143
|
+
/>
|
|
1581
2144
|
<div className="mb-1 flex min-h-8 flex-wrap items-center justify-between gap-x-3 gap-y-1 pb-1">
|
|
1582
2145
|
<DatabaseViewTabs
|
|
1583
2146
|
viewConfig={viewConfig}
|
|
@@ -1793,41 +2356,40 @@ function DatabaseTable({
|
|
|
1793
2356
|
onSaveForEveryone={() => void savePersonalQueryForEveryone()}
|
|
1794
2357
|
/>
|
|
1795
2358
|
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
/>
|
|
2359
|
+
{builderSources.map((builderSource) => {
|
|
2360
|
+
const continuationKey = builderSourceContinuationKey(builderSource);
|
|
2361
|
+
return (
|
|
2362
|
+
<BuilderSourceContinuationBar
|
|
2363
|
+
key={builderSource.id}
|
|
2364
|
+
source={builderSource}
|
|
2365
|
+
canEdit={canEdit}
|
|
2366
|
+
pending={
|
|
2367
|
+
sourcePendingOperations.refreshSourceId === builderSource.id
|
|
2368
|
+
}
|
|
2369
|
+
clientError={
|
|
2370
|
+
!!continuationKey &&
|
|
2371
|
+
builderContinuationClientErrorKeys.includes(continuationKey)
|
|
2372
|
+
}
|
|
2373
|
+
progressHighWater={builderProgressHighWater[builderSource.id]}
|
|
2374
|
+
onRetry={() => {
|
|
2375
|
+
if (!continuationKey) return;
|
|
2376
|
+
setBuilderContinuationClientErrorKeys((current) =>
|
|
2377
|
+
current.filter((key) => key !== continuationKey),
|
|
2378
|
+
);
|
|
2379
|
+
databaseRecordBuilderContinuationAttempt(
|
|
2380
|
+
autoContinueBuilderSourceRef.current,
|
|
2381
|
+
continuationKey,
|
|
2382
|
+
);
|
|
2383
|
+
builderContinuationWatchdogRef.current.set(continuationKey, 0);
|
|
2384
|
+
runSourceRefresh(builderSource.id, () =>
|
|
2385
|
+
setBuilderContinuationClientErrorKeys((current) =>
|
|
2386
|
+
addUniqueKey(current, continuationKey),
|
|
2387
|
+
),
|
|
2388
|
+
);
|
|
2389
|
+
}}
|
|
2390
|
+
/>
|
|
2391
|
+
);
|
|
2392
|
+
})}
|
|
1831
2393
|
|
|
1832
2394
|
{activeView.type === "form" ? (
|
|
1833
2395
|
<DatabaseFormView
|
|
@@ -1846,7 +2408,7 @@ function DatabaseTable({
|
|
|
1846
2408
|
groupProperty={boardGroupProperty}
|
|
1847
2409
|
databaseDocumentId={document.id}
|
|
1848
2410
|
canEdit={canEdit}
|
|
1849
|
-
isLoading={
|
|
2411
|
+
isLoading={isDatabaseViewLoading}
|
|
1850
2412
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1851
2413
|
hasActiveConstraints={!!searchQuery || activeFilters.length > 0}
|
|
1852
2414
|
isMoving={setProperty.isPending}
|
|
@@ -1874,7 +2436,7 @@ function DatabaseTable({
|
|
|
1874
2436
|
items={visibleItems}
|
|
1875
2437
|
databaseDocumentId={document.id}
|
|
1876
2438
|
canEdit={canEdit}
|
|
1877
|
-
isLoading={
|
|
2439
|
+
isLoading={isDatabaseViewLoading}
|
|
1878
2440
|
isCreating={addItem.isPending}
|
|
1879
2441
|
activeFilters={activeFilters}
|
|
1880
2442
|
hasSearch={!!searchQuery}
|
|
@@ -1897,7 +2459,7 @@ function DatabaseTable({
|
|
|
1897
2459
|
items={visibleItems}
|
|
1898
2460
|
databaseDocumentId={document.id}
|
|
1899
2461
|
canEdit={canEdit}
|
|
1900
|
-
isLoading={
|
|
2462
|
+
isLoading={isDatabaseViewLoading}
|
|
1901
2463
|
isCreating={addItem.isPending}
|
|
1902
2464
|
activeFilters={activeFilters}
|
|
1903
2465
|
hasSearch={!!searchQuery}
|
|
@@ -1920,7 +2482,7 @@ function DatabaseTable({
|
|
|
1920
2482
|
items={visibleItems}
|
|
1921
2483
|
databaseDocumentId={document.id}
|
|
1922
2484
|
canEdit={canEdit}
|
|
1923
|
-
isLoading={
|
|
2485
|
+
isLoading={isDatabaseViewLoading}
|
|
1924
2486
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1925
2487
|
activeFilters={activeFilters}
|
|
1926
2488
|
hasSearch={!!searchQuery}
|
|
@@ -1946,7 +2508,7 @@ function DatabaseTable({
|
|
|
1946
2508
|
items={visibleItems}
|
|
1947
2509
|
databaseDocumentId={document.id}
|
|
1948
2510
|
canEdit={canEdit}
|
|
1949
|
-
isLoading={
|
|
2511
|
+
isLoading={isDatabaseViewLoading}
|
|
1950
2512
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1951
2513
|
activeFilters={activeFilters}
|
|
1952
2514
|
hasSearch={!!searchQuery}
|
|
@@ -1980,7 +2542,7 @@ function DatabaseTable({
|
|
|
1980
2542
|
sources={sources}
|
|
1981
2543
|
databaseDocumentId={document.id}
|
|
1982
2544
|
canEdit={canEdit}
|
|
1983
|
-
isLoading={
|
|
2545
|
+
isLoading={isDatabaseViewLoading}
|
|
1984
2546
|
isCreating={addItem.isPending}
|
|
1985
2547
|
columnWidths={columnWidths}
|
|
1986
2548
|
sorts={sorts}
|
|
@@ -2027,7 +2589,7 @@ function DatabaseTable({
|
|
|
2027
2589
|
/>
|
|
2028
2590
|
)}
|
|
2029
2591
|
|
|
2030
|
-
{hasMoreItems ? (
|
|
2592
|
+
{hasMoreItems && !isSearchExpansionPending ? (
|
|
2031
2593
|
<div className="flex items-center justify-center border-t border-border/45 py-3">
|
|
2032
2594
|
<Button
|
|
2033
2595
|
type="button"
|
|
@@ -2063,6 +2625,7 @@ function DatabaseTable({
|
|
|
2063
2625
|
if (!open) {
|
|
2064
2626
|
setPreviewDocumentId(null);
|
|
2065
2627
|
setPreviewTitleFocusDocumentId(null);
|
|
2628
|
+
setCreatedPreviewItem(null);
|
|
2066
2629
|
}
|
|
2067
2630
|
}}
|
|
2068
2631
|
onPreviewItem={(item) => {
|
|
@@ -2080,6 +2643,7 @@ function DatabaseTable({
|
|
|
2080
2643
|
panel={settingsPanel}
|
|
2081
2644
|
documentId={document.id}
|
|
2082
2645
|
canEdit={canEdit}
|
|
2646
|
+
canManage={document.canManage === true}
|
|
2083
2647
|
activeView={activeView}
|
|
2084
2648
|
properties={orderedProperties}
|
|
2085
2649
|
items={items}
|
|
@@ -2125,15 +2689,11 @@ function DatabaseTable({
|
|
|
2125
2689
|
onDisconnectSecondary={(sourceId) =>
|
|
2126
2690
|
disconnectSource.mutate({ documentId: document.id, sourceId })
|
|
2127
2691
|
}
|
|
2128
|
-
onRefreshSource={(sourceId) =>
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
onHydrateBuilderBodies={(sourceId) =>
|
|
2135
|
-
processBuilderBodies.mutate({ sourceId })
|
|
2136
|
-
}
|
|
2692
|
+
onRefreshSource={(sourceId) => {
|
|
2693
|
+
const targetSourceId = sourceId ?? source?.id;
|
|
2694
|
+
if (targetSourceId) runSourceRefresh(targetSourceId);
|
|
2695
|
+
}}
|
|
2696
|
+
onHydrateBuilderBodies={(sourceId) => runBuilderHydration(sourceId)}
|
|
2137
2697
|
onDisconnectSource={(sourceId) =>
|
|
2138
2698
|
disconnectSource.mutate(
|
|
2139
2699
|
{
|
|
@@ -2161,30 +2721,29 @@ function DatabaseTable({
|
|
|
2161
2721
|
},
|
|
2162
2722
|
)
|
|
2163
2723
|
}
|
|
2164
|
-
onReviewBuilderUpdate={() => {
|
|
2165
|
-
|
|
2166
|
-
setBuilderReviewCheckedAt(null);
|
|
2167
|
-
setBuilderReviewOpen(true);
|
|
2724
|
+
onReviewBuilderUpdate={(sourceId) => {
|
|
2725
|
+
openBuilderReview(sourceId);
|
|
2168
2726
|
}}
|
|
2169
|
-
onSetBuilderLiveWrites={(
|
|
2727
|
+
onSetBuilderLiveWrites={(settings) =>
|
|
2170
2728
|
setSourceWriteMode.mutate(
|
|
2171
2729
|
{
|
|
2172
2730
|
documentId: document.id,
|
|
2173
|
-
|
|
2174
|
-
|
|
2731
|
+
sourceId: settings.sourceId,
|
|
2732
|
+
writeMode: settings.writeMode,
|
|
2733
|
+
allowPublicationTransitions:
|
|
2734
|
+
settings.writeMode === "publish_updates" &&
|
|
2735
|
+
settings.allowPublicationTransitions === true,
|
|
2175
2736
|
},
|
|
2176
2737
|
{
|
|
2177
2738
|
onSuccess: () => {
|
|
2178
|
-
toast.success(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
},
|
|
2187
|
-
);
|
|
2739
|
+
toast.success(dbText("builderWriteModeUpdated"), {
|
|
2740
|
+
description:
|
|
2741
|
+
settings.writeMode === "publish_updates"
|
|
2742
|
+
? "Approved updates can write through to Builder while preserving publication state."
|
|
2743
|
+
: settings.writeMode === "stage_only"
|
|
2744
|
+
? "Approved updates will stage Builder autosave revisions."
|
|
2745
|
+
: "Builder writes are disabled for this source.",
|
|
2746
|
+
});
|
|
2188
2747
|
},
|
|
2189
2748
|
onError: (error) => {
|
|
2190
2749
|
toast.error(dbText("builderWriteModeWasNotChanged"), {
|
|
@@ -2203,8 +2762,10 @@ function DatabaseTable({
|
|
|
2203
2762
|
disconnectSource.isPending ||
|
|
2204
2763
|
prepareBuilderReview.isPending ||
|
|
2205
2764
|
executeBuilderExecution.isPending ||
|
|
2765
|
+
cancelPreparedBuilderUpdate.isPending ||
|
|
2206
2766
|
setSourceWriteMode.isPending
|
|
2207
2767
|
}
|
|
2768
|
+
sourcePendingOperations={sourcePendingOperations}
|
|
2208
2769
|
onViewTypeChange={(type) =>
|
|
2209
2770
|
setViewConfig(updateDatabaseViewType(viewConfig, activeView.id, type))
|
|
2210
2771
|
}
|
|
@@ -2225,14 +2786,36 @@ function DatabaseTable({
|
|
|
2225
2786
|
<BuilderSourceReviewDialog
|
|
2226
2787
|
open={builderReviewOpen}
|
|
2227
2788
|
review={activeBuilderReview}
|
|
2228
|
-
source={
|
|
2789
|
+
source={builderReviewSource}
|
|
2229
2790
|
canEdit={canEdit}
|
|
2230
2791
|
pending={
|
|
2231
|
-
|
|
2792
|
+
builderReviewPreviewQuery.isFetching ||
|
|
2793
|
+
prepareBuilderReview.isPending ||
|
|
2794
|
+
executeBuilderExecution.isPending ||
|
|
2795
|
+
cancelPreparedBuilderUpdate.isPending
|
|
2796
|
+
}
|
|
2797
|
+
error={
|
|
2798
|
+
builderReviewError ??
|
|
2799
|
+
(builderReviewPreviewQuery.error instanceof Error
|
|
2800
|
+
? builderReviewPreviewQuery.error.message
|
|
2801
|
+
: null)
|
|
2232
2802
|
}
|
|
2233
2803
|
checkedAt={builderReviewCheckedAt}
|
|
2234
|
-
|
|
2235
|
-
|
|
2804
|
+
preparedForExecution={preparedBuilderReviewConfirmation !== null}
|
|
2805
|
+
autoSelectReviewRows={selectedItemIds.length > 0}
|
|
2806
|
+
selectionChangeSetIdMap={builderReviewSelectionRemap}
|
|
2807
|
+
onClose={closeBuilderReview}
|
|
2808
|
+
onValidate={(selection) => void handleBuilderReviewPush(selection)}
|
|
2809
|
+
onCancelPrepared={(changeSetId) =>
|
|
2810
|
+
void handleCancelPreparedBuilderUpdate(changeSetId)
|
|
2811
|
+
}
|
|
2812
|
+
onSelectionChange={() => {
|
|
2813
|
+
setBuilderReviewResult(null);
|
|
2814
|
+
setBuilderReviewCheckedAt(null);
|
|
2815
|
+
setBuilderReviewError(null);
|
|
2816
|
+
setPreparedBuilderReviewConfirmation(null);
|
|
2817
|
+
setBuilderReviewSelectionRemap(null);
|
|
2818
|
+
}}
|
|
2236
2819
|
/>
|
|
2237
2820
|
|
|
2238
2821
|
{!isDatabaseInitialLoading ? (
|
|
@@ -2524,6 +3107,518 @@ export function databaseBuilderBulkUpdateSource(
|
|
|
2524
3107
|
);
|
|
2525
3108
|
}
|
|
2526
3109
|
|
|
3110
|
+
export function databaseAttachedSources(
|
|
3111
|
+
sources: ContentDatabaseSource[] | undefined,
|
|
3112
|
+
primarySource: ContentDatabaseSource | null,
|
|
3113
|
+
) {
|
|
3114
|
+
const attached = sources ? [...sources] : [];
|
|
3115
|
+
if (
|
|
3116
|
+
primarySource &&
|
|
3117
|
+
!attached.some((source) => source.id === primarySource.id)
|
|
3118
|
+
) {
|
|
3119
|
+
attached.unshift(primarySource);
|
|
3120
|
+
}
|
|
3121
|
+
return attached;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
export function databaseAttachedBuilderSources(
|
|
3125
|
+
sources: ContentDatabaseSource[],
|
|
3126
|
+
primarySource: ContentDatabaseSource | null,
|
|
3127
|
+
) {
|
|
3128
|
+
return databaseAttachedSources(sources, primarySource).filter(
|
|
3129
|
+
(source) => source.sourceType === "builder-cms",
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
export function databaseAttachedBuilderModelNames(
|
|
3134
|
+
sources: ContentDatabaseSource[],
|
|
3135
|
+
primarySource: ContentDatabaseSource | null,
|
|
3136
|
+
) {
|
|
3137
|
+
return Array.from(
|
|
3138
|
+
new Set(
|
|
3139
|
+
databaseAttachedBuilderSources(sources, primarySource).map(
|
|
3140
|
+
(source) => source.sourceTable,
|
|
3141
|
+
),
|
|
3142
|
+
),
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
export function databaseAttachedBuilderSource(
|
|
3147
|
+
sources: ContentDatabaseSource[],
|
|
3148
|
+
primarySource: ContentDatabaseSource | null,
|
|
3149
|
+
selection: { sourceId?: string | null; modelName?: string | null },
|
|
3150
|
+
) {
|
|
3151
|
+
const builderSources = databaseAttachedBuilderSources(sources, primarySource);
|
|
3152
|
+
if (selection.sourceId) {
|
|
3153
|
+
return (
|
|
3154
|
+
builderSources.find((source) => source.id === selection.sourceId) ?? null
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
if (selection.modelName) {
|
|
3158
|
+
return (
|
|
3159
|
+
builderSources.find(
|
|
3160
|
+
(source) => source.sourceTable === selection.modelName,
|
|
3161
|
+
) ?? null
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
return null;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
export function databaseBuilderReviewSource(
|
|
3168
|
+
sources: ContentDatabaseSource[],
|
|
3169
|
+
primarySource: ContentDatabaseSource | null,
|
|
3170
|
+
sourceId: string | null,
|
|
3171
|
+
) {
|
|
3172
|
+
return sourceId
|
|
3173
|
+
? databaseAttachedBuilderSource(sources, primarySource, { sourceId })
|
|
3174
|
+
: null;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
export type BuilderReviewSession = {
|
|
3178
|
+
sourceId: string;
|
|
3179
|
+
generation: number;
|
|
3180
|
+
};
|
|
3181
|
+
|
|
3182
|
+
export type PreparedBuilderReviewConfirmation = BuilderReviewSession & {
|
|
3183
|
+
selectionFingerprint: string;
|
|
3184
|
+
};
|
|
3185
|
+
|
|
3186
|
+
export type PreparedBuilderChangeSetMapping = {
|
|
3187
|
+
requestedChangeSetId: string;
|
|
3188
|
+
preparedChangeSetId: string;
|
|
3189
|
+
};
|
|
3190
|
+
|
|
3191
|
+
export function builderReviewSelectionFingerprint(selection: {
|
|
3192
|
+
changeSetIds: string[];
|
|
3193
|
+
transitions: BuilderReviewPublicationTransitions;
|
|
3194
|
+
}) {
|
|
3195
|
+
return JSON.stringify({
|
|
3196
|
+
changeSetIds: [...selection.changeSetIds].sort(),
|
|
3197
|
+
transitions: Object.entries(selection.transitions)
|
|
3198
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
3199
|
+
.map(([changeSetId, transition]) => [changeSetId, transition]),
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
export function preparedBuilderReviewMatches(
|
|
3204
|
+
confirmation: PreparedBuilderReviewConfirmation | null,
|
|
3205
|
+
session: BuilderReviewSession,
|
|
3206
|
+
selectionFingerprint: string,
|
|
3207
|
+
) {
|
|
3208
|
+
return (
|
|
3209
|
+
confirmation?.sourceId === session.sourceId &&
|
|
3210
|
+
confirmation.generation === session.generation &&
|
|
3211
|
+
confirmation.selectionFingerprint === selectionFingerprint
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
export function databaseBuilderReviewSessionIsCurrent(
|
|
3216
|
+
current: BuilderReviewSession | null,
|
|
3217
|
+
captured: BuilderReviewSession,
|
|
3218
|
+
) {
|
|
3219
|
+
return (
|
|
3220
|
+
current?.sourceId === captured.sourceId &&
|
|
3221
|
+
current.generation === captured.generation
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
export function databaseBuilderExecutionRequiresReconciliation(
|
|
3226
|
+
message: string,
|
|
3227
|
+
) {
|
|
3228
|
+
return /reconciliation required|requires reconciliation|remote outcome is unknown|write timed out/i.test(
|
|
3229
|
+
message,
|
|
3230
|
+
);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
export function databaseBuilderReviewAfterExecutionError(
|
|
3234
|
+
review: ContentDatabaseSourceReviewPayload | null,
|
|
3235
|
+
message: string,
|
|
3236
|
+
) {
|
|
3237
|
+
if (!review) return null;
|
|
3238
|
+
if (databaseBuilderExecutionRequiresReconciliation(message)) {
|
|
3239
|
+
return {
|
|
3240
|
+
...review,
|
|
3241
|
+
result: {
|
|
3242
|
+
status: "reconciliation_required" as const,
|
|
3243
|
+
message,
|
|
3244
|
+
},
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
if (/execution is already running/i.test(message)) {
|
|
3248
|
+
return {
|
|
3249
|
+
...review,
|
|
3250
|
+
result: {
|
|
3251
|
+
status: "running" as const,
|
|
3252
|
+
message: dbText("builderPushAlreadyRunning"),
|
|
3253
|
+
},
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
return null;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
export function databaseBuilderReviewBelongsToSource(
|
|
3260
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3261
|
+
source: ContentDatabaseSource,
|
|
3262
|
+
authoritativeChangeSetIds?: string[],
|
|
3263
|
+
) {
|
|
3264
|
+
const sourceChangeSetIds = new Set(
|
|
3265
|
+
authoritativeChangeSetIds ??
|
|
3266
|
+
source.changeSets.map((changeSet) => changeSet.id),
|
|
3267
|
+
);
|
|
3268
|
+
return review.rows.every((row) => sourceChangeSetIds.has(row.changeSetId));
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
export function databaseBuilderReviewSelectionIsValid(
|
|
3272
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3273
|
+
source: ContentDatabaseSource,
|
|
3274
|
+
changeSetIds: string[],
|
|
3275
|
+
requireExactReviewRows = false,
|
|
3276
|
+
authoritativeChangeSetIds?: string[],
|
|
3277
|
+
) {
|
|
3278
|
+
if (changeSetIds.length === 0) return false;
|
|
3279
|
+
const selectedIds = new Set(changeSetIds);
|
|
3280
|
+
if (selectedIds.size !== changeSetIds.length) return false;
|
|
3281
|
+
if (
|
|
3282
|
+
!databaseBuilderReviewBelongsToSource(
|
|
3283
|
+
review,
|
|
3284
|
+
source,
|
|
3285
|
+
authoritativeChangeSetIds,
|
|
3286
|
+
)
|
|
3287
|
+
) {
|
|
3288
|
+
return false;
|
|
3289
|
+
}
|
|
3290
|
+
const reviewIds = new Set(review.rows.map((row) => row.changeSetId));
|
|
3291
|
+
if (reviewIds.size !== review.rows.length) return false;
|
|
3292
|
+
if (!changeSetIds.every((changeSetId) => reviewIds.has(changeSetId))) {
|
|
3293
|
+
return false;
|
|
3294
|
+
}
|
|
3295
|
+
return !requireExactReviewRows || reviewIds.size === selectedIds.size;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
export function databaseBuilderReviewExactSelectionIsValid(
|
|
3299
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3300
|
+
changeSetIds: string[],
|
|
3301
|
+
) {
|
|
3302
|
+
if (changeSetIds.length === 0) return false;
|
|
3303
|
+
const selectedIds = new Set(changeSetIds);
|
|
3304
|
+
const reviewIds = new Set(review.rows.map((row) => row.changeSetId));
|
|
3305
|
+
return (
|
|
3306
|
+
selectedIds.size === changeSetIds.length &&
|
|
3307
|
+
reviewIds.size === review.rows.length &&
|
|
3308
|
+
reviewIds.size === selectedIds.size &&
|
|
3309
|
+
changeSetIds.every((changeSetId) => reviewIds.has(changeSetId))
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
export function databaseBuilderPreparedReviewSelection(
|
|
3314
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3315
|
+
source: ContentDatabaseSource,
|
|
3316
|
+
requestedChangeSetIds: string[],
|
|
3317
|
+
mappings: PreparedBuilderChangeSetMapping[],
|
|
3318
|
+
) {
|
|
3319
|
+
if (review.sourceTable !== source.sourceTable) return null;
|
|
3320
|
+
if (mappings.length !== requestedChangeSetIds.length) return null;
|
|
3321
|
+
const requestedIds = new Set(requestedChangeSetIds);
|
|
3322
|
+
const mappedRequestedIds = new Set(
|
|
3323
|
+
mappings.map((mapping) => mapping.requestedChangeSetId),
|
|
3324
|
+
);
|
|
3325
|
+
const preparedChangeSetIds = mappings.map(
|
|
3326
|
+
(mapping) => mapping.preparedChangeSetId,
|
|
3327
|
+
);
|
|
3328
|
+
if (
|
|
3329
|
+
requestedIds.size !== requestedChangeSetIds.length ||
|
|
3330
|
+
mappedRequestedIds.size !== mappings.length ||
|
|
3331
|
+
!requestedChangeSetIds.every((id) => mappedRequestedIds.has(id)) ||
|
|
3332
|
+
!databaseBuilderReviewExactSelectionIsValid(review, preparedChangeSetIds)
|
|
3333
|
+
) {
|
|
3334
|
+
return null;
|
|
3335
|
+
}
|
|
3336
|
+
return {
|
|
3337
|
+
preparedChangeSetIds,
|
|
3338
|
+
changeSetIdMap: Object.fromEntries(
|
|
3339
|
+
mappings.map((mapping) => [
|
|
3340
|
+
mapping.requestedChangeSetId,
|
|
3341
|
+
mapping.preparedChangeSetId,
|
|
3342
|
+
]),
|
|
3343
|
+
),
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
export function databaseBuilderReviewSelectedChangeSetIds(
|
|
3348
|
+
review: ContentDatabaseSourceReviewPayload,
|
|
3349
|
+
source: ContentDatabaseSource,
|
|
3350
|
+
changeSetIds: string[],
|
|
3351
|
+
authoritativeChangeSetIds?: string[],
|
|
3352
|
+
) {
|
|
3353
|
+
return databaseBuilderReviewSelectionIsValid(
|
|
3354
|
+
review,
|
|
3355
|
+
source,
|
|
3356
|
+
changeSetIds,
|
|
3357
|
+
false,
|
|
3358
|
+
authoritativeChangeSetIds,
|
|
3359
|
+
)
|
|
3360
|
+
? [...changeSetIds]
|
|
3361
|
+
: null;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
export type BuilderSourceProgressHighWater = {
|
|
3365
|
+
fetchedCount: number;
|
|
3366
|
+
hydratedCount: number;
|
|
3367
|
+
rowsComplete: boolean;
|
|
3368
|
+
generation: number;
|
|
3369
|
+
observedFetchedCount: number;
|
|
3370
|
+
observedHydratedCount: number;
|
|
3371
|
+
hydrationTotal: number;
|
|
3372
|
+
lastRefreshedAt: string | null;
|
|
3373
|
+
sourceFetchState: "idle" | "fetching" | "error" | null;
|
|
3374
|
+
};
|
|
3375
|
+
|
|
3376
|
+
export function databaseBuilderProgressHighWater(
|
|
3377
|
+
sources: ContentDatabaseSource[],
|
|
3378
|
+
current: Record<string, BuilderSourceProgressHighWater>,
|
|
3379
|
+
) {
|
|
3380
|
+
const next: Record<string, BuilderSourceProgressHighWater> = {};
|
|
3381
|
+
for (const source of sources) {
|
|
3382
|
+
const rawFetchedCount =
|
|
3383
|
+
typeof source.metadata.lastReadFetchedEntryCount === "number"
|
|
3384
|
+
? source.metadata.lastReadFetchedEntryCount
|
|
3385
|
+
: typeof source.metadata.lastReadEntryCount === "number"
|
|
3386
|
+
? source.metadata.lastReadEntryCount
|
|
3387
|
+
: 0;
|
|
3388
|
+
const fetchedCount =
|
|
3389
|
+
source.metadata.lastReadHasMore === false
|
|
3390
|
+
? Math.max(rawFetchedCount, source.bodyHydration?.total ?? 0)
|
|
3391
|
+
: rawFetchedCount;
|
|
3392
|
+
const previous = current[source.id];
|
|
3393
|
+
const observedHydratedCount = source.bodyHydration?.hydrated ?? 0;
|
|
3394
|
+
const hydrationTotal = source.bodyHydration?.total ?? 0;
|
|
3395
|
+
const sourceFetchState = source.metadata.sourceFetchState ?? null;
|
|
3396
|
+
const lifecycleTimestampChanged =
|
|
3397
|
+
!!previous &&
|
|
3398
|
+
previous.lastRefreshedAt !== (source.lastRefreshedAt ?? null);
|
|
3399
|
+
const fetchRestarted =
|
|
3400
|
+
!!previous &&
|
|
3401
|
+
previous.rowsComplete &&
|
|
3402
|
+
source.metadata.lastReadHasMore !== false &&
|
|
3403
|
+
(sourceFetchState === "fetching" || lifecycleTimestampChanged);
|
|
3404
|
+
const countsRewound =
|
|
3405
|
+
!!previous && rawFetchedCount < previous.observedFetchedCount;
|
|
3406
|
+
const hydrationQueueRestarted =
|
|
3407
|
+
!!previous &&
|
|
3408
|
+
(observedHydratedCount < previous.observedHydratedCount ||
|
|
3409
|
+
hydrationTotal < previous.hydrationTotal) &&
|
|
3410
|
+
(source.bodyHydration?.pending ?? 0) > 0;
|
|
3411
|
+
const resetLifecycle =
|
|
3412
|
+
fetchRestarted || countsRewound || hydrationQueueRestarted;
|
|
3413
|
+
const baseline = resetLifecycle ? undefined : previous;
|
|
3414
|
+
next[source.id] = {
|
|
3415
|
+
fetchedCount: Math.max(baseline?.fetchedCount ?? 0, fetchedCount),
|
|
3416
|
+
hydratedCount: Math.max(
|
|
3417
|
+
baseline?.hydratedCount ?? 0,
|
|
3418
|
+
observedHydratedCount,
|
|
3419
|
+
),
|
|
3420
|
+
rowsComplete:
|
|
3421
|
+
baseline?.rowsComplete === true ||
|
|
3422
|
+
source.metadata.lastReadHasMore === false,
|
|
3423
|
+
generation: (previous?.generation ?? 0) + (resetLifecycle ? 1 : 0),
|
|
3424
|
+
observedFetchedCount: rawFetchedCount,
|
|
3425
|
+
observedHydratedCount,
|
|
3426
|
+
hydrationTotal,
|
|
3427
|
+
lastRefreshedAt: source.lastRefreshedAt ?? null,
|
|
3428
|
+
sourceFetchState,
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
const currentIds = Object.keys(current);
|
|
3432
|
+
const nextIds = Object.keys(next);
|
|
3433
|
+
return currentIds.length === nextIds.length &&
|
|
3434
|
+
nextIds.every(
|
|
3435
|
+
(sourceId) =>
|
|
3436
|
+
current[sourceId]?.fetchedCount === next[sourceId]?.fetchedCount &&
|
|
3437
|
+
current[sourceId]?.hydratedCount === next[sourceId]?.hydratedCount &&
|
|
3438
|
+
current[sourceId]?.rowsComplete === next[sourceId]?.rowsComplete &&
|
|
3439
|
+
current[sourceId]?.generation === next[sourceId]?.generation &&
|
|
3440
|
+
current[sourceId]?.observedFetchedCount ===
|
|
3441
|
+
next[sourceId]?.observedFetchedCount &&
|
|
3442
|
+
current[sourceId]?.observedHydratedCount ===
|
|
3443
|
+
next[sourceId]?.observedHydratedCount &&
|
|
3444
|
+
current[sourceId]?.hydrationTotal === next[sourceId]?.hydrationTotal &&
|
|
3445
|
+
current[sourceId]?.lastRefreshedAt ===
|
|
3446
|
+
next[sourceId]?.lastRefreshedAt &&
|
|
3447
|
+
current[sourceId]?.sourceFetchState ===
|
|
3448
|
+
next[sourceId]?.sourceFetchState,
|
|
3449
|
+
)
|
|
3450
|
+
? current
|
|
3451
|
+
: next;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
type BuilderSourcePumpSelection = {
|
|
3455
|
+
attemptedKeys: ReadonlySet<string>;
|
|
3456
|
+
errorKeys: ReadonlySet<string>;
|
|
3457
|
+
};
|
|
3458
|
+
|
|
3459
|
+
export function databaseRecordBuilderContinuationAttempt(
|
|
3460
|
+
attemptedKeys: Set<string>,
|
|
3461
|
+
continuationKey: string,
|
|
3462
|
+
) {
|
|
3463
|
+
attemptedKeys.add(continuationKey);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
export function databaseNextBuilderContinuationSource(
|
|
3467
|
+
sources: ContentDatabaseSource[],
|
|
3468
|
+
selection: BuilderSourcePumpSelection,
|
|
3469
|
+
) {
|
|
3470
|
+
return (
|
|
3471
|
+
sources.find((source) => {
|
|
3472
|
+
const key = builderSourceContinuationKey(source);
|
|
3473
|
+
return (
|
|
3474
|
+
source.sourceType === "builder-cms" &&
|
|
3475
|
+
!sourceAddsDetails(source) &&
|
|
3476
|
+
builderSourceRowFetchStatus(source) === "fetching" &&
|
|
3477
|
+
source.metadata.lastReadHasMore === true &&
|
|
3478
|
+
!!key &&
|
|
3479
|
+
!selection.attemptedKeys.has(key) &&
|
|
3480
|
+
!selection.errorKeys.has(key)
|
|
3481
|
+
);
|
|
3482
|
+
}) ?? null
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
export function databaseNextBuilderContinuationWatchdogSource(
|
|
3487
|
+
sources: ContentDatabaseSource[],
|
|
3488
|
+
selection: BuilderSourcePumpSelection & {
|
|
3489
|
+
refiresByKey: ReadonlyMap<string, number>;
|
|
3490
|
+
},
|
|
3491
|
+
) {
|
|
3492
|
+
return (
|
|
3493
|
+
sources
|
|
3494
|
+
.filter((source) => {
|
|
3495
|
+
const key = builderSourceContinuationKey(source);
|
|
3496
|
+
return (
|
|
3497
|
+
source.sourceType === "builder-cms" &&
|
|
3498
|
+
!sourceAddsDetails(source) &&
|
|
3499
|
+
builderSourceRowFetchStatus(source) === "fetching" &&
|
|
3500
|
+
source.metadata.lastReadHasMore === true &&
|
|
3501
|
+
!!key &&
|
|
3502
|
+
selection.attemptedKeys.has(key) &&
|
|
3503
|
+
!selection.errorKeys.has(key)
|
|
3504
|
+
);
|
|
3505
|
+
})
|
|
3506
|
+
.sort((left, right) => {
|
|
3507
|
+
const leftKey = builderSourceContinuationKey(left) ?? "";
|
|
3508
|
+
const rightKey = builderSourceContinuationKey(right) ?? "";
|
|
3509
|
+
return (
|
|
3510
|
+
(selection.refiresByKey.get(leftKey) ?? 0) -
|
|
3511
|
+
(selection.refiresByKey.get(rightKey) ?? 0)
|
|
3512
|
+
);
|
|
3513
|
+
})[0] ?? null
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
export function databaseNextBuilderHydrationSource(
|
|
3518
|
+
sources: ContentDatabaseSource[],
|
|
3519
|
+
selection: BuilderSourcePumpSelection,
|
|
3520
|
+
) {
|
|
3521
|
+
return (
|
|
3522
|
+
sources.find((source) => {
|
|
3523
|
+
const key = builderBodyHydrationPumpKey(source);
|
|
3524
|
+
return (
|
|
3525
|
+
!!key &&
|
|
3526
|
+
!selection.attemptedKeys.has(key) &&
|
|
3527
|
+
!selection.errorKeys.has(key) &&
|
|
3528
|
+
shouldPumpBuilderBodyHydration(source, false, null)
|
|
3529
|
+
);
|
|
3530
|
+
}) ?? null
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
export type DatabaseSourcePendingOperation =
|
|
3535
|
+
| "changeRole"
|
|
3536
|
+
| "refresh"
|
|
3537
|
+
| "hydration"
|
|
3538
|
+
| "disconnect"
|
|
3539
|
+
| "review"
|
|
3540
|
+
| "execute"
|
|
3541
|
+
| "writeMode";
|
|
3542
|
+
|
|
3543
|
+
export type DatabaseSourcePendingOperations = {
|
|
3544
|
+
attach: boolean;
|
|
3545
|
+
changeRole: boolean;
|
|
3546
|
+
refresh: boolean;
|
|
3547
|
+
hydration: boolean;
|
|
3548
|
+
disconnect: boolean;
|
|
3549
|
+
review: boolean;
|
|
3550
|
+
execute: boolean;
|
|
3551
|
+
writeMode: boolean;
|
|
3552
|
+
changeRoleSourceId: string | null;
|
|
3553
|
+
refreshSourceId: string | null;
|
|
3554
|
+
hydrationSourceId: string | null;
|
|
3555
|
+
disconnectSourceId: string | null;
|
|
3556
|
+
reviewSourceId: string | null;
|
|
3557
|
+
executeSourceId: string | null;
|
|
3558
|
+
writeModeSourceId: string | null;
|
|
3559
|
+
};
|
|
3560
|
+
|
|
3561
|
+
export function acquireDatabaseSourceOperation(
|
|
3562
|
+
lock: { current: string | null },
|
|
3563
|
+
sourceId: string,
|
|
3564
|
+
) {
|
|
3565
|
+
if (lock.current !== null) return false;
|
|
3566
|
+
lock.current = sourceId;
|
|
3567
|
+
return true;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
export function releaseDatabaseSourceOperation(
|
|
3571
|
+
lock: { current: string | null },
|
|
3572
|
+
sourceId: string,
|
|
3573
|
+
) {
|
|
3574
|
+
if (lock.current === sourceId) lock.current = null;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
export function pendingMutationSourceId(
|
|
3578
|
+
isPending: boolean,
|
|
3579
|
+
variables: { sourceId?: string } | undefined,
|
|
3580
|
+
) {
|
|
3581
|
+
return isPending ? (variables?.sourceId ?? null) : null;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
export function databaseSourceOperationIsPending(
|
|
3585
|
+
pending: DatabaseSourcePendingOperations,
|
|
3586
|
+
sourceId: string,
|
|
3587
|
+
operations: DatabaseSourcePendingOperation[],
|
|
3588
|
+
) {
|
|
3589
|
+
const sourceIds: Record<DatabaseSourcePendingOperation, string | null> = {
|
|
3590
|
+
changeRole: pending.changeRoleSourceId,
|
|
3591
|
+
refresh: pending.refreshSourceId,
|
|
3592
|
+
hydration: pending.hydrationSourceId,
|
|
3593
|
+
disconnect: pending.disconnectSourceId,
|
|
3594
|
+
review: pending.reviewSourceId,
|
|
3595
|
+
execute: pending.executeSourceId,
|
|
3596
|
+
writeMode: pending.writeModeSourceId,
|
|
3597
|
+
};
|
|
3598
|
+
return operations.some((operation) => sourceIds[operation] === sourceId);
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
export function databaseBuilderWriteModeOperationPending(
|
|
3602
|
+
pending: DatabaseSourcePendingOperations,
|
|
3603
|
+
sourceId: string,
|
|
3604
|
+
) {
|
|
3605
|
+
return (
|
|
3606
|
+
pending.writeMode ||
|
|
3607
|
+
databaseSourceOperationIsPending(pending, sourceId, [
|
|
3608
|
+
"changeRole",
|
|
3609
|
+
"refresh",
|
|
3610
|
+
"disconnect",
|
|
3611
|
+
"review",
|
|
3612
|
+
"execute",
|
|
3613
|
+
"writeMode",
|
|
3614
|
+
])
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
function addUniqueKey(current: string[], key: string) {
|
|
3619
|
+
return current.includes(key) ? current : [...current, key];
|
|
3620
|
+
}
|
|
3621
|
+
|
|
2527
3622
|
export function databaseBulkScalarInputState(
|
|
2528
3623
|
type: DocumentPropertyType,
|
|
2529
3624
|
input: string,
|
|
@@ -2782,19 +3877,44 @@ function DatabaseItemPreview({
|
|
|
2782
3877
|
const deleteDocument = useDeleteDocument();
|
|
2783
3878
|
const duplicateItem = useDuplicateDatabaseItem(databaseDocumentId);
|
|
2784
3879
|
const { data: document, isLoading } = useDocument(item.document.id);
|
|
3880
|
+
const { data: persistedPreviewDraft } = usePreviewDocumentDraft(
|
|
3881
|
+
item.document.id,
|
|
3882
|
+
);
|
|
3883
|
+
const updatePreviewDraft = useUpdatePreviewDocumentDraft();
|
|
2785
3884
|
const previewDocument = document ?? item.document;
|
|
2786
3885
|
const previewTitle = databaseItemPreviewTitle(item);
|
|
3886
|
+
const [bodyDraftConflict, setBodyDraftConflict] = useState<{
|
|
3887
|
+
documentId: string;
|
|
3888
|
+
serverPayload: {
|
|
3889
|
+
title: string;
|
|
3890
|
+
content: string;
|
|
3891
|
+
loadedUpdatedAt?: string;
|
|
3892
|
+
loadedContentWasEmpty?: boolean;
|
|
3893
|
+
};
|
|
3894
|
+
} | null>(null);
|
|
3895
|
+
const activeBodyDraftConflict =
|
|
3896
|
+
bodyDraftConflict?.documentId === item.document.id
|
|
3897
|
+
? bodyDraftConflict
|
|
3898
|
+
: null;
|
|
2787
3899
|
const canEdit = document?.canEdit ?? item.document.canEdit ?? true;
|
|
2788
3900
|
const canManage = document?.canManage ?? item.document.canManage ?? false;
|
|
2789
|
-
const
|
|
2790
|
-
item
|
|
2791
|
-
document
|
|
2792
|
-
|
|
3901
|
+
const sourceBackedPreview = Boolean(
|
|
3902
|
+
item.bodyHydration ||
|
|
3903
|
+
item.document.databaseMembership?.sourceId ||
|
|
3904
|
+
document?.databaseMembership?.sourceId,
|
|
3905
|
+
);
|
|
3906
|
+
const bodyHydrationPending =
|
|
3907
|
+
(sourceBackedPreview && (isLoading || !document)) ||
|
|
3908
|
+
previewBodyHydrationIsPending({
|
|
3909
|
+
item,
|
|
3910
|
+
document,
|
|
3911
|
+
});
|
|
2793
3912
|
const bodyHydrationError = previewBodyHydrationIsTerminalError({
|
|
2794
3913
|
item,
|
|
2795
3914
|
document,
|
|
2796
3915
|
});
|
|
2797
|
-
const previewCanEdit =
|
|
3916
|
+
const previewCanEdit =
|
|
3917
|
+
canEdit && !bodyHydrationPending && !activeBodyDraftConflict;
|
|
2798
3918
|
const location = useLocation();
|
|
2799
3919
|
// Seed the displayed title/content from a RETAINED dirty controller's pending
|
|
2800
3920
|
// edit if one exists for this doc (reopen-before-evict), so an unsaved peek
|
|
@@ -2839,63 +3959,249 @@ function DatabaseItemPreview({
|
|
|
2839
3959
|
queryClientRef.current = queryClient;
|
|
2840
3960
|
const bodyHydrationPendingRef = useRef(bodyHydrationPending);
|
|
2841
3961
|
bodyHydrationPendingRef.current = bodyHydrationPending;
|
|
3962
|
+
const draftVersionsRef = useRef<Map<string, number | null>>(new Map());
|
|
3963
|
+
const draftWriteChainsRef = useRef<Map<string, Promise<void>>>(new Map());
|
|
3964
|
+
const draftSaveTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
|
|
3965
|
+
new Map(),
|
|
3966
|
+
);
|
|
3967
|
+
const updatePreviewDraftRef = useRef(updatePreviewDraft);
|
|
3968
|
+
updatePreviewDraftRef.current = updatePreviewDraft;
|
|
2842
3969
|
// Doc ids that have been deleted in this peek's lifetime. A pending save must
|
|
2843
3970
|
// never resurrect a deleted document, so dispatch is suppressed for these.
|
|
2844
3971
|
const deletedIdsRef = useRef<Set<string>>(new Set());
|
|
2845
3972
|
|
|
2846
3973
|
const documentId = item.document.id;
|
|
2847
3974
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3975
|
+
function enqueueDraftWrite(
|
|
3976
|
+
controller: ReturnType<typeof createPreviewDocumentSaveController>,
|
|
3977
|
+
operation: "upsert" | "delete",
|
|
3978
|
+
expectedPayload?: { title: string; content: string },
|
|
3979
|
+
allowCreateRetry = true,
|
|
3980
|
+
) {
|
|
3981
|
+
const snapshot = controller.draftSnapshot();
|
|
3982
|
+
const previous =
|
|
3983
|
+
draftWriteChainsRef.current.get(documentId) ?? Promise.resolve();
|
|
3984
|
+
const next = previous
|
|
3985
|
+
.catch(() => undefined)
|
|
3986
|
+
.then(async () => {
|
|
3987
|
+
const expectedVersion =
|
|
3988
|
+
draftVersionsRef.current.get(documentId) ?? null;
|
|
3989
|
+
if (operation === "delete") {
|
|
3990
|
+
if (expectedVersion === null) return;
|
|
3991
|
+
if (!expectedPayload) return;
|
|
3992
|
+
const result = await updatePreviewDraftRef.current.mutateAsync({
|
|
3993
|
+
operation: "delete",
|
|
3994
|
+
documentId,
|
|
3995
|
+
expectedVersion,
|
|
3996
|
+
expectedTitle: expectedPayload.title,
|
|
3997
|
+
expectedContent: expectedPayload.content,
|
|
3998
|
+
});
|
|
3999
|
+
if (result.status === "deleted")
|
|
4000
|
+
draftVersionsRef.current.set(documentId, null);
|
|
4001
|
+
else if (result.draft) {
|
|
4002
|
+
draftVersionsRef.current.set(documentId, result.draft.version);
|
|
4003
|
+
if (
|
|
4004
|
+
previewDraftNeedsConflict({
|
|
4005
|
+
returnedDraft: result.draft,
|
|
4006
|
+
pending: controller.pending,
|
|
4007
|
+
})
|
|
4008
|
+
) {
|
|
4009
|
+
controller.notifyDraftConflict({
|
|
4010
|
+
lastSaved: snapshot.lastSaved,
|
|
4011
|
+
pending: {
|
|
4012
|
+
title: result.draft.title,
|
|
4013
|
+
content: result.draft.content,
|
|
4014
|
+
loadedUpdatedAt:
|
|
4015
|
+
result.draft.baseDocumentUpdatedAt ?? undefined,
|
|
4016
|
+
loadedContentWasEmpty:
|
|
4017
|
+
result.draft.loadedContentWasEmpty === 1,
|
|
4018
|
+
},
|
|
4019
|
+
deferredReason: "conflict",
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
} else {
|
|
4023
|
+
// Another tab already removed the row. Treat the stale delete as
|
|
4024
|
+
// converged instead of retaining a version that can never match.
|
|
4025
|
+
draftVersionsRef.current.set(documentId, null);
|
|
2873
4026
|
}
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
});
|
|
2889
|
-
void queryClientRef.current.invalidateQueries({
|
|
2890
|
-
queryKey: ["action", "list-documents"],
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
const result = await updatePreviewDraftRef.current.mutateAsync({
|
|
4030
|
+
operation: "upsert",
|
|
4031
|
+
documentId,
|
|
4032
|
+
expectedVersion,
|
|
4033
|
+
draft: {
|
|
4034
|
+
title: snapshot.pending.title,
|
|
4035
|
+
content: snapshot.pending.content,
|
|
4036
|
+
baseDocumentUpdatedAt: snapshot.pending.loadedUpdatedAt ?? null,
|
|
4037
|
+
loadedContentWasEmpty:
|
|
4038
|
+
snapshot.pending.loadedContentWasEmpty === true,
|
|
4039
|
+
deferredReason: snapshot.deferredReason,
|
|
4040
|
+
},
|
|
2891
4041
|
});
|
|
2892
|
-
|
|
2893
|
-
|
|
4042
|
+
if (result.status === "saved" && result.draft) {
|
|
4043
|
+
draftVersionsRef.current.set(documentId, result.draft.version);
|
|
4044
|
+
} else if (result.draft) {
|
|
4045
|
+
draftVersionsRef.current.set(documentId, result.draft.version);
|
|
4046
|
+
if (
|
|
4047
|
+
previewDraftNeedsConflict({
|
|
4048
|
+
returnedDraft: result.draft,
|
|
4049
|
+
pending: controller.pending,
|
|
4050
|
+
})
|
|
4051
|
+
) {
|
|
4052
|
+
controller.notifyDraftConflict({
|
|
4053
|
+
lastSaved: snapshot.lastSaved,
|
|
4054
|
+
pending: {
|
|
4055
|
+
title: result.draft.title,
|
|
4056
|
+
content: result.draft.content,
|
|
4057
|
+
loadedUpdatedAt:
|
|
4058
|
+
result.draft.baseDocumentUpdatedAt ?? undefined,
|
|
4059
|
+
loadedContentWasEmpty: result.draft.loadedContentWasEmpty === 1,
|
|
4060
|
+
},
|
|
4061
|
+
deferredReason: "conflict",
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
} else {
|
|
4065
|
+
// A competing tab deleted the version we tried to update. Reset to
|
|
4066
|
+
// create mode and enqueue the latest pending payload once; keeping
|
|
4067
|
+
// the stale version would make every later upsert conflict forever.
|
|
4068
|
+
draftVersionsRef.current.set(documentId, null);
|
|
4069
|
+
const recovery = previewDraftMissingCasRecovery({
|
|
4070
|
+
operation: "upsert",
|
|
4071
|
+
allowCreateRetry,
|
|
4072
|
+
});
|
|
4073
|
+
if (recovery === "retry-create") {
|
|
4074
|
+
enqueueDraftWrite(controller, "upsert", undefined, false);
|
|
4075
|
+
} else {
|
|
4076
|
+
toast.error(dbText("failedToSavePagePreview"), {
|
|
4077
|
+
description: dbText("somethingWentWrong"),
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
});
|
|
4082
|
+
draftWriteChainsRef.current.set(
|
|
4083
|
+
documentId,
|
|
4084
|
+
next.catch((error) => {
|
|
2894
4085
|
toast.error(dbText("failedToSavePagePreview"), {
|
|
2895
4086
|
description:
|
|
2896
|
-
|
|
4087
|
+
error instanceof Error
|
|
4088
|
+
? error.message
|
|
4089
|
+
: dbText("somethingWentWrong"),
|
|
2897
4090
|
});
|
|
4091
|
+
}),
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
function scheduleDraftWrite(
|
|
4096
|
+
controller: ReturnType<typeof createPreviewDocumentSaveController>,
|
|
4097
|
+
) {
|
|
4098
|
+
const existing = draftSaveTimersRef.current.get(documentId);
|
|
4099
|
+
if (existing) clearTimeout(existing);
|
|
4100
|
+
const timer = setTimeout(() => {
|
|
4101
|
+
draftSaveTimersRef.current.delete(documentId);
|
|
4102
|
+
enqueueDraftWrite(controller, "upsert");
|
|
4103
|
+
}, 250);
|
|
4104
|
+
draftSaveTimersRef.current.set(documentId, timer);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
// Adapters are replaced whenever a document is acquired. A controller can
|
|
4108
|
+
// outlive one preview mount while its final flush settles, but it must never
|
|
4109
|
+
// retain that old mount's hydration refs, mutation callback, or conflict UI.
|
|
4110
|
+
const makeSaveAdapter = (): PreviewDocumentSaveAdapter => ({
|
|
4111
|
+
save: (id, payload, baseline) =>
|
|
4112
|
+
new Promise((resolve, reject) => {
|
|
4113
|
+
// A just-deleted doc must not be re-dispatched (resurrection guard).
|
|
4114
|
+
if (deletedIdsRef.current.has(id)) {
|
|
4115
|
+
resolve(undefined);
|
|
4116
|
+
return;
|
|
4117
|
+
}
|
|
4118
|
+
const titleChanged = payload.title !== baseline?.title;
|
|
4119
|
+
const contentChanged = payload.content !== baseline?.content;
|
|
4120
|
+
if (bodyHydrationPendingRef.current && contentChanged) {
|
|
4121
|
+
resolve(deferredPreviewDocumentSave());
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
updateDocumentRef.current.mutate(
|
|
4125
|
+
{
|
|
4126
|
+
id,
|
|
4127
|
+
...(titleChanged ? { title: payload.title } : {}),
|
|
4128
|
+
...(contentChanged
|
|
4129
|
+
? {
|
|
4130
|
+
content: payload.content,
|
|
4131
|
+
loadedUpdatedAt: payload.loadedUpdatedAt,
|
|
4132
|
+
loadedContentWasEmpty: payload.loadedContentWasEmpty,
|
|
4133
|
+
}
|
|
4134
|
+
: {}),
|
|
4135
|
+
...(contentChanged && payload.loadedUpdatedAt
|
|
4136
|
+
? { baseUpdatedAt: payload.loadedUpdatedAt }
|
|
4137
|
+
: {}),
|
|
4138
|
+
},
|
|
4139
|
+
{
|
|
4140
|
+
onSuccess: (result) => {
|
|
4141
|
+
if (isDocumentUpdateConflict(result)) {
|
|
4142
|
+
resolve(
|
|
4143
|
+
deferredPreviewDocumentSave("conflict", {
|
|
4144
|
+
lastSaved: baseline ?? payload,
|
|
4145
|
+
pending: {
|
|
4146
|
+
title: result.document.title,
|
|
4147
|
+
content: result.document.content,
|
|
4148
|
+
loadedUpdatedAt: result.document.updatedAt,
|
|
4149
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4150
|
+
result.document.content,
|
|
4151
|
+
),
|
|
4152
|
+
},
|
|
4153
|
+
deferredReason: "conflict",
|
|
4154
|
+
}),
|
|
4155
|
+
);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
resolve(undefined);
|
|
4159
|
+
},
|
|
4160
|
+
onError: reject,
|
|
4161
|
+
},
|
|
4162
|
+
);
|
|
4163
|
+
}),
|
|
4164
|
+
onSaved: (persistedPayload) => {
|
|
4165
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4166
|
+
if (controller) enqueueDraftWrite(controller, "delete", persistedPayload);
|
|
4167
|
+
void queryClientRef.current.invalidateQueries({
|
|
4168
|
+
queryKey: contentDatabaseQueryKey(databaseDocumentId),
|
|
4169
|
+
});
|
|
4170
|
+
void queryClientRef.current.invalidateQueries({
|
|
4171
|
+
queryKey: ["action", "list-documents"],
|
|
4172
|
+
});
|
|
4173
|
+
},
|
|
4174
|
+
onError: (err) => {
|
|
4175
|
+
toast.error(dbText("failedToSavePagePreview"), {
|
|
4176
|
+
description:
|
|
4177
|
+
err instanceof Error ? err.message : dbText("somethingWentWrong"),
|
|
4178
|
+
});
|
|
4179
|
+
},
|
|
4180
|
+
onDraftConflict: (snapshot) => {
|
|
4181
|
+
setBodyDraftConflict({
|
|
4182
|
+
documentId,
|
|
4183
|
+
serverPayload: {
|
|
4184
|
+
title: snapshot.pending.title,
|
|
4185
|
+
content: snapshot.pending.content,
|
|
4186
|
+
loadedUpdatedAt: snapshot.pending.loadedUpdatedAt,
|
|
4187
|
+
loadedContentWasEmpty: snapshot.pending.loadedContentWasEmpty,
|
|
4188
|
+
},
|
|
4189
|
+
});
|
|
4190
|
+
},
|
|
4191
|
+
});
|
|
4192
|
+
|
|
4193
|
+
const makeController = () =>
|
|
4194
|
+
createPreviewDocumentSaveController({
|
|
4195
|
+
documentId,
|
|
4196
|
+
initial: {
|
|
4197
|
+
title: item.document.title,
|
|
4198
|
+
content: item.document.content,
|
|
4199
|
+
loadedUpdatedAt: item.document.updatedAt,
|
|
4200
|
+
loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
|
|
4201
|
+
item.document.content,
|
|
4202
|
+
),
|
|
2898
4203
|
},
|
|
4204
|
+
...makeSaveAdapter(),
|
|
2899
4205
|
});
|
|
2900
4206
|
|
|
2901
4207
|
// Acquire the controller for the current row, and release it on row-switch /
|
|
@@ -2929,8 +4235,21 @@ function DatabaseItemPreview({
|
|
|
2929
4235
|
saveControllerRef.current = acquirePreviewDocumentSaveController(
|
|
2930
4236
|
documentId,
|
|
2931
4237
|
makeController,
|
|
4238
|
+
(controller) => controller.replaceSaveAdapter(makeSaveAdapter()),
|
|
2932
4239
|
);
|
|
2933
4240
|
return () => {
|
|
4241
|
+
const controller = saveControllerRef.current;
|
|
4242
|
+
const draftTimer = draftSaveTimersRef.current.get(documentId);
|
|
4243
|
+
if (draftTimer) {
|
|
4244
|
+
clearTimeout(draftTimer);
|
|
4245
|
+
draftSaveTimersRef.current.delete(documentId);
|
|
4246
|
+
}
|
|
4247
|
+
if (
|
|
4248
|
+
controller &&
|
|
4249
|
+
!previewPayloadsEqual(controller.pending, controller.lastSaved)
|
|
4250
|
+
) {
|
|
4251
|
+
enqueueDraftWrite(controller, "upsert");
|
|
4252
|
+
}
|
|
2934
4253
|
saveControllerRef.current = null;
|
|
2935
4254
|
releasePreviewDocumentSaveController(documentId);
|
|
2936
4255
|
};
|
|
@@ -2940,6 +4259,58 @@ function DatabaseItemPreview({
|
|
|
2940
4259
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2941
4260
|
}, [documentId]);
|
|
2942
4261
|
|
|
4262
|
+
useEffect(() => {
|
|
4263
|
+
const draft = persistedPreviewDraft?.draft;
|
|
4264
|
+
if (!draft) {
|
|
4265
|
+
if (!draftVersionsRef.current.has(documentId)) {
|
|
4266
|
+
draftVersionsRef.current.set(documentId, null);
|
|
4267
|
+
}
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
draftVersionsRef.current.set(documentId, draft.version);
|
|
4271
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4272
|
+
if (!controller) return;
|
|
4273
|
+
const controllerDirty = !previewPayloadsEqual(
|
|
4274
|
+
controller.pending,
|
|
4275
|
+
controller.lastSaved,
|
|
4276
|
+
);
|
|
4277
|
+
const snapshot = {
|
|
4278
|
+
lastSaved: controller.lastSaved,
|
|
4279
|
+
pending: {
|
|
4280
|
+
title: draft.title,
|
|
4281
|
+
content: draft.content,
|
|
4282
|
+
loadedUpdatedAt: draft.baseDocumentUpdatedAt ?? undefined,
|
|
4283
|
+
loadedContentWasEmpty: draft.loadedContentWasEmpty === 1,
|
|
4284
|
+
},
|
|
4285
|
+
deferredReason:
|
|
4286
|
+
draft.deferredReason === "hydration" ||
|
|
4287
|
+
draft.deferredReason === "conflict"
|
|
4288
|
+
? draft.deferredReason
|
|
4289
|
+
: null,
|
|
4290
|
+
} as const;
|
|
4291
|
+
if (controllerDirty) {
|
|
4292
|
+
if (
|
|
4293
|
+
previewDraftNeedsConflict({
|
|
4294
|
+
returnedDraft: draft,
|
|
4295
|
+
pending: controller.pending,
|
|
4296
|
+
})
|
|
4297
|
+
) {
|
|
4298
|
+
controller.notifyDraftConflict(snapshot);
|
|
4299
|
+
} else {
|
|
4300
|
+
setBodyDraftConflict((current) =>
|
|
4301
|
+
current?.documentId === documentId ? null : current,
|
|
4302
|
+
);
|
|
4303
|
+
}
|
|
4304
|
+
return;
|
|
4305
|
+
}
|
|
4306
|
+
controller.restoreDraft(snapshot);
|
|
4307
|
+
setLocalTitle(snapshot.pending.title);
|
|
4308
|
+
setLocalContent(snapshot.pending.content);
|
|
4309
|
+
if (snapshot.deferredReason === "conflict") {
|
|
4310
|
+
controller.notifyDraftConflict(snapshot);
|
|
4311
|
+
}
|
|
4312
|
+
}, [documentId, persistedPreviewDraft?.draft]);
|
|
4313
|
+
|
|
2943
4314
|
// Sync displayed state to the current row, and adopt fresh server content
|
|
2944
4315
|
// (e.g. an agent edit) as the controller's new confirmed baseline. mark()
|
|
2945
4316
|
// touches only THIS row's controller — never another row's — because the
|
|
@@ -2973,12 +4344,31 @@ function DatabaseItemPreview({
|
|
|
2973
4344
|
controller.lastSaved.loadedContentWasEmpty === true &&
|
|
2974
4345
|
isEffectivelyEmptyDocumentContent(controller.pending.content) &&
|
|
2975
4346
|
!isEffectivelyEmptyDocumentContent(nextContent);
|
|
4347
|
+
const hydratedBodyConflictsWithDraft =
|
|
4348
|
+
!!controller &&
|
|
4349
|
+
dirty &&
|
|
4350
|
+
(controller.deferredReason === "conflict" ||
|
|
4351
|
+
previewDraftConflictsWithHydratedBody({
|
|
4352
|
+
loadedContent: controller.lastSaved.content,
|
|
4353
|
+
loadedUpdatedAt: controller.lastSaved.loadedUpdatedAt,
|
|
4354
|
+
loadedContentWasEmpty: controller.lastSaved.loadedContentWasEmpty,
|
|
4355
|
+
pendingContent: controller.pending.content,
|
|
4356
|
+
hydratedContent: nextContent,
|
|
4357
|
+
hydratedUpdatedAt: nextLoadedUpdatedAt,
|
|
4358
|
+
}));
|
|
2976
4359
|
// Only adopt the server's title/content — into BOTH the displayed editor
|
|
2977
4360
|
// state and the controller baseline — when the user hasn't typed something
|
|
2978
4361
|
// newer on this row. If a dirty in-progress edit exists, preserve it: don't
|
|
2979
4362
|
// clobber the visible text (the controller already holds the unsaved edit,
|
|
2980
4363
|
// so nothing is lost, but the editor must keep showing what the user typed).
|
|
2981
|
-
if (
|
|
4364
|
+
if (hydratedBodyConflictsWithDraft) {
|
|
4365
|
+
setBodyDraftConflict({ documentId, serverPayload });
|
|
4366
|
+
setLocalTitle(controller.pending.title);
|
|
4367
|
+
setLocalContent(controller.pending.content);
|
|
4368
|
+
} else if (
|
|
4369
|
+
staleEmptyPendingOverFreshServer ||
|
|
4370
|
+
(!dirty && !savedAheadOfServer)
|
|
4371
|
+
) {
|
|
2982
4372
|
setLocalTitle(nextTitle);
|
|
2983
4373
|
setLocalContent(nextContent);
|
|
2984
4374
|
controller?.mark(serverPayload);
|
|
@@ -3001,6 +4391,36 @@ function DatabaseItemPreview({
|
|
|
3001
4391
|
item.document.updatedAt,
|
|
3002
4392
|
]);
|
|
3003
4393
|
|
|
4394
|
+
// If hydration began after a keystroke, the controller deferred that save
|
|
4395
|
+
// without discarding it. Retry once the authoritative document is ready —
|
|
4396
|
+
// unless a non-empty Builder body arrived over an empty baseline. In that
|
|
4397
|
+
// conflict case the draft remains dirty in the registry instead of silently
|
|
4398
|
+
// overwriting either side; the update action's CAS is a final fail-closed
|
|
4399
|
+
// guard if the source changed between this check and the write.
|
|
4400
|
+
useEffect(() => {
|
|
4401
|
+
if (bodyHydrationPending || !document) return;
|
|
4402
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4403
|
+
if (!controller || controller.deferredReason !== "hydration") return;
|
|
4404
|
+
if (
|
|
4405
|
+
previewDraftConflictsWithHydratedBody({
|
|
4406
|
+
loadedContent: controller.lastSaved.content,
|
|
4407
|
+
loadedUpdatedAt: controller.lastSaved.loadedUpdatedAt,
|
|
4408
|
+
loadedContentWasEmpty: controller.lastSaved.loadedContentWasEmpty,
|
|
4409
|
+
pendingContent: controller.pending.content,
|
|
4410
|
+
hydratedContent: document.content,
|
|
4411
|
+
hydratedUpdatedAt: document.updatedAt,
|
|
4412
|
+
})
|
|
4413
|
+
) {
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
void controller.flush();
|
|
4417
|
+
}, [
|
|
4418
|
+
bodyHydrationPending,
|
|
4419
|
+
document?.content,
|
|
4420
|
+
document?.updatedAt,
|
|
4421
|
+
documentId,
|
|
4422
|
+
]);
|
|
4423
|
+
|
|
3004
4424
|
useEffect(() => {
|
|
3005
4425
|
if (!focusTitle || !previewCanEdit || isLoading || !document) return;
|
|
3006
4426
|
|
|
@@ -3016,7 +4436,9 @@ function DatabaseItemPreview({
|
|
|
3016
4436
|
function handleTitleChange(nextTitle: string) {
|
|
3017
4437
|
setLocalTitle(nextTitle);
|
|
3018
4438
|
if (!previewCanEdit || !document) return;
|
|
3019
|
-
saveControllerRef.current
|
|
4439
|
+
const controller = saveControllerRef.current;
|
|
4440
|
+
controller?.changeTitle(nextTitle);
|
|
4441
|
+
if (controller) scheduleDraftWrite(controller);
|
|
3020
4442
|
}
|
|
3021
4443
|
|
|
3022
4444
|
function handleContentChange(nextContent: string) {
|
|
@@ -3031,7 +4453,31 @@ function DatabaseItemPreview({
|
|
|
3031
4453
|
}
|
|
3032
4454
|
setLocalContent(nextContent);
|
|
3033
4455
|
if (!previewCanEdit || !document) return;
|
|
3034
|
-
saveControllerRef.current
|
|
4456
|
+
const controller = saveControllerRef.current;
|
|
4457
|
+
controller?.changeContent(nextContent);
|
|
4458
|
+
if (controller) scheduleDraftWrite(controller);
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
function keepLocalBodyDraft() {
|
|
4462
|
+
if (!activeBodyDraftConflict) return;
|
|
4463
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4464
|
+
if (!controller) return;
|
|
4465
|
+
controller.rebasePending(activeBodyDraftConflict.serverPayload);
|
|
4466
|
+
setBodyDraftConflict(null);
|
|
4467
|
+
void controller.flush();
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
function reloadBuilderBody() {
|
|
4471
|
+
if (!activeBodyDraftConflict) return;
|
|
4472
|
+
const controller = peekPreviewDocumentSaveController(documentId);
|
|
4473
|
+
if (!controller) return;
|
|
4474
|
+
const serverPayload = activeBodyDraftConflict.serverPayload;
|
|
4475
|
+
const discardedPayload = controller.pending;
|
|
4476
|
+
controller.mark(serverPayload);
|
|
4477
|
+
enqueueDraftWrite(controller, "delete", discardedPayload);
|
|
4478
|
+
setLocalTitle(serverPayload.title);
|
|
4479
|
+
setLocalContent(serverPayload.content);
|
|
4480
|
+
setBodyDraftConflict(null);
|
|
3035
4481
|
}
|
|
3036
4482
|
|
|
3037
4483
|
function handleIconChange(nextIcon: string | null) {
|
|
@@ -3326,6 +4772,37 @@ function DatabaseItemPreview({
|
|
|
3326
4772
|
|
|
3327
4773
|
return (
|
|
3328
4774
|
<div className="grid gap-4">
|
|
4775
|
+
{activeBodyDraftConflict ? (
|
|
4776
|
+
<div
|
|
4777
|
+
role="status"
|
|
4778
|
+
className="rounded-lg border border-amber-500/35 bg-amber-500/10 px-4 py-4 text-sm"
|
|
4779
|
+
>
|
|
4780
|
+
<div className="flex items-center gap-2 font-medium text-foreground">
|
|
4781
|
+
<IconLock className="size-4 text-amber-600 dark:text-amber-400" />
|
|
4782
|
+
{dbText("builderDraftConflictTitle")}
|
|
4783
|
+
</div>
|
|
4784
|
+
<p className="mt-2 max-w-2xl leading-6 text-muted-foreground">
|
|
4785
|
+
{dbText("builderDraftConflictDescription")}
|
|
4786
|
+
</p>
|
|
4787
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
4788
|
+
<Button
|
|
4789
|
+
type="button"
|
|
4790
|
+
size="sm"
|
|
4791
|
+
onClick={keepLocalBodyDraft}
|
|
4792
|
+
>
|
|
4793
|
+
{dbText("keepLocalDraft")}
|
|
4794
|
+
</Button>
|
|
4795
|
+
<Button
|
|
4796
|
+
type="button"
|
|
4797
|
+
size="sm"
|
|
4798
|
+
variant="outline"
|
|
4799
|
+
onClick={reloadBuilderBody}
|
|
4800
|
+
>
|
|
4801
|
+
{dbText("reloadBuilderBody")}
|
|
4802
|
+
</Button>
|
|
4803
|
+
</div>
|
|
4804
|
+
</div>
|
|
4805
|
+
) : null}
|
|
3329
4806
|
{bodyHydrationError ? (
|
|
3330
4807
|
<BuilderBodySyncingNotice
|
|
3331
4808
|
title={dbText("builderBodySyncFailedNotice")}
|
|
@@ -5390,7 +6867,12 @@ type PendingSourceCandidate = {
|
|
|
5390
6867
|
type SourceNavStep =
|
|
5391
6868
|
| { kind: "provider"; providerId: "builder" }
|
|
5392
6869
|
| { kind: "space"; spaceId: string; spaceName: string }
|
|
5393
|
-
| {
|
|
6870
|
+
| {
|
|
6871
|
+
kind: "model";
|
|
6872
|
+
model?: BuilderCmsModelSummary;
|
|
6873
|
+
sourceId?: string;
|
|
6874
|
+
sourceName?: string;
|
|
6875
|
+
}
|
|
5394
6876
|
| { kind: "addSource" }
|
|
5395
6877
|
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
5396
6878
|
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
@@ -5405,7 +6887,7 @@ function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
|
5405
6887
|
if (top.kind === "secondarySource") return top.sourceName;
|
|
5406
6888
|
if (top.kind === "keyConfirm") return dbText("matchExistingItemsToDetails");
|
|
5407
6889
|
if (top.kind === "fieldPicker") return dbText("chooseFields");
|
|
5408
|
-
return top.model.
|
|
6890
|
+
return top.model?.displayName ?? top.sourceName ?? "Builder";
|
|
5409
6891
|
}
|
|
5410
6892
|
|
|
5411
6893
|
// The Builder "B" brand mark (first glyph of the wordmark), drawn with
|
|
@@ -5453,6 +6935,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5453
6935
|
panel,
|
|
5454
6936
|
documentId,
|
|
5455
6937
|
canEdit,
|
|
6938
|
+
canManage,
|
|
5456
6939
|
activeView,
|
|
5457
6940
|
properties,
|
|
5458
6941
|
items,
|
|
@@ -5472,6 +6955,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5472
6955
|
onReviewBuilderUpdate,
|
|
5473
6956
|
onSetBuilderLiveWrites,
|
|
5474
6957
|
sourceActionPending,
|
|
6958
|
+
sourcePendingOperations,
|
|
5475
6959
|
onViewTypeChange,
|
|
5476
6960
|
onWrapCellsChange,
|
|
5477
6961
|
onOpenPagesInChange,
|
|
@@ -5486,6 +6970,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5486
6970
|
panel: DatabaseSettingsPanel;
|
|
5487
6971
|
documentId: string;
|
|
5488
6972
|
canEdit: boolean;
|
|
6973
|
+
canManage: boolean;
|
|
5489
6974
|
activeView: ContentDatabaseView;
|
|
5490
6975
|
properties: DocumentProperty[];
|
|
5491
6976
|
items: ContentDatabaseItem[];
|
|
@@ -5512,9 +6997,10 @@ function DatabaseSettingsPanelSheet({
|
|
|
5512
6997
|
onRefreshSource: (sourceId?: string) => void;
|
|
5513
6998
|
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
5514
6999
|
onDisconnectSource: (sourceId?: string) => void;
|
|
5515
|
-
onReviewBuilderUpdate: () => void;
|
|
5516
|
-
onSetBuilderLiveWrites: (
|
|
7000
|
+
onReviewBuilderUpdate: (sourceId: string) => void;
|
|
7001
|
+
onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
|
|
5517
7002
|
sourceActionPending: boolean;
|
|
7003
|
+
sourcePendingOperations: DatabaseSourcePendingOperations;
|
|
5518
7004
|
onViewTypeChange: (type: ContentDatabaseViewType) => void;
|
|
5519
7005
|
onWrapCellsChange: (wrapCells: boolean) => void;
|
|
5520
7006
|
onOpenPagesInChange: (openPagesIn: ContentDatabaseOpenPagesIn) => void;
|
|
@@ -5598,6 +7084,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5598
7084
|
documentId={documentId}
|
|
5599
7085
|
itemCount={items.length}
|
|
5600
7086
|
canEdit={canEdit}
|
|
7087
|
+
canManage={canManage}
|
|
5601
7088
|
nav={sourceNavStack}
|
|
5602
7089
|
onNavPush={(step) => setSourceNavStack((stack) => [...stack, step])}
|
|
5603
7090
|
onNavReplace={setSourceNavStack}
|
|
@@ -5614,6 +7101,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5614
7101
|
onReviewBuilderUpdate={onReviewBuilderUpdate}
|
|
5615
7102
|
onSetBuilderLiveWrites={onSetBuilderLiveWrites}
|
|
5616
7103
|
sourceActionPending={sourceActionPending}
|
|
7104
|
+
sourcePendingOperations={sourcePendingOperations}
|
|
5617
7105
|
/>
|
|
5618
7106
|
) : panel === "layout" ? (
|
|
5619
7107
|
<DatabaseSettingsLayoutPanel
|
|
@@ -5728,6 +7216,9 @@ export function builderReviewableChangeSets(
|
|
|
5728
7216
|
return source.changeSets.filter(
|
|
5729
7217
|
(changeSet) =>
|
|
5730
7218
|
changeSet.direction === "outbound" &&
|
|
7219
|
+
!changeSet.executions.some(
|
|
7220
|
+
(execution) => execution.state === "succeeded",
|
|
7221
|
+
) &&
|
|
5731
7222
|
(changeSet.state === "pending_push" ||
|
|
5732
7223
|
changeSet.state === "staged_revision" ||
|
|
5733
7224
|
changeSet.state === "approved"),
|
|
@@ -5753,12 +7244,33 @@ function maxSourceReviewRisk(
|
|
|
5753
7244
|
|
|
5754
7245
|
export function builderReviewExecutableRows(
|
|
5755
7246
|
review: ContentDatabaseSourceReviewPayload,
|
|
7247
|
+
selectedChangeSetIds: ReadonlySet<string> = new Set(
|
|
7248
|
+
review.rows.map((row) => row.changeSetId),
|
|
7249
|
+
),
|
|
5756
7250
|
) {
|
|
5757
|
-
if (!review.liveWritesEnabled
|
|
5758
|
-
return [];
|
|
5759
|
-
}
|
|
7251
|
+
if (!review.liveWritesEnabled) return [];
|
|
5760
7252
|
return review.rows.filter(
|
|
5761
|
-
(row) =>
|
|
7253
|
+
(row) =>
|
|
7254
|
+
selectedChangeSetIds.has(row.changeSetId) &&
|
|
7255
|
+
row.execution?.idempotencyKey &&
|
|
7256
|
+
(row.execution.state === "ready" ||
|
|
7257
|
+
row.execution.state === "running" ||
|
|
7258
|
+
row.execution.state === "response_received" ||
|
|
7259
|
+
row.execution.state === "reconciliation_required" ||
|
|
7260
|
+
(row.execution.state === "failed" &&
|
|
7261
|
+
builderExecutionPayloadHasSuccessfulResponse(row.execution.payload))),
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
function builderExecutionPayloadHasSuccessfulResponse(
|
|
7266
|
+
payload: Record<string, unknown>,
|
|
7267
|
+
) {
|
|
7268
|
+
const response = payload.response;
|
|
7269
|
+
return (
|
|
7270
|
+
!!response &&
|
|
7271
|
+
typeof response === "object" &&
|
|
7272
|
+
!Array.isArray(response) &&
|
|
7273
|
+
(response as Record<string, unknown>).ok === true
|
|
5762
7274
|
);
|
|
5763
7275
|
}
|
|
5764
7276
|
|
|
@@ -5766,24 +7278,67 @@ export function builderSourceLiveWriteControlState(
|
|
|
5766
7278
|
source: ContentDatabaseSource | null,
|
|
5767
7279
|
) {
|
|
5768
7280
|
const isBuilderSource = source?.sourceType === "builder-cms";
|
|
5769
|
-
const
|
|
5770
|
-
|
|
5771
|
-
|
|
7281
|
+
const legacyAllowedWriteModes = source?.metadata.allowedWriteModes ?? [];
|
|
7282
|
+
const writeMode =
|
|
7283
|
+
source?.metadata.writeMode ??
|
|
7284
|
+
(source?.capabilities.liveWritesEnabled === true
|
|
7285
|
+
? legacyAllowedWriteModes.some((mode) => mode !== "autosave")
|
|
7286
|
+
? "publish_updates"
|
|
7287
|
+
: "stage_only"
|
|
7288
|
+
: "read_only");
|
|
7289
|
+
const enabled = writeMode !== "read_only";
|
|
5772
7290
|
return {
|
|
5773
|
-
safeTarget,
|
|
7291
|
+
safeTarget: isBuilderSource,
|
|
5774
7292
|
enabled,
|
|
5775
|
-
|
|
7293
|
+
writeMode,
|
|
7294
|
+
allowPublicationTransitions:
|
|
7295
|
+
writeMode === "publish_updates" &&
|
|
7296
|
+
source?.metadata.allowPublicationTransitions === true,
|
|
7297
|
+
availableWriteModes: BUILDER_WRITE_MODE_OPTIONS.map(
|
|
7298
|
+
(option) => option.mode,
|
|
7299
|
+
),
|
|
7300
|
+
showPublicationTransitions: writeMode === "publish_updates",
|
|
7301
|
+
showAction: isBuilderSource,
|
|
5776
7302
|
actionLabel: enabled ? "Disable" : "Enable",
|
|
5777
7303
|
description: enabled
|
|
5778
|
-
?
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
7304
|
+
? writeMode === "publish_updates"
|
|
7305
|
+
? "Approved updates can write through to Builder while preserving publication state."
|
|
7306
|
+
: "Approved updates can create Builder autosave revisions."
|
|
7307
|
+
: isBuilderSource
|
|
7308
|
+
? "Off by default. An administrator can choose a guarded write tier for this source."
|
|
7309
|
+
: "Live writes are not available for this source.",
|
|
5784
7310
|
};
|
|
5785
7311
|
}
|
|
5786
7312
|
|
|
7313
|
+
const BUILDER_WRITE_MODE_OPTIONS: Array<{
|
|
7314
|
+
mode: ContentDatabaseSourceWriteMode;
|
|
7315
|
+
labelKey: DatabaseMessageKey;
|
|
7316
|
+
descriptionKey: DatabaseMessageKey;
|
|
7317
|
+
}> = [
|
|
7318
|
+
{
|
|
7319
|
+
mode: "read_only",
|
|
7320
|
+
labelKey: "readOnly",
|
|
7321
|
+
descriptionKey: "noBuilderWrites",
|
|
7322
|
+
},
|
|
7323
|
+
{
|
|
7324
|
+
mode: "stage_only",
|
|
7325
|
+
labelKey: "stageOnly",
|
|
7326
|
+
descriptionKey: "savesDraftsNeverPublishes",
|
|
7327
|
+
},
|
|
7328
|
+
{
|
|
7329
|
+
mode: "publish_updates",
|
|
7330
|
+
labelKey: "publishUpdates",
|
|
7331
|
+
descriptionKey: "writesUpdatesToLiveEntries",
|
|
7332
|
+
},
|
|
7333
|
+
];
|
|
7334
|
+
|
|
7335
|
+
function builderWriteModeSummary(mode: ContentDatabaseSourceWriteMode) {
|
|
7336
|
+
return dbText(
|
|
7337
|
+
BUILDER_WRITE_MODE_OPTIONS.find((option) => option.mode === mode)
|
|
7338
|
+
?.descriptionKey ?? "noBuilderWrites",
|
|
7339
|
+
);
|
|
7340
|
+
}
|
|
7341
|
+
|
|
5787
7342
|
export function buildClientBuilderReviewPayload(
|
|
5788
7343
|
source: ContentDatabaseSource,
|
|
5789
7344
|
changeSets: ContentDatabaseSourceChangeSet[],
|
|
@@ -5809,6 +7364,7 @@ export function buildClientBuilderReviewPayload(
|
|
|
5809
7364
|
(field) => field.localFieldKey === "title",
|
|
5810
7365
|
);
|
|
5811
7366
|
const proposedTitle = titleChange?.proposedValue;
|
|
7367
|
+
const effect = resolveBuilderCmsWriteEffect({ source, changeSet });
|
|
5812
7368
|
|
|
5813
7369
|
return {
|
|
5814
7370
|
changeSetId: changeSet.id,
|
|
@@ -5818,12 +7374,14 @@ export function buildClientBuilderReviewPayload(
|
|
|
5818
7374
|
typeof proposedTitle === "string" && proposedTitle.trim()
|
|
5819
7375
|
? proposedTitle
|
|
5820
7376
|
: sourceRow?.sourceDisplayKey || "Untitled",
|
|
7377
|
+
targetEntryId:
|
|
7378
|
+
effect === "create_draft" ? null : (sourceRow?.sourceRowId ?? null),
|
|
5821
7379
|
fieldChanges: changeSet.fieldChanges,
|
|
5822
7380
|
bodyChange: changeSet.bodyChange,
|
|
5823
7381
|
riskLevel: changeSet.riskLevel,
|
|
5824
7382
|
riskReasons: changeSet.riskReasons,
|
|
5825
7383
|
conflictState: changeSet.conflictState,
|
|
5826
|
-
effect
|
|
7384
|
+
effect,
|
|
5827
7385
|
execution: latestExecution,
|
|
5828
7386
|
};
|
|
5829
7387
|
});
|
|
@@ -5846,19 +7404,22 @@ export function buildClientBuilderReviewPayload(
|
|
|
5846
7404
|
executionStates.length > 0 &&
|
|
5847
7405
|
executionStates.every((state) => state === "succeeded")
|
|
5848
7406
|
? "succeeded"
|
|
5849
|
-
: executionStates.includes("
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
7407
|
+
: executionStates.includes("reconciliation_required") ||
|
|
7408
|
+
executionStates.includes("response_received")
|
|
7409
|
+
? "reconciliation_required"
|
|
7410
|
+
: executionStates.includes("failed")
|
|
7411
|
+
? "failed"
|
|
7412
|
+
: executionStates.includes("running")
|
|
7413
|
+
? "running"
|
|
7414
|
+
: statuses.some((status) => status.status === "stale")
|
|
7415
|
+
? "stale"
|
|
7416
|
+
: statuses.some((status) => status.status === "blocked")
|
|
7417
|
+
? "blocked"
|
|
7418
|
+
: statuses.some((status) => status.status === "validated")
|
|
5860
7419
|
? "validated"
|
|
5861
|
-
:
|
|
7420
|
+
: source.capabilities.liveWritesEnabled
|
|
7421
|
+
? "validated"
|
|
7422
|
+
: "write_disabled";
|
|
5862
7423
|
|
|
5863
7424
|
return {
|
|
5864
7425
|
summary:
|
|
@@ -5880,19 +7441,21 @@ export function buildClientBuilderReviewPayload(
|
|
|
5880
7441
|
? "Pushed to Builder and reconciled locally."
|
|
5881
7442
|
: resultStatus === "failed"
|
|
5882
7443
|
? "Builder push failed. The change remains retryable."
|
|
5883
|
-
: resultStatus === "
|
|
5884
|
-
? "Builder
|
|
5885
|
-
: resultStatus === "
|
|
5886
|
-
?
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
: resultStatus === "
|
|
5894
|
-
? "Push needs
|
|
5895
|
-
:
|
|
7444
|
+
: resultStatus === "reconciliation_required"
|
|
7445
|
+
? "Builder reconciliation is required. Do not retry this write."
|
|
7446
|
+
: resultStatus === "running"
|
|
7447
|
+
? "Builder push is running."
|
|
7448
|
+
: resultStatus === "validated"
|
|
7449
|
+
? source.capabilities.liveWritesEnabled
|
|
7450
|
+
? hasExecutionEvidence
|
|
7451
|
+
? "Push checked successfully. Ready to send to Builder."
|
|
7452
|
+
: "Ready to send to Builder."
|
|
7453
|
+
: "Push checked successfully. Nothing was sent to Builder."
|
|
7454
|
+
: resultStatus === "blocked"
|
|
7455
|
+
? "Push needs attention before anything can be sent to Builder."
|
|
7456
|
+
: resultStatus === "stale"
|
|
7457
|
+
? "Push needs a fresh review because the plan changed."
|
|
7458
|
+
: "Builder writes are off in this local build. Push will check the update only.",
|
|
5896
7459
|
},
|
|
5897
7460
|
};
|
|
5898
7461
|
}
|
|
@@ -5903,6 +7466,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
5903
7466
|
documentId,
|
|
5904
7467
|
itemCount,
|
|
5905
7468
|
canEdit,
|
|
7469
|
+
canManage,
|
|
5906
7470
|
nav,
|
|
5907
7471
|
onNavPush,
|
|
5908
7472
|
onNavReplace,
|
|
@@ -5916,12 +7480,14 @@ function DatabaseSettingsSourcePanel({
|
|
|
5916
7480
|
onReviewBuilderUpdate,
|
|
5917
7481
|
onSetBuilderLiveWrites,
|
|
5918
7482
|
sourceActionPending,
|
|
7483
|
+
sourcePendingOperations,
|
|
5919
7484
|
}: {
|
|
5920
7485
|
source: ContentDatabaseSource | null;
|
|
5921
7486
|
sources: ContentDatabaseSource[];
|
|
5922
7487
|
documentId: string;
|
|
5923
7488
|
itemCount: number;
|
|
5924
7489
|
canEdit: boolean;
|
|
7490
|
+
canManage: boolean;
|
|
5925
7491
|
nav: SourceNavStep[];
|
|
5926
7492
|
onNavPush: (step: SourceNavStep) => void;
|
|
5927
7493
|
onNavReplace: (stack: SourceNavStep[]) => void;
|
|
@@ -5942,26 +7508,13 @@ function DatabaseSettingsSourcePanel({
|
|
|
5942
7508
|
onRefreshSource: (sourceId?: string) => void;
|
|
5943
7509
|
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
5944
7510
|
onDisconnectSource: (sourceId?: string) => void;
|
|
5945
|
-
onReviewBuilderUpdate: () => void;
|
|
5946
|
-
onSetBuilderLiveWrites: (
|
|
7511
|
+
onReviewBuilderUpdate: (sourceId: string) => void;
|
|
7512
|
+
onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
|
|
5947
7513
|
sourceActionPending: boolean;
|
|
7514
|
+
sourcePendingOperations: DatabaseSourcePendingOperations;
|
|
5948
7515
|
}) {
|
|
5949
|
-
const outboundChangeSets =
|
|
5950
|
-
source?.changeSets.filter(
|
|
5951
|
-
(changeSet) => changeSet.direction === "outbound",
|
|
5952
|
-
) ?? [];
|
|
5953
|
-
const reviewableBuilderChangeSets = outboundChangeSets.filter(
|
|
5954
|
-
(changeSet) =>
|
|
5955
|
-
changeSet.state === "pending_push" ||
|
|
5956
|
-
changeSet.state === "staged_revision" ||
|
|
5957
|
-
changeSet.state === "approved",
|
|
5958
|
-
);
|
|
5959
|
-
const conflictChangeSets =
|
|
5960
|
-
source?.changeSets.filter(
|
|
5961
|
-
(changeSet) => changeSet.conflictState === "source_changed",
|
|
5962
|
-
) ?? [];
|
|
5963
7516
|
const { isCodeMode } = useCodeMode();
|
|
5964
|
-
const
|
|
7517
|
+
const builderSources = databaseAttachedBuilderSources(sources, source);
|
|
5965
7518
|
const builderStatus = useBuilderStatus();
|
|
5966
7519
|
const builderConfigured = builderStatus.status?.configured === true;
|
|
5967
7520
|
const builderOrgName = builderStatus.status?.orgName ?? null;
|
|
@@ -5980,10 +7533,6 @@ function DatabaseSettingsSourcePanel({
|
|
|
5980
7533
|
void builderStatus.refetch();
|
|
5981
7534
|
},
|
|
5982
7535
|
});
|
|
5983
|
-
const builderSyncFailed =
|
|
5984
|
-
isBuilderSource &&
|
|
5985
|
-
(source?.syncState === "error" || Boolean(source?.lastError));
|
|
5986
|
-
|
|
5987
7536
|
const top = nav[nav.length - 1];
|
|
5988
7537
|
|
|
5989
7538
|
// ── Sources list (root) ───────────────────────────────────────────────
|
|
@@ -5994,9 +7543,21 @@ function DatabaseSettingsSourcePanel({
|
|
|
5994
7543
|
sources={sources}
|
|
5995
7544
|
builderConfigured={builderConfigured}
|
|
5996
7545
|
builderSpaceLabel={builderSpaceLabel}
|
|
5997
|
-
reviewableCount={
|
|
5998
|
-
|
|
5999
|
-
|
|
7546
|
+
reviewableCount={builderSources.reduce(
|
|
7547
|
+
(total, candidate) =>
|
|
7548
|
+
total + builderReviewableChangeSets(candidate).length,
|
|
7549
|
+
0,
|
|
7550
|
+
)}
|
|
7551
|
+
onOpenBuilder={(builderSource) =>
|
|
7552
|
+
onNavPush(
|
|
7553
|
+
builderSource
|
|
7554
|
+
? {
|
|
7555
|
+
kind: "model",
|
|
7556
|
+
sourceId: builderSource.id,
|
|
7557
|
+
sourceName: builderSource.sourceName,
|
|
7558
|
+
}
|
|
7559
|
+
: { kind: "provider", providerId: "builder" },
|
|
7560
|
+
)
|
|
6000
7561
|
}
|
|
6001
7562
|
onOpenNotion={() => onNavPush({ kind: "addSource" })}
|
|
6002
7563
|
onOpenSecondary={(secondary) =>
|
|
@@ -6200,22 +7761,28 @@ function DatabaseSettingsSourcePanel({
|
|
|
6200
7761
|
if (top.kind === "space") {
|
|
6201
7762
|
return (
|
|
6202
7763
|
<BuilderSpaceModelsView
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
7764
|
+
attachedModelNames={databaseAttachedBuilderModelNames(sources, source)}
|
|
7765
|
+
onOpenModel={(model) => {
|
|
7766
|
+
const attached = databaseAttachedBuilderSource(sources, source, {
|
|
7767
|
+
modelName: model.name,
|
|
7768
|
+
});
|
|
7769
|
+
onNavPush({ kind: "model", model, sourceId: attached?.id });
|
|
7770
|
+
}}
|
|
6207
7771
|
/>
|
|
6208
7772
|
);
|
|
6209
7773
|
}
|
|
6210
7774
|
|
|
6211
7775
|
// ── Model leaf ────────────────────────────────────────────────────────
|
|
6212
7776
|
const model = top.model;
|
|
6213
|
-
const
|
|
6214
|
-
|
|
7777
|
+
const selectedBuilderSource = databaseAttachedBuilderSource(sources, source, {
|
|
7778
|
+
sourceId: top.sourceId,
|
|
7779
|
+
modelName: model?.name,
|
|
7780
|
+
});
|
|
6215
7781
|
|
|
6216
7782
|
// Unattached model → the attach affordance (the model is already chosen by
|
|
6217
7783
|
// drilling in, so there's no model picker here).
|
|
6218
|
-
if (!
|
|
7784
|
+
if (!selectedBuilderSource) {
|
|
7785
|
+
if (!model) return null;
|
|
6219
7786
|
return (
|
|
6220
7787
|
<div className="grid min-w-0 gap-3">
|
|
6221
7788
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
@@ -6294,41 +7861,91 @@ function DatabaseSettingsSourcePanel({
|
|
|
6294
7861
|
);
|
|
6295
7862
|
}
|
|
6296
7863
|
|
|
6297
|
-
|
|
7864
|
+
const selectedSource = selectedBuilderSource;
|
|
7865
|
+
const reviewableBuilderChangeSets =
|
|
7866
|
+
builderReviewableChangeSets(selectedSource);
|
|
7867
|
+
const outboundChangeSets = reviewableBuilderChangeSets;
|
|
7868
|
+
const conflictChangeSets = selectedSource.changeSets.filter(
|
|
7869
|
+
(changeSet) => changeSet.conflictState === "source_changed",
|
|
7870
|
+
);
|
|
7871
|
+
const builderSyncFailed =
|
|
7872
|
+
selectedSource.syncState === "error" || Boolean(selectedSource.lastError);
|
|
7873
|
+
const liveWriteControl = builderSourceLiveWriteControlState(selectedSource);
|
|
7874
|
+
const builderWriteMode = liveWriteControl.writeMode;
|
|
7875
|
+
const selectedSourceMutationPending = databaseSourceOperationIsPending(
|
|
7876
|
+
sourcePendingOperations,
|
|
7877
|
+
selectedSource.id,
|
|
7878
|
+
[
|
|
7879
|
+
"changeRole",
|
|
7880
|
+
"refresh",
|
|
7881
|
+
"hydration",
|
|
7882
|
+
"disconnect",
|
|
7883
|
+
"review",
|
|
7884
|
+
"execute",
|
|
7885
|
+
"writeMode",
|
|
7886
|
+
],
|
|
7887
|
+
);
|
|
7888
|
+
const selectedSourceReviewPending = databaseSourceOperationIsPending(
|
|
7889
|
+
sourcePendingOperations,
|
|
7890
|
+
selectedSource.id,
|
|
7891
|
+
["refresh", "disconnect", "review", "execute", "writeMode"],
|
|
7892
|
+
);
|
|
7893
|
+
const selectedSourceHydrationPending = databaseSourceOperationIsPending(
|
|
7894
|
+
sourcePendingOperations,
|
|
7895
|
+
selectedSource.id,
|
|
7896
|
+
["refresh", "hydration", "disconnect"],
|
|
7897
|
+
);
|
|
7898
|
+
const selectedSourceRefreshControlPending =
|
|
7899
|
+
sourcePendingOperations.refresh || selectedSourceMutationPending;
|
|
7900
|
+
const selectedSourceReviewControlPending =
|
|
7901
|
+
sourcePendingOperations.review ||
|
|
7902
|
+
sourcePendingOperations.execute ||
|
|
7903
|
+
selectedSourceReviewPending;
|
|
7904
|
+
const selectedSourceHydrationControlPending =
|
|
7905
|
+
sourcePendingOperations.hydration || selectedSourceHydrationPending;
|
|
7906
|
+
const selectedSourceRoleControlPending =
|
|
7907
|
+
sourcePendingOperations.changeRole || selectedSourceMutationPending;
|
|
7908
|
+
const selectedSourceDisconnectControlPending =
|
|
7909
|
+
sourcePendingOperations.disconnect || selectedSourceMutationPending;
|
|
7910
|
+
const builderWriteModeDisabled =
|
|
7911
|
+
!canManage ||
|
|
7912
|
+
databaseBuilderWriteModeOperationPending(
|
|
7913
|
+
sourcePendingOperations,
|
|
7914
|
+
selectedSource.id,
|
|
7915
|
+
);
|
|
7916
|
+
|
|
7917
|
+
// Attached model → source details and guarded write policy.
|
|
6298
7918
|
return (
|
|
6299
7919
|
<div className="grid min-w-0 gap-4">
|
|
6300
7920
|
<>
|
|
6301
7921
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
6302
7922
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
6303
|
-
<span
|
|
6304
|
-
|
|
7923
|
+
<span
|
|
7924
|
+
className="truncate font-medium"
|
|
7925
|
+
title={selectedSource.sourceName}
|
|
7926
|
+
>
|
|
7927
|
+
{selectedSource.sourceName}
|
|
6305
7928
|
</span>
|
|
6306
7929
|
<div className="flex shrink-0 items-center gap-1">
|
|
6307
|
-
{
|
|
6308
|
-
|
|
6309
|
-
<
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
</span>
|
|
6313
|
-
) : (
|
|
6314
|
-
<span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
6315
|
-
<IconLock className="size-3" />
|
|
6316
|
-
{dbText("readOnly")}
|
|
6317
|
-
</span>
|
|
6318
|
-
)
|
|
7930
|
+
{selectedSource.capabilities.liveWritesEnabled ? (
|
|
7931
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-foreground">
|
|
7932
|
+
<IconPencil className="size-3" />
|
|
7933
|
+
{dbText("liveWritesOn")}
|
|
7934
|
+
</span>
|
|
6319
7935
|
) : (
|
|
6320
|
-
<span className="rounded-full border border-border px-2 py-0.5 text-[11px]
|
|
6321
|
-
|
|
7936
|
+
<span className="inline-flex items-center gap-1 rounded-full border border-border px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
|
|
7937
|
+
<IconLock className="size-3" />
|
|
7938
|
+
{dbText("readOnly")}
|
|
6322
7939
|
</span>
|
|
6323
7940
|
)}
|
|
6324
|
-
{
|
|
7941
|
+
{!builderSyncFailed ? (
|
|
6325
7942
|
<Button
|
|
6326
7943
|
type="button"
|
|
6327
7944
|
variant="ghost"
|
|
6328
7945
|
size="sm"
|
|
6329
7946
|
className="h-7 gap-1 px-2 text-xs"
|
|
6330
|
-
disabled={!canEdit ||
|
|
6331
|
-
onClick={() => onRefreshSource(
|
|
7947
|
+
disabled={!canEdit || selectedSourceRefreshControlPending}
|
|
7948
|
+
onClick={() => onRefreshSource(selectedSource.id)}
|
|
6332
7949
|
>
|
|
6333
7950
|
<IconRefresh className="size-3" />
|
|
6334
7951
|
{dbText("refreshSource")}
|
|
@@ -6341,109 +7958,186 @@ function DatabaseSettingsSourcePanel({
|
|
|
6341
7958
|
<button
|
|
6342
7959
|
type="button"
|
|
6343
7960
|
className="inline-flex items-center gap-1 text-destructive hover:underline disabled:opacity-60"
|
|
6344
|
-
disabled={!canEdit ||
|
|
6345
|
-
onClick={() => onRefreshSource(
|
|
7961
|
+
disabled={!canEdit || selectedSourceRefreshControlPending}
|
|
7962
|
+
onClick={() => onRefreshSource(selectedSource.id)}
|
|
6346
7963
|
>
|
|
6347
7964
|
<IconRefresh className="size-3" />
|
|
6348
7965
|
{dbText("couldntSyncRetry")}
|
|
6349
7966
|
</button>
|
|
6350
|
-
) :
|
|
7967
|
+
) : (
|
|
6351
7968
|
[
|
|
6352
7969
|
builderConfigured ? (builderSpaceLabel ?? "Connected") : null,
|
|
6353
|
-
|
|
7970
|
+
selectedSource.lastRefreshedAt
|
|
6354
7971
|
? `synced ${
|
|
6355
|
-
formatRelativeSyncTime(
|
|
6356
|
-
|
|
7972
|
+
formatRelativeSyncTime(selectedSource.lastRefreshedAt) ??
|
|
7973
|
+
selectedSource.freshness
|
|
6357
7974
|
}`
|
|
6358
|
-
:
|
|
6359
|
-
typeof
|
|
7975
|
+
: selectedSource.freshness,
|
|
7976
|
+
typeof selectedSource.metadata.lastReadFetchedEntryCount ===
|
|
7977
|
+
"number"
|
|
6360
7978
|
? dbText("builderRowsFetched", {
|
|
6361
|
-
count:
|
|
7979
|
+
count: selectedSource.metadata.lastReadFetchedEntryCount,
|
|
6362
7980
|
})
|
|
6363
7981
|
: null,
|
|
6364
|
-
builderSourceRowFetchStatus(
|
|
7982
|
+
builderSourceRowFetchStatus(selectedSource) === "error"
|
|
6365
7983
|
? dbText("builderRowsFetchFailed")
|
|
6366
|
-
: builderSourceRowFetchStatus(
|
|
7984
|
+
: builderSourceRowFetchStatus(selectedSource) === "fetching"
|
|
6367
7985
|
? dbText("builderRowsFetchingMore")
|
|
6368
7986
|
: null,
|
|
6369
7987
|
]
|
|
6370
7988
|
.filter(Boolean)
|
|
6371
7989
|
.join(" · ")
|
|
6372
|
-
) : (
|
|
6373
|
-
`Local snapshot · ${source.freshness}`
|
|
6374
7990
|
)}
|
|
6375
7991
|
</div>
|
|
7992
|
+
<div className="grid min-w-0 gap-2 border-t border-border pt-3 text-xs text-muted-foreground">
|
|
7993
|
+
<div className="flex items-center justify-between gap-3">
|
|
7994
|
+
<span className="font-medium text-foreground">
|
|
7995
|
+
{dbText("builderWriteMode")}
|
|
7996
|
+
</span>
|
|
7997
|
+
<span>{builderWriteModeSummary(builderWriteMode)}</span>
|
|
7998
|
+
</div>
|
|
7999
|
+
<div
|
|
8000
|
+
className="grid grid-cols-3 gap-0.5 rounded-md border border-border bg-muted/35 p-0.5"
|
|
8001
|
+
aria-label={dbText("builderWriteMode")}
|
|
8002
|
+
>
|
|
8003
|
+
{BUILDER_WRITE_MODE_OPTIONS.map((option) => {
|
|
8004
|
+
const selected = builderWriteMode === option.mode;
|
|
8005
|
+
return (
|
|
8006
|
+
<button
|
|
8007
|
+
key={option.mode}
|
|
8008
|
+
type="button"
|
|
8009
|
+
aria-pressed={selected}
|
|
8010
|
+
title={dbText(option.descriptionKey)}
|
|
8011
|
+
disabled={builderWriteModeDisabled}
|
|
8012
|
+
className={cn(
|
|
8013
|
+
"min-w-0 rounded px-2 py-1.5 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60",
|
|
8014
|
+
selected
|
|
8015
|
+
? "bg-[#2383e2] text-white shadow-sm"
|
|
8016
|
+
: "text-muted-foreground hover:bg-background hover:text-foreground",
|
|
8017
|
+
)}
|
|
8018
|
+
onClick={() =>
|
|
8019
|
+
onSetBuilderLiveWrites({
|
|
8020
|
+
sourceId: selectedSource.id,
|
|
8021
|
+
writeMode: option.mode,
|
|
8022
|
+
allowPublicationTransitions:
|
|
8023
|
+
option.mode === "publish_updates"
|
|
8024
|
+
? liveWriteControl.allowPublicationTransitions
|
|
8025
|
+
: false,
|
|
8026
|
+
})
|
|
8027
|
+
}
|
|
8028
|
+
>
|
|
8029
|
+
<span className="block truncate">
|
|
8030
|
+
{dbText(option.labelKey)}
|
|
8031
|
+
</span>
|
|
8032
|
+
</button>
|
|
8033
|
+
);
|
|
8034
|
+
})}
|
|
8035
|
+
</div>
|
|
8036
|
+
{liveWriteControl.showPublicationTransitions ? (
|
|
8037
|
+
<button
|
|
8038
|
+
type="button"
|
|
8039
|
+
role="checkbox"
|
|
8040
|
+
aria-checked={liveWriteControl.allowPublicationTransitions}
|
|
8041
|
+
disabled={builderWriteModeDisabled}
|
|
8042
|
+
className="flex min-w-0 items-start gap-2 rounded px-1 py-1 text-left transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60"
|
|
8043
|
+
onClick={() =>
|
|
8044
|
+
onSetBuilderLiveWrites({
|
|
8045
|
+
sourceId: selectedSource.id,
|
|
8046
|
+
writeMode: "publish_updates",
|
|
8047
|
+
allowPublicationTransitions:
|
|
8048
|
+
!liveWriteControl.allowPublicationTransitions,
|
|
8049
|
+
})
|
|
8050
|
+
}
|
|
8051
|
+
>
|
|
8052
|
+
<span
|
|
8053
|
+
className={cn(
|
|
8054
|
+
"mt-0.5 inline-flex size-4 shrink-0 items-center justify-center rounded border",
|
|
8055
|
+
liveWriteControl.allowPublicationTransitions
|
|
8056
|
+
? "border-[#2383e2] bg-[#2383e2] text-white"
|
|
8057
|
+
: "border-muted-foreground/40 bg-background text-transparent",
|
|
8058
|
+
)}
|
|
8059
|
+
>
|
|
8060
|
+
{liveWriteControl.allowPublicationTransitions ? (
|
|
8061
|
+
<IconCheck className="size-3" />
|
|
8062
|
+
) : null}
|
|
8063
|
+
</span>
|
|
8064
|
+
<span className="min-w-0">
|
|
8065
|
+
<span className="block text-foreground">
|
|
8066
|
+
{dbText("allowPublishUnpublishPerItem")}
|
|
8067
|
+
</span>
|
|
8068
|
+
</span>
|
|
8069
|
+
</button>
|
|
8070
|
+
) : null}
|
|
8071
|
+
</div>
|
|
6376
8072
|
</div>
|
|
6377
8073
|
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
: `${reviewableBuilderChangeSets.length} change${
|
|
8074
|
+
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-muted/30 p-3">
|
|
8075
|
+
<div className="flex items-center justify-between gap-3">
|
|
8076
|
+
<div className="min-w-0">
|
|
8077
|
+
<div className="text-sm font-medium">
|
|
8078
|
+
{conflictChangeSets.length > 0
|
|
8079
|
+
? `${conflictChangeSets.length} change${
|
|
8080
|
+
conflictChangeSets.length === 1 ? "" : "s"
|
|
8081
|
+
} need review`
|
|
8082
|
+
: reviewableBuilderChangeSets.length > 0
|
|
8083
|
+
? `${reviewableBuilderChangeSets.length} change${
|
|
6389
8084
|
reviewableBuilderChangeSets.length === 1 ? "" : "s"
|
|
6390
|
-
} ready to push`
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
8085
|
+
} ready to push`
|
|
8086
|
+
: "Check for local content changes"}
|
|
8087
|
+
</div>
|
|
8088
|
+
<div className="mt-0.5 break-words text-xs text-muted-foreground">
|
|
8089
|
+
{dbText("reviewDiffDescription")}
|
|
6395
8090
|
</div>
|
|
6396
|
-
<Button
|
|
6397
|
-
type="button"
|
|
6398
|
-
size="sm"
|
|
6399
|
-
className="shrink-0"
|
|
6400
|
-
disabled={!canEdit || sourceActionPending}
|
|
6401
|
-
onClick={onReviewBuilderUpdate}
|
|
6402
|
-
>
|
|
6403
|
-
<IconCheck className="mr-1.5 size-3.5" />
|
|
6404
|
-
{dbText("reviewDiff")}
|
|
6405
|
-
</Button>
|
|
6406
8091
|
</div>
|
|
8092
|
+
<Button
|
|
8093
|
+
type="button"
|
|
8094
|
+
size="sm"
|
|
8095
|
+
className="shrink-0"
|
|
8096
|
+
disabled={!canEdit || selectedSourceReviewControlPending}
|
|
8097
|
+
onClick={() => onReviewBuilderUpdate(selectedSource.id)}
|
|
8098
|
+
>
|
|
8099
|
+
<IconCheck className="mr-1.5 size-3.5" />
|
|
8100
|
+
{dbText("reviewDiff")}
|
|
8101
|
+
</Button>
|
|
6407
8102
|
</div>
|
|
6408
|
-
|
|
8103
|
+
</div>
|
|
6409
8104
|
|
|
6410
|
-
{
|
|
8105
|
+
{selectedSource.bodyHydration ? (
|
|
6411
8106
|
<BuilderBodyHydrationCard
|
|
6412
|
-
source={
|
|
8107
|
+
source={selectedSource}
|
|
6413
8108
|
canEdit={canEdit}
|
|
6414
|
-
pending={
|
|
6415
|
-
onHydrate={() => onHydrateBuilderBodies(
|
|
8109
|
+
pending={selectedSourceHydrationControlPending}
|
|
8110
|
+
onHydrate={() => onHydrateBuilderBodies(selectedSource.id)}
|
|
6416
8111
|
/>
|
|
6417
8112
|
) : null}
|
|
6418
8113
|
|
|
8114
|
+
<BuilderRequiredFieldsCard
|
|
8115
|
+
documentId={documentId}
|
|
8116
|
+
source={selectedSource}
|
|
8117
|
+
canEdit={canEdit}
|
|
8118
|
+
pending={selectedSourceMutationPending}
|
|
8119
|
+
/>
|
|
8120
|
+
|
|
6419
8121
|
{isCodeMode ? (
|
|
6420
8122
|
<>
|
|
6421
8123
|
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3 text-sm">
|
|
6422
|
-
<div className="font-medium">
|
|
6423
|
-
{source.sourceType === "builder-cms"
|
|
6424
|
-
? "Local Builder changes"
|
|
6425
|
-
: "Local outbound changes"}
|
|
6426
|
-
</div>
|
|
8124
|
+
<div className="font-medium">{dbText("builderChanges")}</div>
|
|
6427
8125
|
<div className="text-xs text-muted-foreground">
|
|
6428
|
-
{
|
|
6429
|
-
?
|
|
6430
|
-
|
|
6431
|
-
: "Local edits can be staged as a Builder save revision/autosave record. Live Builder writes are disabled."
|
|
6432
|
-
: "No local outbound push lane is active for this mock source."}
|
|
8126
|
+
{selectedSource.capabilities.liveWritesEnabled
|
|
8127
|
+
? "Local edits can be reviewed and sent through the guarded Builder autosave path."
|
|
8128
|
+
: "Local edits can be staged as a Builder save revision/autosave record. Live Builder writes are disabled."}
|
|
6433
8129
|
</div>
|
|
6434
8130
|
<div className="grid min-w-0 gap-2">
|
|
6435
8131
|
{outboundChangeSets.slice(0, 6).map((changeSet) => (
|
|
6436
8132
|
<SourceChangeSetReviewCard
|
|
6437
8133
|
key={changeSet.id}
|
|
6438
8134
|
changeSet={changeSet}
|
|
6439
|
-
source={
|
|
8135
|
+
source={selectedSource}
|
|
6440
8136
|
/>
|
|
6441
8137
|
))}
|
|
6442
8138
|
{outboundChangeSets.length === 0 ? (
|
|
6443
8139
|
<div className="text-xs text-muted-foreground">
|
|
6444
|
-
{
|
|
6445
|
-
? "No pending local Builder changes yet. Rename a source-backed row to see a local outbound diff."
|
|
6446
|
-
: "No local outbound changes yet."}
|
|
8140
|
+
{dbText("noPendingChangesEditASourceBackedRow")}
|
|
6447
8141
|
</div>
|
|
6448
8142
|
) : null}
|
|
6449
8143
|
</div>
|
|
@@ -6452,33 +8146,33 @@ function DatabaseSettingsSourcePanel({
|
|
|
6452
8146
|
) : null}
|
|
6453
8147
|
|
|
6454
8148
|
<SourceRoleCard
|
|
6455
|
-
source={
|
|
8149
|
+
source={selectedSource}
|
|
6456
8150
|
canAddDetails={sources.some(
|
|
6457
|
-
(item) => item.id !==
|
|
8151
|
+
(item) => item.id !== selectedSource.id && !sourceAddsDetails(item),
|
|
6458
8152
|
)}
|
|
6459
8153
|
canEdit={canEdit}
|
|
6460
|
-
pending={
|
|
8154
|
+
pending={selectedSourceRoleControlPending}
|
|
6461
8155
|
onAddDetails={() =>
|
|
6462
8156
|
onNavPush({
|
|
6463
8157
|
kind: "keyConfirm",
|
|
6464
8158
|
candidate: {
|
|
6465
|
-
sourceType:
|
|
6466
|
-
sourceName:
|
|
6467
|
-
sourceTable:
|
|
6468
|
-
displayName:
|
|
6469
|
-
existingSourceId:
|
|
8159
|
+
sourceType: selectedSource.sourceType,
|
|
8160
|
+
sourceName: selectedSource.sourceName,
|
|
8161
|
+
sourceTable: selectedSource.sourceTable,
|
|
8162
|
+
displayName: selectedSource.sourceName,
|
|
8163
|
+
existingSourceId: selectedSource.id,
|
|
6470
8164
|
},
|
|
6471
8165
|
})
|
|
6472
8166
|
}
|
|
6473
8167
|
onAddItems={async () => {
|
|
6474
|
-
await onChangeSourceRole(
|
|
8168
|
+
await onChangeSourceRole(selectedSource.id, "items");
|
|
6475
8169
|
onNavReplace([]);
|
|
6476
8170
|
}}
|
|
6477
8171
|
onChooseFields={() =>
|
|
6478
8172
|
onNavPush({
|
|
6479
8173
|
kind: "fieldPicker",
|
|
6480
|
-
sourceId:
|
|
6481
|
-
sourceName:
|
|
8174
|
+
sourceId: selectedSource.id,
|
|
8175
|
+
sourceName: selectedSource.sourceName,
|
|
6482
8176
|
})
|
|
6483
8177
|
}
|
|
6484
8178
|
/>
|
|
@@ -6495,10 +8189,10 @@ function DatabaseSettingsSourcePanel({
|
|
|
6495
8189
|
size="sm"
|
|
6496
8190
|
variant="outline"
|
|
6497
8191
|
className="mt-2 h-8 text-xs text-destructive hover:text-destructive"
|
|
6498
|
-
disabled={!canEdit ||
|
|
6499
|
-
onClick={() => onDisconnectSource(
|
|
8192
|
+
disabled={!canEdit || selectedSourceDisconnectControlPending}
|
|
8193
|
+
onClick={() => onDisconnectSource(selectedSource.id)}
|
|
6500
8194
|
>
|
|
6501
|
-
{
|
|
8195
|
+
{selectedSourceDisconnectControlPending ? (
|
|
6502
8196
|
<Spinner className="mr-1 size-3.5" />
|
|
6503
8197
|
) : (
|
|
6504
8198
|
<IconX className="mr-1 size-3.5" />
|
|
@@ -6529,7 +8223,7 @@ function SourcesListView({
|
|
|
6529
8223
|
builderConfigured: boolean;
|
|
6530
8224
|
builderSpaceLabel: string | null;
|
|
6531
8225
|
reviewableCount: number;
|
|
6532
|
-
onOpenBuilder: () => void;
|
|
8226
|
+
onOpenBuilder: (source?: ContentDatabaseSource) => void;
|
|
6533
8227
|
onOpenNotion: () => void;
|
|
6534
8228
|
onOpenSecondary: (source: ContentDatabaseSource) => void;
|
|
6535
8229
|
onAddSource: () => void;
|
|
@@ -6564,7 +8258,7 @@ function SourcesListView({
|
|
|
6564
8258
|
connected.metadata.federation?.role === "secondary"
|
|
6565
8259
|
? () => onOpenSecondary(connected)
|
|
6566
8260
|
: connected.sourceType === "builder-cms"
|
|
6567
|
-
? onOpenBuilder
|
|
8261
|
+
? () => onOpenBuilder(connected)
|
|
6568
8262
|
: undefined
|
|
6569
8263
|
}
|
|
6570
8264
|
disabled={
|
|
@@ -7194,12 +8888,7 @@ function BuilderSourceContinuationBar({
|
|
|
7194
8888
|
canEdit: boolean;
|
|
7195
8889
|
pending: boolean;
|
|
7196
8890
|
clientError: boolean;
|
|
7197
|
-
progressHighWater
|
|
7198
|
-
sourceId: string | null;
|
|
7199
|
-
fetchedCount: number;
|
|
7200
|
-
hydratedCount: number;
|
|
7201
|
-
rowsComplete: boolean;
|
|
7202
|
-
};
|
|
8891
|
+
progressHighWater?: BuilderSourceProgressHighWater;
|
|
7203
8892
|
onRetry: () => void;
|
|
7204
8893
|
}) {
|
|
7205
8894
|
if (
|
|
@@ -7211,8 +8900,7 @@ function BuilderSourceContinuationBar({
|
|
|
7211
8900
|
}
|
|
7212
8901
|
const rowsComplete =
|
|
7213
8902
|
source.metadata.lastReadHasMore === false ||
|
|
7214
|
-
|
|
7215
|
-
progressHighWater.rowsComplete);
|
|
8903
|
+
progressHighWater?.rowsComplete === true;
|
|
7216
8904
|
const rawStatus = clientError ? "error" : builderSourceRowFetchStatus(source);
|
|
7217
8905
|
const status = rowsComplete && rawStatus === "fetching" ? null : rawStatus;
|
|
7218
8906
|
const bodyHydration = source.bodyHydration;
|
|
@@ -7225,10 +8913,7 @@ function BuilderSourceContinuationBar({
|
|
|
7225
8913
|
const hydratedCount = bodyHydration
|
|
7226
8914
|
? builderBodyHydrationDisplayHydratedCount({
|
|
7227
8915
|
summary: bodyHydration,
|
|
7228
|
-
highWaterCount:
|
|
7229
|
-
progressHighWater.sourceId === source.id
|
|
7230
|
-
? progressHighWater.hydratedCount
|
|
7231
|
-
: 0,
|
|
8916
|
+
highWaterCount: progressHighWater?.hydratedCount ?? 0,
|
|
7232
8917
|
})
|
|
7233
8918
|
: 0;
|
|
7234
8919
|
const fetchedCount =
|
|
@@ -7236,9 +8921,7 @@ function BuilderSourceContinuationBar({
|
|
|
7236
8921
|
? Math.max(
|
|
7237
8922
|
rawFetchedCount ?? 0,
|
|
7238
8923
|
bodyHydration.total,
|
|
7239
|
-
progressHighWater
|
|
7240
|
-
? progressHighWater.fetchedCount
|
|
7241
|
-
: 0,
|
|
8924
|
+
progressHighWater?.fetchedCount ?? 0,
|
|
7242
8925
|
)
|
|
7243
8926
|
: rawFetchedCount;
|
|
7244
8927
|
const hasMore = source.metadata.lastReadHasMore === true && !rowsComplete;
|
|
@@ -7485,6 +9168,105 @@ function sourceFieldIconType(
|
|
|
7485
9168
|
return "text";
|
|
7486
9169
|
}
|
|
7487
9170
|
|
|
9171
|
+
export function builderMissingRequiredFields(
|
|
9172
|
+
source: ContentDatabaseSource | null | undefined,
|
|
9173
|
+
) {
|
|
9174
|
+
if (
|
|
9175
|
+
source?.sourceType !== "builder-cms" ||
|
|
9176
|
+
source.sourceTable !== "agent-native-blog-article-test"
|
|
9177
|
+
) {
|
|
9178
|
+
return [];
|
|
9179
|
+
}
|
|
9180
|
+
|
|
9181
|
+
const requiredFieldKeys = new Set(
|
|
9182
|
+
(source.metadata.builderModelFields ?? [])
|
|
9183
|
+
.filter(
|
|
9184
|
+
(field) =>
|
|
9185
|
+
field.required &&
|
|
9186
|
+
field.name !== "title" &&
|
|
9187
|
+
field.name !== "blocks" &&
|
|
9188
|
+
field.name !== "blocksString",
|
|
9189
|
+
)
|
|
9190
|
+
.map((field) => `data.${field.name}`),
|
|
9191
|
+
);
|
|
9192
|
+
|
|
9193
|
+
return source.fields.filter(
|
|
9194
|
+
(field) =>
|
|
9195
|
+
!field.propertyId &&
|
|
9196
|
+
field.mappingType !== "system" &&
|
|
9197
|
+
field.mappingType !== "title" &&
|
|
9198
|
+
requiredFieldKeys.has(field.sourceFieldKey),
|
|
9199
|
+
);
|
|
9200
|
+
}
|
|
9201
|
+
|
|
9202
|
+
export function BuilderRequiredFieldsCard({
|
|
9203
|
+
documentId,
|
|
9204
|
+
source,
|
|
9205
|
+
canEdit,
|
|
9206
|
+
pending,
|
|
9207
|
+
onDone,
|
|
9208
|
+
}: {
|
|
9209
|
+
documentId: string;
|
|
9210
|
+
source: ContentDatabaseSource;
|
|
9211
|
+
canEdit: boolean;
|
|
9212
|
+
pending: boolean;
|
|
9213
|
+
onDone?: () => void;
|
|
9214
|
+
}) {
|
|
9215
|
+
const addRequiredFields = useMaterializeBuilderRequiredFields(documentId);
|
|
9216
|
+
const missingRequiredFields = builderMissingRequiredFields(source);
|
|
9217
|
+
|
|
9218
|
+
if (missingRequiredFields.length === 0) return null;
|
|
9219
|
+
|
|
9220
|
+
const addRequired = async () => {
|
|
9221
|
+
try {
|
|
9222
|
+
await addRequiredFields.mutateAsync({
|
|
9223
|
+
documentId,
|
|
9224
|
+
sourceId: source.id,
|
|
9225
|
+
});
|
|
9226
|
+
toast.success(dbText("publishingFieldsAdded"), {
|
|
9227
|
+
description: dbText("requiredBuilderFieldsReady", {
|
|
9228
|
+
count: missingRequiredFields.length,
|
|
9229
|
+
}),
|
|
9230
|
+
});
|
|
9231
|
+
onDone?.();
|
|
9232
|
+
} catch (error) {
|
|
9233
|
+
toast.error(dbText("fieldsWereNotAdded"), {
|
|
9234
|
+
description:
|
|
9235
|
+
error instanceof Error ? error.message : dbText("tryAgain"),
|
|
9236
|
+
});
|
|
9237
|
+
}
|
|
9238
|
+
};
|
|
9239
|
+
|
|
9240
|
+
return (
|
|
9241
|
+
<div className="grid gap-2 rounded-lg border border-border bg-muted/30 p-3">
|
|
9242
|
+
<div className="text-sm font-medium">
|
|
9243
|
+
{dbText("requiredPublishingFields")}
|
|
9244
|
+
</div>
|
|
9245
|
+
<div className="text-xs text-muted-foreground">
|
|
9246
|
+
{dbText("requiredPublishingFieldsDescription", {
|
|
9247
|
+
fields: missingRequiredFields
|
|
9248
|
+
.map((field) => field.sourceFieldLabel)
|
|
9249
|
+
.join(", "),
|
|
9250
|
+
})}
|
|
9251
|
+
</div>
|
|
9252
|
+
<Button
|
|
9253
|
+
type="button"
|
|
9254
|
+
size="sm"
|
|
9255
|
+
className="justify-self-start"
|
|
9256
|
+
disabled={!canEdit || pending || addRequiredFields.isPending}
|
|
9257
|
+
onClick={addRequired}
|
|
9258
|
+
>
|
|
9259
|
+
{addRequiredFields.isPending ? (
|
|
9260
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
9261
|
+
) : (
|
|
9262
|
+
<IconPlus className="mr-1.5 size-3.5" />
|
|
9263
|
+
)}
|
|
9264
|
+
{dbText("addRequiredFields")}
|
|
9265
|
+
</Button>
|
|
9266
|
+
</div>
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
|
|
7488
9270
|
function SourceDetailsFieldPicker({
|
|
7489
9271
|
documentId,
|
|
7490
9272
|
source,
|
|
@@ -7569,7 +9351,6 @@ function SourceDetailsFieldPicker({
|
|
|
7569
9351
|
});
|
|
7570
9352
|
}
|
|
7571
9353
|
};
|
|
7572
|
-
|
|
7573
9354
|
return (
|
|
7574
9355
|
<div className="grid min-w-0 gap-3">
|
|
7575
9356
|
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3">
|
|
@@ -7580,6 +9361,13 @@ function SourceDetailsFieldPicker({
|
|
|
7580
9361
|
{dbText("pickDetailsBecomeColumns")}
|
|
7581
9362
|
</div>
|
|
7582
9363
|
</div>
|
|
9364
|
+
<BuilderRequiredFieldsCard
|
|
9365
|
+
documentId={documentId}
|
|
9366
|
+
source={source}
|
|
9367
|
+
canEdit={canEdit}
|
|
9368
|
+
pending={pending}
|
|
9369
|
+
onDone={onDone}
|
|
9370
|
+
/>
|
|
7583
9371
|
{fields.length === 0 ? (
|
|
7584
9372
|
<div className="break-words text-xs text-muted-foreground">
|
|
7585
9373
|
{dbText("allAvailableDetailFieldsAlreadyVisible")}
|
|
@@ -7650,10 +9438,10 @@ function SourceDetailsFieldPicker({
|
|
|
7650
9438
|
// A Builder space's data models, as drill-in rows. The attached model (if any)
|
|
7651
9439
|
// is marked; selecting a row opens that model's leaf.
|
|
7652
9440
|
function BuilderSpaceModelsView({
|
|
7653
|
-
|
|
9441
|
+
attachedModelNames,
|
|
7654
9442
|
onOpenModel,
|
|
7655
9443
|
}: {
|
|
7656
|
-
|
|
9444
|
+
attachedModelNames: string[];
|
|
7657
9445
|
onOpenModel: (model: BuilderCmsModelSummary) => void;
|
|
7658
9446
|
}) {
|
|
7659
9447
|
const modelsQuery = useBuilderCmsModels(true);
|
|
@@ -7661,22 +9449,27 @@ function BuilderSpaceModelsView({
|
|
|
7661
9449
|
const [query, setQuery] = useState("");
|
|
7662
9450
|
|
|
7663
9451
|
if (modelsQuery.isLoading) {
|
|
7664
|
-
if (
|
|
9452
|
+
if (attachedModelNames.length > 0) {
|
|
7665
9453
|
return (
|
|
7666
9454
|
<div className="grid min-w-0 gap-2">
|
|
7667
9455
|
<div className="grid min-w-0 gap-1.5">
|
|
7668
9456
|
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
7669
9457
|
{dbText("alreadyAttached")}
|
|
7670
9458
|
</div>
|
|
7671
|
-
|
|
7672
|
-
<
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
9459
|
+
{attachedModelNames.map((attachedModelName) => (
|
|
9460
|
+
<div
|
|
9461
|
+
key={attachedModelName}
|
|
9462
|
+
className="flex min-w-0 items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-sm"
|
|
9463
|
+
>
|
|
9464
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
9465
|
+
<IconLayoutGrid className="size-4 shrink-0 text-muted-foreground" />
|
|
9466
|
+
<span className="min-w-0 truncate">{attachedModelName}</span>
|
|
9467
|
+
</span>
|
|
9468
|
+
<span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
9469
|
+
attached
|
|
9470
|
+
</span>
|
|
9471
|
+
</div>
|
|
9472
|
+
))}
|
|
7680
9473
|
</div>
|
|
7681
9474
|
<div className="flex min-w-0 items-center gap-2 px-2 text-xs text-muted-foreground">
|
|
7682
9475
|
<Spinner className="size-3.5" />
|
|
@@ -7750,15 +9543,16 @@ function BuilderSpaceModelsView({
|
|
|
7750
9543
|
model.displayName.toLowerCase().includes(normalizedQuery) ||
|
|
7751
9544
|
model.name.toLowerCase().includes(normalizedQuery);
|
|
7752
9545
|
const filtered = models.filter(matchesQuery);
|
|
7753
|
-
const
|
|
7754
|
-
|
|
9546
|
+
const attachedModelNameSet = new Set(attachedModelNames);
|
|
9547
|
+
const attachedModels = filtered.filter((model) =>
|
|
9548
|
+
attachedModelNameSet.has(model.name),
|
|
7755
9549
|
);
|
|
7756
9550
|
const otherModels = filtered.filter(
|
|
7757
|
-
(model) =>
|
|
9551
|
+
(model) => !attachedModelNameSet.has(model.name),
|
|
7758
9552
|
);
|
|
7759
9553
|
|
|
7760
9554
|
const renderRow = (model: BuilderCmsModelSummary) => {
|
|
7761
|
-
const isAttached =
|
|
9555
|
+
const isAttached = attachedModelNameSet.has(model.name);
|
|
7762
9556
|
return (
|
|
7763
9557
|
<button
|
|
7764
9558
|
key={model.id}
|
|
@@ -12129,8 +13923,10 @@ export function databaseBulkMultiSelectFilteredOptions(
|
|
|
12129
13923
|
) {
|
|
12130
13924
|
const normalizedQuery = query.trim().toLowerCase();
|
|
12131
13925
|
if (!normalizedQuery) return options;
|
|
12132
|
-
return options.filter(
|
|
12133
|
-
option
|
|
13926
|
+
return options.filter(
|
|
13927
|
+
(option) =>
|
|
13928
|
+
option.name.trim().toLowerCase().includes(normalizedQuery) ||
|
|
13929
|
+
option.description?.trim().toLowerCase().includes(normalizedQuery),
|
|
12134
13930
|
);
|
|
12135
13931
|
}
|
|
12136
13932
|
|
|
@@ -13511,7 +15307,14 @@ function DatabaseBulkOptionValueEditor({
|
|
|
13511
15307
|
)
|
|
13512
15308
|
}
|
|
13513
15309
|
>
|
|
13514
|
-
<
|
|
15310
|
+
<span className="min-w-0 flex-1">
|
|
15311
|
+
<DatabaseBulkOptionPill option={option} />
|
|
15312
|
+
{option.description ? (
|
|
15313
|
+
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
15314
|
+
{option.description}
|
|
15315
|
+
</span>
|
|
15316
|
+
) : null}
|
|
15317
|
+
</span>
|
|
13515
15318
|
{presence.presentInAll ? (
|
|
13516
15319
|
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
|
|
13517
15320
|
) : presence.presentInAny ? (
|
|
@@ -13560,7 +15363,14 @@ function DatabaseBulkOptionValueEditor({
|
|
|
13560
15363
|
disabled={disabled}
|
|
13561
15364
|
onClick={() => void onApply({ kind: "set", value: option.id })}
|
|
13562
15365
|
>
|
|
13563
|
-
<
|
|
15366
|
+
<span className="min-w-0 flex-1">
|
|
15367
|
+
<DatabaseBulkOptionPill option={option} />
|
|
15368
|
+
{option.description ? (
|
|
15369
|
+
<span className="mt-0.5 block truncate text-xs text-muted-foreground">
|
|
15370
|
+
{option.description}
|
|
15371
|
+
</span>
|
|
15372
|
+
) : null}
|
|
15373
|
+
</span>
|
|
13564
15374
|
</button>
|
|
13565
15375
|
))}
|
|
13566
15376
|
</div>
|
|
@@ -15858,6 +17668,24 @@ function DatabaseGroupedTableSection({
|
|
|
15858
17668
|
);
|
|
15859
17669
|
}
|
|
15860
17670
|
|
|
17671
|
+
export function databaseItemPropertyForColumn(
|
|
17672
|
+
item: ContentDatabaseItem,
|
|
17673
|
+
columnProperty: DocumentProperty,
|
|
17674
|
+
): DocumentProperty {
|
|
17675
|
+
const itemProperty = item.properties.find(
|
|
17676
|
+
(candidate) => candidate.definition.id === columnProperty.definition.id,
|
|
17677
|
+
);
|
|
17678
|
+
if (!itemProperty) return columnProperty;
|
|
17679
|
+
|
|
17680
|
+
// Row payloads own values, not schema meaning. Always pair the row's value
|
|
17681
|
+
// with the database's current canonical definition so a recently described
|
|
17682
|
+
// option cannot remain invisible in an older row snapshot.
|
|
17683
|
+
return {
|
|
17684
|
+
...itemProperty,
|
|
17685
|
+
definition: columnProperty.definition,
|
|
17686
|
+
};
|
|
17687
|
+
}
|
|
17688
|
+
|
|
15861
17689
|
function DatabaseTableRow({
|
|
15862
17690
|
item,
|
|
15863
17691
|
properties,
|
|
@@ -15942,10 +17770,7 @@ function DatabaseTableRow({
|
|
|
15942
17770
|
onPreview={onPreview}
|
|
15943
17771
|
/>
|
|
15944
17772
|
{properties.map((property) => {
|
|
15945
|
-
const itemProperty =
|
|
15946
|
-
item.properties.find(
|
|
15947
|
-
(candidate) => candidate.definition.id === property.definition.id,
|
|
15948
|
-
) ?? property;
|
|
17773
|
+
const itemProperty = databaseItemPropertyForColumn(item, property);
|
|
15949
17774
|
const bodyCellHydrationPending =
|
|
15950
17775
|
itemProperty.definition.type === "blocks" &&
|
|
15951
17776
|
databaseItemBodyHydrationIsPending(item);
|