@agent-native/core 0.101.7 → 0.101.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +22 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
- package/corpus/templates/content/app/i18n-data.ts +287 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
|
@@ -49,7 +49,6 @@ import {
|
|
|
49
49
|
TooltipTrigger,
|
|
50
50
|
} from "@agent-native/toolkit/ui/tooltip";
|
|
51
51
|
import {
|
|
52
|
-
BUILDER_CMS_SAFE_WRITE_MODEL,
|
|
53
52
|
type BuilderCmsModelSummary,
|
|
54
53
|
type ContentDatabaseItem,
|
|
55
54
|
type ContentDatabaseResponse,
|
|
@@ -57,6 +56,8 @@ import {
|
|
|
57
56
|
type ContentDatabaseSourceChangeSet,
|
|
58
57
|
type ContentDatabaseSourceJoinRequest,
|
|
59
58
|
type ContentDatabaseSourceReviewPayload,
|
|
59
|
+
type ContentDatabaseSourceWriteMode,
|
|
60
|
+
type ProcessBuilderBodyHydrationResponse,
|
|
60
61
|
type SourceJoinSuggestion,
|
|
61
62
|
type ContentDatabasePersonalViewOverrides,
|
|
62
63
|
type ContentDatabaseView,
|
|
@@ -132,6 +133,7 @@ import {
|
|
|
132
133
|
import { useQueryClient } from "@tanstack/react-query";
|
|
133
134
|
import {
|
|
134
135
|
useEffect,
|
|
136
|
+
useCallback,
|
|
135
137
|
useMemo,
|
|
136
138
|
useRef,
|
|
137
139
|
useState,
|
|
@@ -149,6 +151,7 @@ import {
|
|
|
149
151
|
useAddContentDatabaseSourceFieldProperty,
|
|
150
152
|
useAttachContentDatabaseSource,
|
|
151
153
|
useBuilderCmsModels,
|
|
154
|
+
useCancelPreparedBuilderSourceUpdate,
|
|
152
155
|
useChangeContentDatabaseSourceRole,
|
|
153
156
|
useContentDatabase,
|
|
154
157
|
useContentDatabasePersonalView,
|
|
@@ -160,8 +163,10 @@ import {
|
|
|
160
163
|
useDuplicateDatabaseItems,
|
|
161
164
|
useExecuteBuilderSourceExecution,
|
|
162
165
|
useMoveDatabaseItem,
|
|
166
|
+
useMaterializeBuilderRequiredFields,
|
|
163
167
|
useNotionDatabaseSources,
|
|
164
168
|
usePrepareBuilderSourceReview,
|
|
169
|
+
usePreviewBuilderSourceReview,
|
|
165
170
|
useProcessBuilderBodyHydration,
|
|
166
171
|
useRefreshContentDatabaseSource,
|
|
167
172
|
useSetContentDatabaseSourceWriteMode,
|
|
@@ -174,9 +179,12 @@ import {
|
|
|
174
179
|
useSetDocumentProperty,
|
|
175
180
|
} from "@/hooks/use-document-properties";
|
|
176
181
|
import {
|
|
182
|
+
isDocumentUpdateConflict,
|
|
177
183
|
useDeleteDocument,
|
|
178
184
|
useDocument,
|
|
179
185
|
seedDatabaseItemDocumentCaches,
|
|
186
|
+
usePreviewDocumentDraft,
|
|
187
|
+
useUpdatePreviewDocumentDraft,
|
|
180
188
|
useUpdateDocument,
|
|
181
189
|
} from "@/hooks/use-documents";
|
|
182
190
|
import { messagesByLocale } from "@/i18n-data";
|
|
@@ -189,6 +197,7 @@ import {
|
|
|
189
197
|
isEffectivelyEmptyDocumentContent,
|
|
190
198
|
previewBodyHydrationIsPending,
|
|
191
199
|
previewBodyHydrationIsTerminalError,
|
|
200
|
+
previewDraftConflictsWithHydratedBody,
|
|
192
201
|
shouldIgnorePreviewEmptyNormalization,
|
|
193
202
|
} from "../body-hydration";
|
|
194
203
|
import {
|
|
@@ -227,7 +236,8 @@ import {
|
|
|
227
236
|
import { EmojiPicker } from "../EmojiPicker";
|
|
228
237
|
import {
|
|
229
238
|
createPreviewDocumentSaveController,
|
|
230
|
-
|
|
239
|
+
deferredPreviewDocumentSave,
|
|
240
|
+
type PreviewDocumentSaveAdapter,
|
|
231
241
|
} from "../previewDocumentSaveController";
|
|
232
242
|
import {
|
|
233
243
|
acquirePreviewDocumentSaveController,
|
|
@@ -252,6 +262,26 @@ export interface DatabaseViewProps {
|
|
|
252
262
|
const CONTENT_DATABASE_PAGE_SIZE = 100;
|
|
253
263
|
const CONTENT_DATABASE_MAX_ITEM_LIMIT = 5_000;
|
|
254
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
|
+
|
|
255
285
|
export type SortDirection = ContentDatabaseSortDirection;
|
|
256
286
|
export type DatabaseSort = ContentDatabaseSort;
|
|
257
287
|
export type FilterOperator = ContentDatabaseFilterOperator;
|
|
@@ -291,6 +321,32 @@ export const BUILDER_SOURCE_CONTINUATION_MAX_BACKOFF_MS = 30_000;
|
|
|
291
321
|
export type PersonalDatabaseViewOverrides =
|
|
292
322
|
ContentDatabasePersonalViewOverrides;
|
|
293
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
|
+
|
|
294
350
|
type DatabaseMessageKey = keyof (typeof messagesByLocale)["en-US"]["database"];
|
|
295
351
|
|
|
296
352
|
export function dbText(
|
|
@@ -315,6 +371,96 @@ export function dbText(
|
|
|
315
371
|
export type CreateDatabaseRowHandler = (
|
|
316
372
|
title?: string,
|
|
317
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
|
+
|
|
318
464
|
type DatabaseDragPreviewState =
|
|
319
465
|
| {
|
|
320
466
|
kind: "view";
|
|
@@ -531,6 +677,9 @@ function DatabaseTable({
|
|
|
531
677
|
const processBuilderBodies = useProcessBuilderBodyHydration(document.id);
|
|
532
678
|
const prepareBuilderReview = usePrepareBuilderSourceReview(document.id);
|
|
533
679
|
const executeBuilderExecution = useExecuteBuilderSourceExecution(document.id);
|
|
680
|
+
const cancelPreparedBuilderUpdate = useCancelPreparedBuilderSourceUpdate(
|
|
681
|
+
document.id,
|
|
682
|
+
);
|
|
534
683
|
const setSourceWriteMode = useSetContentDatabaseSourceWriteMode(document.id);
|
|
535
684
|
const setProperty = useSetDocumentProperty(document.id, document.id);
|
|
536
685
|
const updateDatabaseDocument = useUpdateDocument();
|
|
@@ -554,10 +703,19 @@ function DatabaseTable({
|
|
|
554
703
|
const personalView = useContentDatabasePersonalView(databaseId);
|
|
555
704
|
const updatePersonalView = useUpdateContentDatabasePersonalView(databaseId);
|
|
556
705
|
const source = data?.source ?? null;
|
|
557
|
-
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
|
+
);
|
|
558
714
|
const [previewDocumentId, setPreviewDocumentId] = useState<string | null>(
|
|
559
715
|
null,
|
|
560
716
|
);
|
|
717
|
+
const [createdPreviewItem, setCreatedPreviewItem] =
|
|
718
|
+
useState<ContentDatabaseItem | null>(null);
|
|
561
719
|
const [selectedItemIds, setSelectedItemIds] = useState<string[]>([]);
|
|
562
720
|
const [previewTitleFocusDocumentId, setPreviewTitleFocusDocumentId] =
|
|
563
721
|
useState<string | null>(null);
|
|
@@ -566,6 +724,18 @@ function DatabaseTable({
|
|
|
566
724
|
>(null);
|
|
567
725
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
568
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;
|
|
569
739
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
570
740
|
const [inlineFilterControlsOpen, setInlineFilterControlsOpen] =
|
|
571
741
|
useState(false);
|
|
@@ -580,11 +750,28 @@ function DatabaseTable({
|
|
|
580
750
|
null,
|
|
581
751
|
);
|
|
582
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);
|
|
583
761
|
const [builderReviewResult, setBuilderReviewResult] =
|
|
584
762
|
useState<ContentDatabaseSourceReviewPayload | null>(null);
|
|
585
763
|
const [builderReviewCheckedAt, setBuilderReviewCheckedAt] = useState<
|
|
586
764
|
string | null
|
|
587
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);
|
|
588
775
|
const [settingsPanel, setSettingsPanel] =
|
|
589
776
|
useState<DatabaseSettingsPanel>("main");
|
|
590
777
|
const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
|
|
@@ -629,29 +816,22 @@ function DatabaseTable({
|
|
|
629
816
|
const personalViewSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
630
817
|
null,
|
|
631
818
|
);
|
|
632
|
-
const autoContinueBuilderSourceRef = useRef<string
|
|
633
|
-
const builderContinuationWatchdogRef = useRef<
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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);
|
|
637
825
|
const [
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
] = useState<string
|
|
641
|
-
const autoPumpBuilderBodiesRef = useRef<string
|
|
642
|
-
const [
|
|
643
|
-
useState<string
|
|
644
|
-
const [builderProgressHighWater, setBuilderProgressHighWater] = useState<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
hydratedCount: number;
|
|
648
|
-
rowsComplete: boolean;
|
|
649
|
-
}>({
|
|
650
|
-
sourceId: null,
|
|
651
|
-
fetchedCount: 0,
|
|
652
|
-
hydratedCount: 0,
|
|
653
|
-
rowsComplete: false,
|
|
654
|
-
});
|
|
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
|
+
>({});
|
|
655
835
|
const previewStateRef = useRef<{
|
|
656
836
|
documentId: string | null;
|
|
657
837
|
visibleItems: ContentDatabaseItem[];
|
|
@@ -708,8 +888,11 @@ function DatabaseTable({
|
|
|
708
888
|
activeFilters.length === 0 &&
|
|
709
889
|
!databaseGroupProperty;
|
|
710
890
|
const hasResultConstraints = !!searchQuery.trim() || activeFilters.length > 0;
|
|
711
|
-
const previewItem =
|
|
712
|
-
items
|
|
891
|
+
const previewItem = databasePreviewItem(
|
|
892
|
+
items,
|
|
893
|
+
previewDocumentId,
|
|
894
|
+
createdPreviewItem,
|
|
895
|
+
);
|
|
713
896
|
const previousPreviewItem = previewItem
|
|
714
897
|
? databaseItemPreviewNeighbor(
|
|
715
898
|
screenVisibleItems,
|
|
@@ -731,18 +914,136 @@ function DatabaseTable({
|
|
|
731
914
|
() => databaseSelectedItems(visibleItems, selectedItemIds),
|
|
732
915
|
[visibleItems, selectedItemIds],
|
|
733
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;
|
|
734
937
|
const builderReviewChangeSets = useMemo(
|
|
735
|
-
() => builderReviewableChangeSets(
|
|
736
|
-
[
|
|
938
|
+
() => builderReviewableChangeSets(builderReviewSource),
|
|
939
|
+
[builderReviewSource],
|
|
737
940
|
);
|
|
738
941
|
const builderReviewPreview = useMemo(
|
|
739
942
|
() =>
|
|
740
|
-
|
|
741
|
-
? buildClientBuilderReviewPayload(
|
|
943
|
+
builderReviewSource && builderReviewChangeSets.length > 0
|
|
944
|
+
? buildClientBuilderReviewPayload(
|
|
945
|
+
builderReviewSource,
|
|
946
|
+
builderReviewChangeSets,
|
|
947
|
+
)
|
|
742
948
|
: null,
|
|
743
|
-
[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],
|
|
744
1046
|
);
|
|
745
|
-
const activeBuilderReview = builderReviewResult ?? builderReviewPreview;
|
|
746
1047
|
|
|
747
1048
|
useEffect(() => {
|
|
748
1049
|
previewStateRef.current = {
|
|
@@ -758,191 +1059,125 @@ function DatabaseTable({
|
|
|
758
1059
|
}, [visibleItems]);
|
|
759
1060
|
|
|
760
1061
|
useEffect(() => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
sourceId: null,
|
|
766
|
-
fetchedCount: 0,
|
|
767
|
-
hydratedCount: 0,
|
|
768
|
-
rowsComplete: false,
|
|
769
|
-
});
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const rawFetchedCount =
|
|
773
|
-
typeof source.metadata.lastReadFetchedEntryCount === "number"
|
|
774
|
-
? source.metadata.lastReadFetchedEntryCount
|
|
775
|
-
: typeof source.metadata.lastReadEntryCount === "number"
|
|
776
|
-
? source.metadata.lastReadEntryCount
|
|
777
|
-
: 0;
|
|
778
|
-
const fetchedCount =
|
|
779
|
-
source.metadata.lastReadHasMore === false
|
|
780
|
-
? Math.max(rawFetchedCount, source.bodyHydration?.total ?? 0)
|
|
781
|
-
: rawFetchedCount;
|
|
782
|
-
const hydratedCount = source.bodyHydration?.hydrated ?? 0;
|
|
783
|
-
const rowsComplete = source.metadata.lastReadHasMore === false;
|
|
784
|
-
setBuilderProgressHighWater((current) => {
|
|
785
|
-
if (current.sourceId !== source.id) {
|
|
786
|
-
return {
|
|
787
|
-
sourceId: source.id,
|
|
788
|
-
fetchedCount,
|
|
789
|
-
hydratedCount,
|
|
790
|
-
rowsComplete,
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
const next = {
|
|
794
|
-
sourceId: source.id,
|
|
795
|
-
fetchedCount: Math.max(current.fetchedCount, fetchedCount),
|
|
796
|
-
hydratedCount: Math.max(current.hydratedCount, hydratedCount),
|
|
797
|
-
rowsComplete: current.rowsComplete || rowsComplete,
|
|
798
|
-
};
|
|
799
|
-
return next.fetchedCount === current.fetchedCount &&
|
|
800
|
-
next.hydratedCount === current.hydratedCount &&
|
|
801
|
-
next.rowsComplete === current.rowsComplete
|
|
802
|
-
? current
|
|
803
|
-
: next;
|
|
804
|
-
});
|
|
805
|
-
}, [source]);
|
|
1062
|
+
setBuilderProgressHighWater((current) =>
|
|
1063
|
+
databaseBuilderProgressHighWater(builderSources, current),
|
|
1064
|
+
);
|
|
1065
|
+
}, [builderSources]);
|
|
806
1066
|
|
|
807
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
|
+
);
|
|
808
1080
|
if (
|
|
809
|
-
!
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
refreshSource.isPending
|
|
815
|
-
) {
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const sourceStatus = builderSourceRowFetchStatus(source);
|
|
819
|
-
if (
|
|
820
|
-
sourceStatus !== "fetching" ||
|
|
821
|
-
source.metadata.lastReadHasMore !== true
|
|
1081
|
+
!runSourceRefresh(candidate.id, () =>
|
|
1082
|
+
setBuilderContinuationClientErrorKeys((current) =>
|
|
1083
|
+
addUniqueKey(current, continuationKey),
|
|
1084
|
+
),
|
|
1085
|
+
)
|
|
822
1086
|
) {
|
|
823
|
-
|
|
1087
|
+
autoContinueBuilderSourceRef.current.delete(continuationKey);
|
|
824
1088
|
}
|
|
825
|
-
const continuationKey = builderSourceContinuationKey(source);
|
|
826
|
-
if (!continuationKey) return;
|
|
827
|
-
if (builderContinuationClientErrorKey === continuationKey) return;
|
|
828
|
-
if (autoContinueBuilderSourceRef.current === continuationKey) return;
|
|
829
|
-
autoContinueBuilderSourceRef.current = continuationKey;
|
|
830
|
-
refreshSource.mutate(
|
|
831
|
-
{
|
|
832
|
-
documentId: document.id,
|
|
833
|
-
sourceId: source.id,
|
|
834
|
-
},
|
|
835
|
-
{
|
|
836
|
-
onError: () => setBuilderContinuationClientErrorKey(continuationKey),
|
|
837
|
-
},
|
|
838
|
-
);
|
|
839
1089
|
}, [
|
|
840
|
-
|
|
1090
|
+
builderContinuationClientErrorKeys,
|
|
1091
|
+
builderSources,
|
|
841
1092
|
canEdit,
|
|
842
|
-
document.id,
|
|
843
1093
|
isActive,
|
|
844
|
-
refreshSource.mutate,
|
|
845
1094
|
refreshSource.isPending,
|
|
846
|
-
|
|
1095
|
+
runSourceRefresh,
|
|
847
1096
|
]);
|
|
848
1097
|
|
|
849
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);
|
|
850
1108
|
if (
|
|
851
|
-
!
|
|
852
|
-
!canEdit ||
|
|
853
|
-
!source ||
|
|
854
|
-
!shouldPumpBuilderBodyHydration(
|
|
855
|
-
source,
|
|
856
|
-
processBuilderBodies.isPending,
|
|
857
|
-
builderHydrationClientErrorKey,
|
|
858
|
-
)
|
|
859
|
-
) {
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
const hydrationKey = builderBodyHydrationPumpKey(source);
|
|
863
|
-
if (!hydrationKey || autoPumpBuilderBodiesRef.current === hydrationKey) {
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
autoPumpBuilderBodiesRef.current = hydrationKey;
|
|
867
|
-
processBuilderBodies.mutate(
|
|
868
|
-
{ sourceId: source.id },
|
|
869
|
-
{
|
|
1109
|
+
!runBuilderHydration(candidate.id, {
|
|
870
1110
|
onSuccess: (result) => {
|
|
871
1111
|
if (!builderBodyHydrationMutationMadeProgress(result)) {
|
|
872
|
-
|
|
1112
|
+
setBuilderHydrationClientErrorKeys((current) =>
|
|
1113
|
+
addUniqueKey(current, hydrationKey),
|
|
1114
|
+
);
|
|
873
1115
|
}
|
|
874
1116
|
},
|
|
875
|
-
onError: () =>
|
|
876
|
-
|
|
877
|
-
|
|
1117
|
+
onError: () =>
|
|
1118
|
+
setBuilderHydrationClientErrorKeys((current) =>
|
|
1119
|
+
addUniqueKey(current, hydrationKey),
|
|
1120
|
+
),
|
|
1121
|
+
})
|
|
1122
|
+
) {
|
|
1123
|
+
autoPumpBuilderBodiesRef.current.delete(hydrationKey);
|
|
1124
|
+
}
|
|
878
1125
|
}, [
|
|
879
|
-
|
|
1126
|
+
builderHydrationClientErrorKeys,
|
|
1127
|
+
builderSources,
|
|
880
1128
|
canEdit,
|
|
881
1129
|
isActive,
|
|
882
1130
|
processBuilderBodies.isPending,
|
|
883
|
-
|
|
884
|
-
source,
|
|
1131
|
+
runBuilderHydration,
|
|
885
1132
|
]);
|
|
886
1133
|
|
|
887
1134
|
useEffect(() => {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
source.sourceType !== "builder-cms" ||
|
|
897
|
-
sourceAddsDetails(source) ||
|
|
898
|
-
refreshSource.isPending ||
|
|
899
|
-
builderSourceRowFetchStatus(source) !== "fetching" ||
|
|
900
|
-
source.metadata.lastReadHasMore !== true ||
|
|
901
|
-
!continuationKey ||
|
|
902
|
-
builderContinuationClientErrorKey === continuationKey
|
|
903
|
-
) {
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
if (builderContinuationWatchdogRef.current.key !== continuationKey) {
|
|
907
|
-
builderContinuationWatchdogRef.current = {
|
|
908
|
-
key: continuationKey,
|
|
909
|
-
refires: 0,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
const refireDelay = builderSourceContinuationWatchdogDelay(
|
|
913
|
-
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
|
+
},
|
|
914
1143
|
);
|
|
1144
|
+
if (!candidate) return;
|
|
1145
|
+
const continuationKey = builderSourceContinuationKey(candidate);
|
|
1146
|
+
if (!continuationKey) return;
|
|
1147
|
+
const refires =
|
|
1148
|
+
builderContinuationWatchdogRef.current.get(continuationKey) ?? 0;
|
|
915
1149
|
const timer = window.setTimeout(() => {
|
|
916
|
-
const watchdog = builderContinuationWatchdogRef.current;
|
|
917
|
-
if (watchdog.key !== continuationKey) return;
|
|
918
1150
|
if (
|
|
919
|
-
builderSourceContinuationWatchdogDecision(
|
|
920
|
-
|
|
1151
|
+
builderSourceContinuationWatchdogDecision(refires) !== "refire" ||
|
|
1152
|
+
refreshSource.isPending
|
|
1153
|
+
) {
|
|
921
1154
|
return;
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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));
|
|
937
1173
|
return () => window.clearTimeout(timer);
|
|
938
1174
|
}, [
|
|
939
|
-
|
|
1175
|
+
builderContinuationClientErrorKeys,
|
|
1176
|
+
builderSources,
|
|
940
1177
|
canEdit,
|
|
941
|
-
document.id,
|
|
942
1178
|
isActive,
|
|
943
|
-
refreshSource.mutate,
|
|
944
1179
|
refreshSource.isPending,
|
|
945
|
-
|
|
1180
|
+
runSourceRefresh,
|
|
946
1181
|
]);
|
|
947
1182
|
|
|
948
1183
|
useEffect(() => {
|
|
@@ -1047,19 +1282,20 @@ function DatabaseTable({
|
|
|
1047
1282
|
}
|
|
1048
1283
|
|
|
1049
1284
|
function prioritizeBuilderBodyHydrationForItem(item: ContentDatabaseItem) {
|
|
1050
|
-
const
|
|
1051
|
-
if (!
|
|
1052
|
-
const
|
|
1053
|
-
sources.find((candidate) => candidate.id === sourceId) ?? source;
|
|
1054
|
-
if (builderSource?.sourceType !== "builder-cms") return;
|
|
1285
|
+
const builderSource = databaseBuilderHydrationSourceForItem(item, sources);
|
|
1286
|
+
if (!builderSource) return;
|
|
1287
|
+
const sourceId = builderSource.id;
|
|
1055
1288
|
const hydration =
|
|
1056
1289
|
item.bodyHydration ?? item.document.databaseMembership?.bodyHydration;
|
|
1057
1290
|
if (hydration && !databaseItemBodyHydrationIsPending(item)) return;
|
|
1058
|
-
|
|
1291
|
+
runBuilderHydration(
|
|
1059
1292
|
sourceId,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1293
|
+
{},
|
|
1294
|
+
{
|
|
1295
|
+
documentId: item.document.id,
|
|
1296
|
+
limit: 1,
|
|
1297
|
+
},
|
|
1298
|
+
);
|
|
1063
1299
|
}
|
|
1064
1300
|
|
|
1065
1301
|
async function createRow(
|
|
@@ -1090,14 +1326,21 @@ function DatabaseTable({
|
|
|
1090
1326
|
});
|
|
1091
1327
|
return null;
|
|
1092
1328
|
}
|
|
1093
|
-
const createdItem = response
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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);
|
|
1097
1340
|
setPreviewDocumentId(createdItem.document.id);
|
|
1098
1341
|
setPreviewTitleFocusDocumentId(createdItem.document.id);
|
|
1099
1342
|
}
|
|
1100
|
-
if (createdItem && options.focusInlineTitle) {
|
|
1343
|
+
if (createdItem && options.focusInlineTitle && !needsPreview) {
|
|
1101
1344
|
setInlineTitleFocusDocumentId(createdItem.document.id);
|
|
1102
1345
|
}
|
|
1103
1346
|
return createdItem ?? null;
|
|
@@ -1372,73 +1615,376 @@ function DatabaseTable({
|
|
|
1372
1615
|
updateActiveView((view) => ({ ...view, hideEmptyGroups }));
|
|
1373
1616
|
}
|
|
1374
1617
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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);
|
|
1378
1633
|
setBuilderReviewResult(null);
|
|
1379
1634
|
setBuilderReviewCheckedAt(null);
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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"),
|
|
1390
1691
|
});
|
|
1391
|
-
|
|
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
|
+
);
|
|
1392
1712
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
)
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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;
|
|
1410
1736
|
}
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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.",
|
|
1418
1746
|
);
|
|
1419
|
-
if (reviewedChangeSets.length > 0) {
|
|
1420
|
-
nextReview = buildClientBuilderReviewPayload(
|
|
1421
|
-
executedSource,
|
|
1422
|
-
reviewedChangeSets,
|
|
1423
|
-
);
|
|
1424
|
-
}
|
|
1425
1747
|
}
|
|
1426
|
-
}
|
|
1427
1748
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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,
|
|
1436
1908
|
},
|
|
1437
1909
|
);
|
|
1438
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);
|
|
1439
1938
|
toast.error(dbText("builderUpdateFailed"), {
|
|
1440
|
-
description:
|
|
1441
|
-
|
|
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,
|
|
1442
1988
|
});
|
|
1443
1989
|
}
|
|
1444
1990
|
}
|
|
@@ -1810,41 +2356,40 @@ function DatabaseTable({
|
|
|
1810
2356
|
onSaveForEveryone={() => void savePersonalQueryForEveryone()}
|
|
1811
2357
|
/>
|
|
1812
2358
|
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
/>
|
|
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
|
+
})}
|
|
1848
2393
|
|
|
1849
2394
|
{activeView.type === "form" ? (
|
|
1850
2395
|
<DatabaseFormView
|
|
@@ -1863,7 +2408,7 @@ function DatabaseTable({
|
|
|
1863
2408
|
groupProperty={boardGroupProperty}
|
|
1864
2409
|
databaseDocumentId={document.id}
|
|
1865
2410
|
canEdit={canEdit}
|
|
1866
|
-
isLoading={
|
|
2411
|
+
isLoading={isDatabaseViewLoading}
|
|
1867
2412
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1868
2413
|
hasActiveConstraints={!!searchQuery || activeFilters.length > 0}
|
|
1869
2414
|
isMoving={setProperty.isPending}
|
|
@@ -1891,7 +2436,7 @@ function DatabaseTable({
|
|
|
1891
2436
|
items={visibleItems}
|
|
1892
2437
|
databaseDocumentId={document.id}
|
|
1893
2438
|
canEdit={canEdit}
|
|
1894
|
-
isLoading={
|
|
2439
|
+
isLoading={isDatabaseViewLoading}
|
|
1895
2440
|
isCreating={addItem.isPending}
|
|
1896
2441
|
activeFilters={activeFilters}
|
|
1897
2442
|
hasSearch={!!searchQuery}
|
|
@@ -1914,7 +2459,7 @@ function DatabaseTable({
|
|
|
1914
2459
|
items={visibleItems}
|
|
1915
2460
|
databaseDocumentId={document.id}
|
|
1916
2461
|
canEdit={canEdit}
|
|
1917
|
-
isLoading={
|
|
2462
|
+
isLoading={isDatabaseViewLoading}
|
|
1918
2463
|
isCreating={addItem.isPending}
|
|
1919
2464
|
activeFilters={activeFilters}
|
|
1920
2465
|
hasSearch={!!searchQuery}
|
|
@@ -1937,7 +2482,7 @@ function DatabaseTable({
|
|
|
1937
2482
|
items={visibleItems}
|
|
1938
2483
|
databaseDocumentId={document.id}
|
|
1939
2484
|
canEdit={canEdit}
|
|
1940
|
-
isLoading={
|
|
2485
|
+
isLoading={isDatabaseViewLoading}
|
|
1941
2486
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1942
2487
|
activeFilters={activeFilters}
|
|
1943
2488
|
hasSearch={!!searchQuery}
|
|
@@ -1963,7 +2508,7 @@ function DatabaseTable({
|
|
|
1963
2508
|
items={visibleItems}
|
|
1964
2509
|
databaseDocumentId={document.id}
|
|
1965
2510
|
canEdit={canEdit}
|
|
1966
|
-
isLoading={
|
|
2511
|
+
isLoading={isDatabaseViewLoading}
|
|
1967
2512
|
isCreating={addItem.isPending || setProperty.isPending}
|
|
1968
2513
|
activeFilters={activeFilters}
|
|
1969
2514
|
hasSearch={!!searchQuery}
|
|
@@ -1997,7 +2542,7 @@ function DatabaseTable({
|
|
|
1997
2542
|
sources={sources}
|
|
1998
2543
|
databaseDocumentId={document.id}
|
|
1999
2544
|
canEdit={canEdit}
|
|
2000
|
-
isLoading={
|
|
2545
|
+
isLoading={isDatabaseViewLoading}
|
|
2001
2546
|
isCreating={addItem.isPending}
|
|
2002
2547
|
columnWidths={columnWidths}
|
|
2003
2548
|
sorts={sorts}
|
|
@@ -2044,7 +2589,7 @@ function DatabaseTable({
|
|
|
2044
2589
|
/>
|
|
2045
2590
|
)}
|
|
2046
2591
|
|
|
2047
|
-
{hasMoreItems ? (
|
|
2592
|
+
{hasMoreItems && !isSearchExpansionPending ? (
|
|
2048
2593
|
<div className="flex items-center justify-center border-t border-border/45 py-3">
|
|
2049
2594
|
<Button
|
|
2050
2595
|
type="button"
|
|
@@ -2080,6 +2625,7 @@ function DatabaseTable({
|
|
|
2080
2625
|
if (!open) {
|
|
2081
2626
|
setPreviewDocumentId(null);
|
|
2082
2627
|
setPreviewTitleFocusDocumentId(null);
|
|
2628
|
+
setCreatedPreviewItem(null);
|
|
2083
2629
|
}
|
|
2084
2630
|
}}
|
|
2085
2631
|
onPreviewItem={(item) => {
|
|
@@ -2097,6 +2643,7 @@ function DatabaseTable({
|
|
|
2097
2643
|
panel={settingsPanel}
|
|
2098
2644
|
documentId={document.id}
|
|
2099
2645
|
canEdit={canEdit}
|
|
2646
|
+
canManage={document.canManage === true}
|
|
2100
2647
|
activeView={activeView}
|
|
2101
2648
|
properties={orderedProperties}
|
|
2102
2649
|
items={items}
|
|
@@ -2142,15 +2689,11 @@ function DatabaseTable({
|
|
|
2142
2689
|
onDisconnectSecondary={(sourceId) =>
|
|
2143
2690
|
disconnectSource.mutate({ documentId: document.id, sourceId })
|
|
2144
2691
|
}
|
|
2145
|
-
onRefreshSource={(sourceId) =>
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
onHydrateBuilderBodies={(sourceId) =>
|
|
2152
|
-
processBuilderBodies.mutate({ sourceId })
|
|
2153
|
-
}
|
|
2692
|
+
onRefreshSource={(sourceId) => {
|
|
2693
|
+
const targetSourceId = sourceId ?? source?.id;
|
|
2694
|
+
if (targetSourceId) runSourceRefresh(targetSourceId);
|
|
2695
|
+
}}
|
|
2696
|
+
onHydrateBuilderBodies={(sourceId) => runBuilderHydration(sourceId)}
|
|
2154
2697
|
onDisconnectSource={(sourceId) =>
|
|
2155
2698
|
disconnectSource.mutate(
|
|
2156
2699
|
{
|
|
@@ -2178,30 +2721,29 @@ function DatabaseTable({
|
|
|
2178
2721
|
},
|
|
2179
2722
|
)
|
|
2180
2723
|
}
|
|
2181
|
-
onReviewBuilderUpdate={() => {
|
|
2182
|
-
|
|
2183
|
-
setBuilderReviewCheckedAt(null);
|
|
2184
|
-
setBuilderReviewOpen(true);
|
|
2724
|
+
onReviewBuilderUpdate={(sourceId) => {
|
|
2725
|
+
openBuilderReview(sourceId);
|
|
2185
2726
|
}}
|
|
2186
|
-
onSetBuilderLiveWrites={(
|
|
2727
|
+
onSetBuilderLiveWrites={(settings) =>
|
|
2187
2728
|
setSourceWriteMode.mutate(
|
|
2188
2729
|
{
|
|
2189
2730
|
documentId: document.id,
|
|
2190
|
-
|
|
2191
|
-
|
|
2731
|
+
sourceId: settings.sourceId,
|
|
2732
|
+
writeMode: settings.writeMode,
|
|
2733
|
+
allowPublicationTransitions:
|
|
2734
|
+
settings.writeMode === "publish_updates" &&
|
|
2735
|
+
settings.allowPublicationTransitions === true,
|
|
2192
2736
|
},
|
|
2193
2737
|
{
|
|
2194
2738
|
onSuccess: () => {
|
|
2195
|
-
toast.success(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
},
|
|
2204
|
-
);
|
|
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
|
+
});
|
|
2205
2747
|
},
|
|
2206
2748
|
onError: (error) => {
|
|
2207
2749
|
toast.error(dbText("builderWriteModeWasNotChanged"), {
|
|
@@ -2220,8 +2762,10 @@ function DatabaseTable({
|
|
|
2220
2762
|
disconnectSource.isPending ||
|
|
2221
2763
|
prepareBuilderReview.isPending ||
|
|
2222
2764
|
executeBuilderExecution.isPending ||
|
|
2765
|
+
cancelPreparedBuilderUpdate.isPending ||
|
|
2223
2766
|
setSourceWriteMode.isPending
|
|
2224
2767
|
}
|
|
2768
|
+
sourcePendingOperations={sourcePendingOperations}
|
|
2225
2769
|
onViewTypeChange={(type) =>
|
|
2226
2770
|
setViewConfig(updateDatabaseViewType(viewConfig, activeView.id, type))
|
|
2227
2771
|
}
|
|
@@ -2242,14 +2786,36 @@ function DatabaseTable({
|
|
|
2242
2786
|
<BuilderSourceReviewDialog
|
|
2243
2787
|
open={builderReviewOpen}
|
|
2244
2788
|
review={activeBuilderReview}
|
|
2245
|
-
source={
|
|
2789
|
+
source={builderReviewSource}
|
|
2246
2790
|
canEdit={canEdit}
|
|
2247
2791
|
pending={
|
|
2248
|
-
|
|
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)
|
|
2249
2802
|
}
|
|
2250
2803
|
checkedAt={builderReviewCheckedAt}
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
+
}}
|
|
2253
2819
|
/>
|
|
2254
2820
|
|
|
2255
2821
|
{!isDatabaseInitialLoading ? (
|
|
@@ -2541,6 +3107,518 @@ export function databaseBuilderBulkUpdateSource(
|
|
|
2541
3107
|
);
|
|
2542
3108
|
}
|
|
2543
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
|
+
|
|
2544
3622
|
export function databaseBulkScalarInputState(
|
|
2545
3623
|
type: DocumentPropertyType,
|
|
2546
3624
|
input: string,
|
|
@@ -2799,19 +3877,44 @@ function DatabaseItemPreview({
|
|
|
2799
3877
|
const deleteDocument = useDeleteDocument();
|
|
2800
3878
|
const duplicateItem = useDuplicateDatabaseItem(databaseDocumentId);
|
|
2801
3879
|
const { data: document, isLoading } = useDocument(item.document.id);
|
|
3880
|
+
const { data: persistedPreviewDraft } = usePreviewDocumentDraft(
|
|
3881
|
+
item.document.id,
|
|
3882
|
+
);
|
|
3883
|
+
const updatePreviewDraft = useUpdatePreviewDocumentDraft();
|
|
2802
3884
|
const previewDocument = document ?? item.document;
|
|
2803
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;
|
|
2804
3899
|
const canEdit = document?.canEdit ?? item.document.canEdit ?? true;
|
|
2805
3900
|
const canManage = document?.canManage ?? item.document.canManage ?? false;
|
|
2806
|
-
const
|
|
2807
|
-
item
|
|
2808
|
-
document
|
|
2809
|
-
|
|
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
|
+
});
|
|
2810
3912
|
const bodyHydrationError = previewBodyHydrationIsTerminalError({
|
|
2811
3913
|
item,
|
|
2812
3914
|
document,
|
|
2813
3915
|
});
|
|
2814
|
-
const previewCanEdit =
|
|
3916
|
+
const previewCanEdit =
|
|
3917
|
+
canEdit && !bodyHydrationPending && !activeBodyDraftConflict;
|
|
2815
3918
|
const location = useLocation();
|
|
2816
3919
|
// Seed the displayed title/content from a RETAINED dirty controller's pending
|
|
2817
3920
|
// edit if one exists for this doc (reopen-before-evict), so an unsaved peek
|
|
@@ -2856,16 +3959,237 @@ function DatabaseItemPreview({
|
|
|
2856
3959
|
queryClientRef.current = queryClient;
|
|
2857
3960
|
const bodyHydrationPendingRef = useRef(bodyHydrationPending);
|
|
2858
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;
|
|
2859
3969
|
// Doc ids that have been deleted in this peek's lifetime. A pending save must
|
|
2860
3970
|
// never resurrect a deleted document, so dispatch is suppressed for these.
|
|
2861
3971
|
const deletedIdsRef = useRef<Set<string>>(new Set());
|
|
2862
3972
|
|
|
2863
3973
|
const documentId = item.document.id;
|
|
2864
3974
|
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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);
|
|
4026
|
+
}
|
|
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
|
+
},
|
|
4041
|
+
});
|
|
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) => {
|
|
4085
|
+
toast.error(dbText("failedToSavePagePreview"), {
|
|
4086
|
+
description:
|
|
4087
|
+
error instanceof Error
|
|
4088
|
+
? error.message
|
|
4089
|
+
: dbText("somethingWentWrong"),
|
|
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
|
+
|
|
2869
4193
|
const makeController = () =>
|
|
2870
4194
|
createPreviewDocumentSaveController({
|
|
2871
4195
|
documentId,
|
|
@@ -2877,42 +4201,7 @@ function DatabaseItemPreview({
|
|
|
2877
4201
|
item.document.content,
|
|
2878
4202
|
),
|
|
2879
4203
|
},
|
|
2880
|
-
|
|
2881
|
-
new Promise((resolve, reject) => {
|
|
2882
|
-
// A just-deleted doc must not be re-dispatched (resurrection guard).
|
|
2883
|
-
if (deletedIdsRef.current.has(id)) {
|
|
2884
|
-
resolve(undefined);
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
if (bodyHydrationPendingRef.current) {
|
|
2888
|
-
resolve(skippedPreviewDocumentSave());
|
|
2889
|
-
return;
|
|
2890
|
-
}
|
|
2891
|
-
updateDocumentRef.current.mutate(
|
|
2892
|
-
{
|
|
2893
|
-
id,
|
|
2894
|
-
title: payload.title,
|
|
2895
|
-
content: payload.content,
|
|
2896
|
-
loadedUpdatedAt: payload.loadedUpdatedAt,
|
|
2897
|
-
loadedContentWasEmpty: payload.loadedContentWasEmpty,
|
|
2898
|
-
},
|
|
2899
|
-
{ onSuccess: () => resolve(undefined), onError: reject },
|
|
2900
|
-
);
|
|
2901
|
-
}),
|
|
2902
|
-
onSaved: () => {
|
|
2903
|
-
void queryClientRef.current.invalidateQueries({
|
|
2904
|
-
queryKey: contentDatabaseQueryKey(databaseDocumentId),
|
|
2905
|
-
});
|
|
2906
|
-
void queryClientRef.current.invalidateQueries({
|
|
2907
|
-
queryKey: ["action", "list-documents"],
|
|
2908
|
-
});
|
|
2909
|
-
},
|
|
2910
|
-
onError: (err) => {
|
|
2911
|
-
toast.error(dbText("failedToSavePagePreview"), {
|
|
2912
|
-
description:
|
|
2913
|
-
err instanceof Error ? err.message : dbText("somethingWentWrong"),
|
|
2914
|
-
});
|
|
2915
|
-
},
|
|
4204
|
+
...makeSaveAdapter(),
|
|
2916
4205
|
});
|
|
2917
4206
|
|
|
2918
4207
|
// Acquire the controller for the current row, and release it on row-switch /
|
|
@@ -2946,8 +4235,21 @@ function DatabaseItemPreview({
|
|
|
2946
4235
|
saveControllerRef.current = acquirePreviewDocumentSaveController(
|
|
2947
4236
|
documentId,
|
|
2948
4237
|
makeController,
|
|
4238
|
+
(controller) => controller.replaceSaveAdapter(makeSaveAdapter()),
|
|
2949
4239
|
);
|
|
2950
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
|
+
}
|
|
2951
4253
|
saveControllerRef.current = null;
|
|
2952
4254
|
releasePreviewDocumentSaveController(documentId);
|
|
2953
4255
|
};
|
|
@@ -2957,6 +4259,58 @@ function DatabaseItemPreview({
|
|
|
2957
4259
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2958
4260
|
}, [documentId]);
|
|
2959
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
|
+
|
|
2960
4314
|
// Sync displayed state to the current row, and adopt fresh server content
|
|
2961
4315
|
// (e.g. an agent edit) as the controller's new confirmed baseline. mark()
|
|
2962
4316
|
// touches only THIS row's controller — never another row's — because the
|
|
@@ -2990,12 +4344,31 @@ function DatabaseItemPreview({
|
|
|
2990
4344
|
controller.lastSaved.loadedContentWasEmpty === true &&
|
|
2991
4345
|
isEffectivelyEmptyDocumentContent(controller.pending.content) &&
|
|
2992
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
|
+
}));
|
|
2993
4359
|
// Only adopt the server's title/content — into BOTH the displayed editor
|
|
2994
4360
|
// state and the controller baseline — when the user hasn't typed something
|
|
2995
4361
|
// newer on this row. If a dirty in-progress edit exists, preserve it: don't
|
|
2996
4362
|
// clobber the visible text (the controller already holds the unsaved edit,
|
|
2997
4363
|
// so nothing is lost, but the editor must keep showing what the user typed).
|
|
2998
|
-
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
|
+
) {
|
|
2999
4372
|
setLocalTitle(nextTitle);
|
|
3000
4373
|
setLocalContent(nextContent);
|
|
3001
4374
|
controller?.mark(serverPayload);
|
|
@@ -3018,6 +4391,36 @@ function DatabaseItemPreview({
|
|
|
3018
4391
|
item.document.updatedAt,
|
|
3019
4392
|
]);
|
|
3020
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
|
+
|
|
3021
4424
|
useEffect(() => {
|
|
3022
4425
|
if (!focusTitle || !previewCanEdit || isLoading || !document) return;
|
|
3023
4426
|
|
|
@@ -3033,7 +4436,9 @@ function DatabaseItemPreview({
|
|
|
3033
4436
|
function handleTitleChange(nextTitle: string) {
|
|
3034
4437
|
setLocalTitle(nextTitle);
|
|
3035
4438
|
if (!previewCanEdit || !document) return;
|
|
3036
|
-
saveControllerRef.current
|
|
4439
|
+
const controller = saveControllerRef.current;
|
|
4440
|
+
controller?.changeTitle(nextTitle);
|
|
4441
|
+
if (controller) scheduleDraftWrite(controller);
|
|
3037
4442
|
}
|
|
3038
4443
|
|
|
3039
4444
|
function handleContentChange(nextContent: string) {
|
|
@@ -3048,7 +4453,31 @@ function DatabaseItemPreview({
|
|
|
3048
4453
|
}
|
|
3049
4454
|
setLocalContent(nextContent);
|
|
3050
4455
|
if (!previewCanEdit || !document) return;
|
|
3051
|
-
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);
|
|
3052
4481
|
}
|
|
3053
4482
|
|
|
3054
4483
|
function handleIconChange(nextIcon: string | null) {
|
|
@@ -3343,6 +4772,37 @@ function DatabaseItemPreview({
|
|
|
3343
4772
|
|
|
3344
4773
|
return (
|
|
3345
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}
|
|
3346
4806
|
{bodyHydrationError ? (
|
|
3347
4807
|
<BuilderBodySyncingNotice
|
|
3348
4808
|
title={dbText("builderBodySyncFailedNotice")}
|
|
@@ -5407,7 +6867,12 @@ type PendingSourceCandidate = {
|
|
|
5407
6867
|
type SourceNavStep =
|
|
5408
6868
|
| { kind: "provider"; providerId: "builder" }
|
|
5409
6869
|
| { kind: "space"; spaceId: string; spaceName: string }
|
|
5410
|
-
| {
|
|
6870
|
+
| {
|
|
6871
|
+
kind: "model";
|
|
6872
|
+
model?: BuilderCmsModelSummary;
|
|
6873
|
+
sourceId?: string;
|
|
6874
|
+
sourceName?: string;
|
|
6875
|
+
}
|
|
5411
6876
|
| { kind: "addSource" }
|
|
5412
6877
|
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
5413
6878
|
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
@@ -5422,7 +6887,7 @@ function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
|
5422
6887
|
if (top.kind === "secondarySource") return top.sourceName;
|
|
5423
6888
|
if (top.kind === "keyConfirm") return dbText("matchExistingItemsToDetails");
|
|
5424
6889
|
if (top.kind === "fieldPicker") return dbText("chooseFields");
|
|
5425
|
-
return top.model.
|
|
6890
|
+
return top.model?.displayName ?? top.sourceName ?? "Builder";
|
|
5426
6891
|
}
|
|
5427
6892
|
|
|
5428
6893
|
// The Builder "B" brand mark (first glyph of the wordmark), drawn with
|
|
@@ -5470,6 +6935,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5470
6935
|
panel,
|
|
5471
6936
|
documentId,
|
|
5472
6937
|
canEdit,
|
|
6938
|
+
canManage,
|
|
5473
6939
|
activeView,
|
|
5474
6940
|
properties,
|
|
5475
6941
|
items,
|
|
@@ -5489,6 +6955,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5489
6955
|
onReviewBuilderUpdate,
|
|
5490
6956
|
onSetBuilderLiveWrites,
|
|
5491
6957
|
sourceActionPending,
|
|
6958
|
+
sourcePendingOperations,
|
|
5492
6959
|
onViewTypeChange,
|
|
5493
6960
|
onWrapCellsChange,
|
|
5494
6961
|
onOpenPagesInChange,
|
|
@@ -5503,6 +6970,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5503
6970
|
panel: DatabaseSettingsPanel;
|
|
5504
6971
|
documentId: string;
|
|
5505
6972
|
canEdit: boolean;
|
|
6973
|
+
canManage: boolean;
|
|
5506
6974
|
activeView: ContentDatabaseView;
|
|
5507
6975
|
properties: DocumentProperty[];
|
|
5508
6976
|
items: ContentDatabaseItem[];
|
|
@@ -5529,9 +6997,10 @@ function DatabaseSettingsPanelSheet({
|
|
|
5529
6997
|
onRefreshSource: (sourceId?: string) => void;
|
|
5530
6998
|
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
5531
6999
|
onDisconnectSource: (sourceId?: string) => void;
|
|
5532
|
-
onReviewBuilderUpdate: () => void;
|
|
5533
|
-
onSetBuilderLiveWrites: (
|
|
7000
|
+
onReviewBuilderUpdate: (sourceId: string) => void;
|
|
7001
|
+
onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
|
|
5534
7002
|
sourceActionPending: boolean;
|
|
7003
|
+
sourcePendingOperations: DatabaseSourcePendingOperations;
|
|
5535
7004
|
onViewTypeChange: (type: ContentDatabaseViewType) => void;
|
|
5536
7005
|
onWrapCellsChange: (wrapCells: boolean) => void;
|
|
5537
7006
|
onOpenPagesInChange: (openPagesIn: ContentDatabaseOpenPagesIn) => void;
|
|
@@ -5615,6 +7084,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5615
7084
|
documentId={documentId}
|
|
5616
7085
|
itemCount={items.length}
|
|
5617
7086
|
canEdit={canEdit}
|
|
7087
|
+
canManage={canManage}
|
|
5618
7088
|
nav={sourceNavStack}
|
|
5619
7089
|
onNavPush={(step) => setSourceNavStack((stack) => [...stack, step])}
|
|
5620
7090
|
onNavReplace={setSourceNavStack}
|
|
@@ -5631,6 +7101,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
5631
7101
|
onReviewBuilderUpdate={onReviewBuilderUpdate}
|
|
5632
7102
|
onSetBuilderLiveWrites={onSetBuilderLiveWrites}
|
|
5633
7103
|
sourceActionPending={sourceActionPending}
|
|
7104
|
+
sourcePendingOperations={sourcePendingOperations}
|
|
5634
7105
|
/>
|
|
5635
7106
|
) : panel === "layout" ? (
|
|
5636
7107
|
<DatabaseSettingsLayoutPanel
|
|
@@ -5745,6 +7216,9 @@ export function builderReviewableChangeSets(
|
|
|
5745
7216
|
return source.changeSets.filter(
|
|
5746
7217
|
(changeSet) =>
|
|
5747
7218
|
changeSet.direction === "outbound" &&
|
|
7219
|
+
!changeSet.executions.some(
|
|
7220
|
+
(execution) => execution.state === "succeeded",
|
|
7221
|
+
) &&
|
|
5748
7222
|
(changeSet.state === "pending_push" ||
|
|
5749
7223
|
changeSet.state === "staged_revision" ||
|
|
5750
7224
|
changeSet.state === "approved"),
|
|
@@ -5770,12 +7244,33 @@ function maxSourceReviewRisk(
|
|
|
5770
7244
|
|
|
5771
7245
|
export function builderReviewExecutableRows(
|
|
5772
7246
|
review: ContentDatabaseSourceReviewPayload,
|
|
7247
|
+
selectedChangeSetIds: ReadonlySet<string> = new Set(
|
|
7248
|
+
review.rows.map((row) => row.changeSetId),
|
|
7249
|
+
),
|
|
5773
7250
|
) {
|
|
5774
|
-
if (!review.liveWritesEnabled
|
|
5775
|
-
return [];
|
|
5776
|
-
}
|
|
7251
|
+
if (!review.liveWritesEnabled) return [];
|
|
5777
7252
|
return review.rows.filter(
|
|
5778
|
-
(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
|
|
5779
7274
|
);
|
|
5780
7275
|
}
|
|
5781
7276
|
|
|
@@ -5783,24 +7278,67 @@ export function builderSourceLiveWriteControlState(
|
|
|
5783
7278
|
source: ContentDatabaseSource | null,
|
|
5784
7279
|
) {
|
|
5785
7280
|
const isBuilderSource = source?.sourceType === "builder-cms";
|
|
5786
|
-
const
|
|
5787
|
-
|
|
5788
|
-
|
|
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";
|
|
5789
7290
|
return {
|
|
5790
|
-
safeTarget,
|
|
7291
|
+
safeTarget: isBuilderSource,
|
|
5791
7292
|
enabled,
|
|
5792
|
-
|
|
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,
|
|
5793
7302
|
actionLabel: enabled ? "Disable" : "Enable",
|
|
5794
7303
|
description: enabled
|
|
5795
|
-
?
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
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.",
|
|
5801
7310
|
};
|
|
5802
7311
|
}
|
|
5803
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
|
+
|
|
5804
7342
|
export function buildClientBuilderReviewPayload(
|
|
5805
7343
|
source: ContentDatabaseSource,
|
|
5806
7344
|
changeSets: ContentDatabaseSourceChangeSet[],
|
|
@@ -5826,6 +7364,7 @@ export function buildClientBuilderReviewPayload(
|
|
|
5826
7364
|
(field) => field.localFieldKey === "title",
|
|
5827
7365
|
);
|
|
5828
7366
|
const proposedTitle = titleChange?.proposedValue;
|
|
7367
|
+
const effect = resolveBuilderCmsWriteEffect({ source, changeSet });
|
|
5829
7368
|
|
|
5830
7369
|
return {
|
|
5831
7370
|
changeSetId: changeSet.id,
|
|
@@ -5835,12 +7374,14 @@ export function buildClientBuilderReviewPayload(
|
|
|
5835
7374
|
typeof proposedTitle === "string" && proposedTitle.trim()
|
|
5836
7375
|
? proposedTitle
|
|
5837
7376
|
: sourceRow?.sourceDisplayKey || "Untitled",
|
|
7377
|
+
targetEntryId:
|
|
7378
|
+
effect === "create_draft" ? null : (sourceRow?.sourceRowId ?? null),
|
|
5838
7379
|
fieldChanges: changeSet.fieldChanges,
|
|
5839
7380
|
bodyChange: changeSet.bodyChange,
|
|
5840
7381
|
riskLevel: changeSet.riskLevel,
|
|
5841
7382
|
riskReasons: changeSet.riskReasons,
|
|
5842
7383
|
conflictState: changeSet.conflictState,
|
|
5843
|
-
effect
|
|
7384
|
+
effect,
|
|
5844
7385
|
execution: latestExecution,
|
|
5845
7386
|
};
|
|
5846
7387
|
});
|
|
@@ -5863,19 +7404,22 @@ export function buildClientBuilderReviewPayload(
|
|
|
5863
7404
|
executionStates.length > 0 &&
|
|
5864
7405
|
executionStates.every((state) => state === "succeeded")
|
|
5865
7406
|
? "succeeded"
|
|
5866
|
-
: executionStates.includes("
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
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")
|
|
5877
7419
|
? "validated"
|
|
5878
|
-
:
|
|
7420
|
+
: source.capabilities.liveWritesEnabled
|
|
7421
|
+
? "validated"
|
|
7422
|
+
: "write_disabled";
|
|
5879
7423
|
|
|
5880
7424
|
return {
|
|
5881
7425
|
summary:
|
|
@@ -5897,19 +7441,21 @@ export function buildClientBuilderReviewPayload(
|
|
|
5897
7441
|
? "Pushed to Builder and reconciled locally."
|
|
5898
7442
|
: resultStatus === "failed"
|
|
5899
7443
|
? "Builder push failed. The change remains retryable."
|
|
5900
|
-
: resultStatus === "
|
|
5901
|
-
? "Builder
|
|
5902
|
-
: resultStatus === "
|
|
5903
|
-
?
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
: resultStatus === "
|
|
5911
|
-
? "Push needs
|
|
5912
|
-
:
|
|
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.",
|
|
5913
7459
|
},
|
|
5914
7460
|
};
|
|
5915
7461
|
}
|
|
@@ -5920,6 +7466,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
5920
7466
|
documentId,
|
|
5921
7467
|
itemCount,
|
|
5922
7468
|
canEdit,
|
|
7469
|
+
canManage,
|
|
5923
7470
|
nav,
|
|
5924
7471
|
onNavPush,
|
|
5925
7472
|
onNavReplace,
|
|
@@ -5933,12 +7480,14 @@ function DatabaseSettingsSourcePanel({
|
|
|
5933
7480
|
onReviewBuilderUpdate,
|
|
5934
7481
|
onSetBuilderLiveWrites,
|
|
5935
7482
|
sourceActionPending,
|
|
7483
|
+
sourcePendingOperations,
|
|
5936
7484
|
}: {
|
|
5937
7485
|
source: ContentDatabaseSource | null;
|
|
5938
7486
|
sources: ContentDatabaseSource[];
|
|
5939
7487
|
documentId: string;
|
|
5940
7488
|
itemCount: number;
|
|
5941
7489
|
canEdit: boolean;
|
|
7490
|
+
canManage: boolean;
|
|
5942
7491
|
nav: SourceNavStep[];
|
|
5943
7492
|
onNavPush: (step: SourceNavStep) => void;
|
|
5944
7493
|
onNavReplace: (stack: SourceNavStep[]) => void;
|
|
@@ -5959,26 +7508,13 @@ function DatabaseSettingsSourcePanel({
|
|
|
5959
7508
|
onRefreshSource: (sourceId?: string) => void;
|
|
5960
7509
|
onHydrateBuilderBodies: (sourceId: string) => void;
|
|
5961
7510
|
onDisconnectSource: (sourceId?: string) => void;
|
|
5962
|
-
onReviewBuilderUpdate: () => void;
|
|
5963
|
-
onSetBuilderLiveWrites: (
|
|
7511
|
+
onReviewBuilderUpdate: (sourceId: string) => void;
|
|
7512
|
+
onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
|
|
5964
7513
|
sourceActionPending: boolean;
|
|
7514
|
+
sourcePendingOperations: DatabaseSourcePendingOperations;
|
|
5965
7515
|
}) {
|
|
5966
|
-
const outboundChangeSets =
|
|
5967
|
-
source?.changeSets.filter(
|
|
5968
|
-
(changeSet) => changeSet.direction === "outbound",
|
|
5969
|
-
) ?? [];
|
|
5970
|
-
const reviewableBuilderChangeSets = outboundChangeSets.filter(
|
|
5971
|
-
(changeSet) =>
|
|
5972
|
-
changeSet.state === "pending_push" ||
|
|
5973
|
-
changeSet.state === "staged_revision" ||
|
|
5974
|
-
changeSet.state === "approved",
|
|
5975
|
-
);
|
|
5976
|
-
const conflictChangeSets =
|
|
5977
|
-
source?.changeSets.filter(
|
|
5978
|
-
(changeSet) => changeSet.conflictState === "source_changed",
|
|
5979
|
-
) ?? [];
|
|
5980
7516
|
const { isCodeMode } = useCodeMode();
|
|
5981
|
-
const
|
|
7517
|
+
const builderSources = databaseAttachedBuilderSources(sources, source);
|
|
5982
7518
|
const builderStatus = useBuilderStatus();
|
|
5983
7519
|
const builderConfigured = builderStatus.status?.configured === true;
|
|
5984
7520
|
const builderOrgName = builderStatus.status?.orgName ?? null;
|
|
@@ -5997,10 +7533,6 @@ function DatabaseSettingsSourcePanel({
|
|
|
5997
7533
|
void builderStatus.refetch();
|
|
5998
7534
|
},
|
|
5999
7535
|
});
|
|
6000
|
-
const builderSyncFailed =
|
|
6001
|
-
isBuilderSource &&
|
|
6002
|
-
(source?.syncState === "error" || Boolean(source?.lastError));
|
|
6003
|
-
|
|
6004
7536
|
const top = nav[nav.length - 1];
|
|
6005
7537
|
|
|
6006
7538
|
// ── Sources list (root) ───────────────────────────────────────────────
|
|
@@ -6011,9 +7543,21 @@ function DatabaseSettingsSourcePanel({
|
|
|
6011
7543
|
sources={sources}
|
|
6012
7544
|
builderConfigured={builderConfigured}
|
|
6013
7545
|
builderSpaceLabel={builderSpaceLabel}
|
|
6014
|
-
reviewableCount={
|
|
6015
|
-
|
|
6016
|
-
|
|
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
|
+
)
|
|
6017
7561
|
}
|
|
6018
7562
|
onOpenNotion={() => onNavPush({ kind: "addSource" })}
|
|
6019
7563
|
onOpenSecondary={(secondary) =>
|
|
@@ -6217,22 +7761,28 @@ function DatabaseSettingsSourcePanel({
|
|
|
6217
7761
|
if (top.kind === "space") {
|
|
6218
7762
|
return (
|
|
6219
7763
|
<BuilderSpaceModelsView
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
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
|
+
}}
|
|
6224
7771
|
/>
|
|
6225
7772
|
);
|
|
6226
7773
|
}
|
|
6227
7774
|
|
|
6228
7775
|
// ── Model leaf ────────────────────────────────────────────────────────
|
|
6229
7776
|
const model = top.model;
|
|
6230
|
-
const
|
|
6231
|
-
|
|
7777
|
+
const selectedBuilderSource = databaseAttachedBuilderSource(sources, source, {
|
|
7778
|
+
sourceId: top.sourceId,
|
|
7779
|
+
modelName: model?.name,
|
|
7780
|
+
});
|
|
6232
7781
|
|
|
6233
7782
|
// Unattached model → the attach affordance (the model is already chosen by
|
|
6234
7783
|
// drilling in, so there's no model picker here).
|
|
6235
|
-
if (!
|
|
7784
|
+
if (!selectedBuilderSource) {
|
|
7785
|
+
if (!model) return null;
|
|
6236
7786
|
return (
|
|
6237
7787
|
<div className="grid min-w-0 gap-3">
|
|
6238
7788
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
@@ -6311,41 +7861,91 @@ function DatabaseSettingsSourcePanel({
|
|
|
6311
7861
|
);
|
|
6312
7862
|
}
|
|
6313
7863
|
|
|
6314
|
-
|
|
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.
|
|
6315
7918
|
return (
|
|
6316
7919
|
<div className="grid min-w-0 gap-4">
|
|
6317
7920
|
<>
|
|
6318
7921
|
<div className="grid min-w-0 gap-1.5 rounded-lg border border-border bg-background p-3 text-sm">
|
|
6319
7922
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
6320
|
-
<span
|
|
6321
|
-
|
|
7923
|
+
<span
|
|
7924
|
+
className="truncate font-medium"
|
|
7925
|
+
title={selectedSource.sourceName}
|
|
7926
|
+
>
|
|
7927
|
+
{selectedSource.sourceName}
|
|
6322
7928
|
</span>
|
|
6323
7929
|
<div className="flex shrink-0 items-center gap-1">
|
|
6324
|
-
{
|
|
6325
|
-
|
|
6326
|
-
<
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
</span>
|
|
6330
|
-
) : (
|
|
6331
|
-
<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">
|
|
6332
|
-
<IconLock className="size-3" />
|
|
6333
|
-
{dbText("readOnly")}
|
|
6334
|
-
</span>
|
|
6335
|
-
)
|
|
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>
|
|
6336
7935
|
) : (
|
|
6337
|
-
<span className="rounded-full border border-border px-2 py-0.5 text-[11px]
|
|
6338
|
-
|
|
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")}
|
|
6339
7939
|
</span>
|
|
6340
7940
|
)}
|
|
6341
|
-
{
|
|
7941
|
+
{!builderSyncFailed ? (
|
|
6342
7942
|
<Button
|
|
6343
7943
|
type="button"
|
|
6344
7944
|
variant="ghost"
|
|
6345
7945
|
size="sm"
|
|
6346
7946
|
className="h-7 gap-1 px-2 text-xs"
|
|
6347
|
-
disabled={!canEdit ||
|
|
6348
|
-
onClick={() => onRefreshSource(
|
|
7947
|
+
disabled={!canEdit || selectedSourceRefreshControlPending}
|
|
7948
|
+
onClick={() => onRefreshSource(selectedSource.id)}
|
|
6349
7949
|
>
|
|
6350
7950
|
<IconRefresh className="size-3" />
|
|
6351
7951
|
{dbText("refreshSource")}
|
|
@@ -6358,109 +7958,186 @@ function DatabaseSettingsSourcePanel({
|
|
|
6358
7958
|
<button
|
|
6359
7959
|
type="button"
|
|
6360
7960
|
className="inline-flex items-center gap-1 text-destructive hover:underline disabled:opacity-60"
|
|
6361
|
-
disabled={!canEdit ||
|
|
6362
|
-
onClick={() => onRefreshSource(
|
|
7961
|
+
disabled={!canEdit || selectedSourceRefreshControlPending}
|
|
7962
|
+
onClick={() => onRefreshSource(selectedSource.id)}
|
|
6363
7963
|
>
|
|
6364
7964
|
<IconRefresh className="size-3" />
|
|
6365
7965
|
{dbText("couldntSyncRetry")}
|
|
6366
7966
|
</button>
|
|
6367
|
-
) :
|
|
7967
|
+
) : (
|
|
6368
7968
|
[
|
|
6369
7969
|
builderConfigured ? (builderSpaceLabel ?? "Connected") : null,
|
|
6370
|
-
|
|
7970
|
+
selectedSource.lastRefreshedAt
|
|
6371
7971
|
? `synced ${
|
|
6372
|
-
formatRelativeSyncTime(
|
|
6373
|
-
|
|
7972
|
+
formatRelativeSyncTime(selectedSource.lastRefreshedAt) ??
|
|
7973
|
+
selectedSource.freshness
|
|
6374
7974
|
}`
|
|
6375
|
-
:
|
|
6376
|
-
typeof
|
|
7975
|
+
: selectedSource.freshness,
|
|
7976
|
+
typeof selectedSource.metadata.lastReadFetchedEntryCount ===
|
|
7977
|
+
"number"
|
|
6377
7978
|
? dbText("builderRowsFetched", {
|
|
6378
|
-
count:
|
|
7979
|
+
count: selectedSource.metadata.lastReadFetchedEntryCount,
|
|
6379
7980
|
})
|
|
6380
7981
|
: null,
|
|
6381
|
-
builderSourceRowFetchStatus(
|
|
7982
|
+
builderSourceRowFetchStatus(selectedSource) === "error"
|
|
6382
7983
|
? dbText("builderRowsFetchFailed")
|
|
6383
|
-
: builderSourceRowFetchStatus(
|
|
7984
|
+
: builderSourceRowFetchStatus(selectedSource) === "fetching"
|
|
6384
7985
|
? dbText("builderRowsFetchingMore")
|
|
6385
7986
|
: null,
|
|
6386
7987
|
]
|
|
6387
7988
|
.filter(Boolean)
|
|
6388
7989
|
.join(" · ")
|
|
6389
|
-
) : (
|
|
6390
|
-
`Local snapshot · ${source.freshness}`
|
|
6391
7990
|
)}
|
|
6392
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>
|
|
6393
8072
|
</div>
|
|
6394
8073
|
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
: `${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${
|
|
6406
8084
|
reviewableBuilderChangeSets.length === 1 ? "" : "s"
|
|
6407
|
-
} ready to push`
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
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")}
|
|
6412
8090
|
</div>
|
|
6413
|
-
<Button
|
|
6414
|
-
type="button"
|
|
6415
|
-
size="sm"
|
|
6416
|
-
className="shrink-0"
|
|
6417
|
-
disabled={!canEdit || sourceActionPending}
|
|
6418
|
-
onClick={onReviewBuilderUpdate}
|
|
6419
|
-
>
|
|
6420
|
-
<IconCheck className="mr-1.5 size-3.5" />
|
|
6421
|
-
{dbText("reviewDiff")}
|
|
6422
|
-
</Button>
|
|
6423
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>
|
|
6424
8102
|
</div>
|
|
6425
|
-
|
|
8103
|
+
</div>
|
|
6426
8104
|
|
|
6427
|
-
{
|
|
8105
|
+
{selectedSource.bodyHydration ? (
|
|
6428
8106
|
<BuilderBodyHydrationCard
|
|
6429
|
-
source={
|
|
8107
|
+
source={selectedSource}
|
|
6430
8108
|
canEdit={canEdit}
|
|
6431
|
-
pending={
|
|
6432
|
-
onHydrate={() => onHydrateBuilderBodies(
|
|
8109
|
+
pending={selectedSourceHydrationControlPending}
|
|
8110
|
+
onHydrate={() => onHydrateBuilderBodies(selectedSource.id)}
|
|
6433
8111
|
/>
|
|
6434
8112
|
) : null}
|
|
6435
8113
|
|
|
8114
|
+
<BuilderRequiredFieldsCard
|
|
8115
|
+
documentId={documentId}
|
|
8116
|
+
source={selectedSource}
|
|
8117
|
+
canEdit={canEdit}
|
|
8118
|
+
pending={selectedSourceMutationPending}
|
|
8119
|
+
/>
|
|
8120
|
+
|
|
6436
8121
|
{isCodeMode ? (
|
|
6437
8122
|
<>
|
|
6438
8123
|
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3 text-sm">
|
|
6439
|
-
<div className="font-medium">
|
|
6440
|
-
{source.sourceType === "builder-cms"
|
|
6441
|
-
? "Local Builder changes"
|
|
6442
|
-
: "Local outbound changes"}
|
|
6443
|
-
</div>
|
|
8124
|
+
<div className="font-medium">{dbText("builderChanges")}</div>
|
|
6444
8125
|
<div className="text-xs text-muted-foreground">
|
|
6445
|
-
{
|
|
6446
|
-
?
|
|
6447
|
-
|
|
6448
|
-
: "Local edits can be staged as a Builder save revision/autosave record. Live Builder writes are disabled."
|
|
6449
|
-
: "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."}
|
|
6450
8129
|
</div>
|
|
6451
8130
|
<div className="grid min-w-0 gap-2">
|
|
6452
8131
|
{outboundChangeSets.slice(0, 6).map((changeSet) => (
|
|
6453
8132
|
<SourceChangeSetReviewCard
|
|
6454
8133
|
key={changeSet.id}
|
|
6455
8134
|
changeSet={changeSet}
|
|
6456
|
-
source={
|
|
8135
|
+
source={selectedSource}
|
|
6457
8136
|
/>
|
|
6458
8137
|
))}
|
|
6459
8138
|
{outboundChangeSets.length === 0 ? (
|
|
6460
8139
|
<div className="text-xs text-muted-foreground">
|
|
6461
|
-
{
|
|
6462
|
-
? "No pending local Builder changes yet. Rename a source-backed row to see a local outbound diff."
|
|
6463
|
-
: "No local outbound changes yet."}
|
|
8140
|
+
{dbText("noPendingChangesEditASourceBackedRow")}
|
|
6464
8141
|
</div>
|
|
6465
8142
|
) : null}
|
|
6466
8143
|
</div>
|
|
@@ -6469,33 +8146,33 @@ function DatabaseSettingsSourcePanel({
|
|
|
6469
8146
|
) : null}
|
|
6470
8147
|
|
|
6471
8148
|
<SourceRoleCard
|
|
6472
|
-
source={
|
|
8149
|
+
source={selectedSource}
|
|
6473
8150
|
canAddDetails={sources.some(
|
|
6474
|
-
(item) => item.id !==
|
|
8151
|
+
(item) => item.id !== selectedSource.id && !sourceAddsDetails(item),
|
|
6475
8152
|
)}
|
|
6476
8153
|
canEdit={canEdit}
|
|
6477
|
-
pending={
|
|
8154
|
+
pending={selectedSourceRoleControlPending}
|
|
6478
8155
|
onAddDetails={() =>
|
|
6479
8156
|
onNavPush({
|
|
6480
8157
|
kind: "keyConfirm",
|
|
6481
8158
|
candidate: {
|
|
6482
|
-
sourceType:
|
|
6483
|
-
sourceName:
|
|
6484
|
-
sourceTable:
|
|
6485
|
-
displayName:
|
|
6486
|
-
existingSourceId:
|
|
8159
|
+
sourceType: selectedSource.sourceType,
|
|
8160
|
+
sourceName: selectedSource.sourceName,
|
|
8161
|
+
sourceTable: selectedSource.sourceTable,
|
|
8162
|
+
displayName: selectedSource.sourceName,
|
|
8163
|
+
existingSourceId: selectedSource.id,
|
|
6487
8164
|
},
|
|
6488
8165
|
})
|
|
6489
8166
|
}
|
|
6490
8167
|
onAddItems={async () => {
|
|
6491
|
-
await onChangeSourceRole(
|
|
8168
|
+
await onChangeSourceRole(selectedSource.id, "items");
|
|
6492
8169
|
onNavReplace([]);
|
|
6493
8170
|
}}
|
|
6494
8171
|
onChooseFields={() =>
|
|
6495
8172
|
onNavPush({
|
|
6496
8173
|
kind: "fieldPicker",
|
|
6497
|
-
sourceId:
|
|
6498
|
-
sourceName:
|
|
8174
|
+
sourceId: selectedSource.id,
|
|
8175
|
+
sourceName: selectedSource.sourceName,
|
|
6499
8176
|
})
|
|
6500
8177
|
}
|
|
6501
8178
|
/>
|
|
@@ -6512,10 +8189,10 @@ function DatabaseSettingsSourcePanel({
|
|
|
6512
8189
|
size="sm"
|
|
6513
8190
|
variant="outline"
|
|
6514
8191
|
className="mt-2 h-8 text-xs text-destructive hover:text-destructive"
|
|
6515
|
-
disabled={!canEdit ||
|
|
6516
|
-
onClick={() => onDisconnectSource(
|
|
8192
|
+
disabled={!canEdit || selectedSourceDisconnectControlPending}
|
|
8193
|
+
onClick={() => onDisconnectSource(selectedSource.id)}
|
|
6517
8194
|
>
|
|
6518
|
-
{
|
|
8195
|
+
{selectedSourceDisconnectControlPending ? (
|
|
6519
8196
|
<Spinner className="mr-1 size-3.5" />
|
|
6520
8197
|
) : (
|
|
6521
8198
|
<IconX className="mr-1 size-3.5" />
|
|
@@ -6546,7 +8223,7 @@ function SourcesListView({
|
|
|
6546
8223
|
builderConfigured: boolean;
|
|
6547
8224
|
builderSpaceLabel: string | null;
|
|
6548
8225
|
reviewableCount: number;
|
|
6549
|
-
onOpenBuilder: () => void;
|
|
8226
|
+
onOpenBuilder: (source?: ContentDatabaseSource) => void;
|
|
6550
8227
|
onOpenNotion: () => void;
|
|
6551
8228
|
onOpenSecondary: (source: ContentDatabaseSource) => void;
|
|
6552
8229
|
onAddSource: () => void;
|
|
@@ -6581,7 +8258,7 @@ function SourcesListView({
|
|
|
6581
8258
|
connected.metadata.federation?.role === "secondary"
|
|
6582
8259
|
? () => onOpenSecondary(connected)
|
|
6583
8260
|
: connected.sourceType === "builder-cms"
|
|
6584
|
-
? onOpenBuilder
|
|
8261
|
+
? () => onOpenBuilder(connected)
|
|
6585
8262
|
: undefined
|
|
6586
8263
|
}
|
|
6587
8264
|
disabled={
|
|
@@ -7211,12 +8888,7 @@ function BuilderSourceContinuationBar({
|
|
|
7211
8888
|
canEdit: boolean;
|
|
7212
8889
|
pending: boolean;
|
|
7213
8890
|
clientError: boolean;
|
|
7214
|
-
progressHighWater
|
|
7215
|
-
sourceId: string | null;
|
|
7216
|
-
fetchedCount: number;
|
|
7217
|
-
hydratedCount: number;
|
|
7218
|
-
rowsComplete: boolean;
|
|
7219
|
-
};
|
|
8891
|
+
progressHighWater?: BuilderSourceProgressHighWater;
|
|
7220
8892
|
onRetry: () => void;
|
|
7221
8893
|
}) {
|
|
7222
8894
|
if (
|
|
@@ -7228,8 +8900,7 @@ function BuilderSourceContinuationBar({
|
|
|
7228
8900
|
}
|
|
7229
8901
|
const rowsComplete =
|
|
7230
8902
|
source.metadata.lastReadHasMore === false ||
|
|
7231
|
-
|
|
7232
|
-
progressHighWater.rowsComplete);
|
|
8903
|
+
progressHighWater?.rowsComplete === true;
|
|
7233
8904
|
const rawStatus = clientError ? "error" : builderSourceRowFetchStatus(source);
|
|
7234
8905
|
const status = rowsComplete && rawStatus === "fetching" ? null : rawStatus;
|
|
7235
8906
|
const bodyHydration = source.bodyHydration;
|
|
@@ -7242,10 +8913,7 @@ function BuilderSourceContinuationBar({
|
|
|
7242
8913
|
const hydratedCount = bodyHydration
|
|
7243
8914
|
? builderBodyHydrationDisplayHydratedCount({
|
|
7244
8915
|
summary: bodyHydration,
|
|
7245
|
-
highWaterCount:
|
|
7246
|
-
progressHighWater.sourceId === source.id
|
|
7247
|
-
? progressHighWater.hydratedCount
|
|
7248
|
-
: 0,
|
|
8916
|
+
highWaterCount: progressHighWater?.hydratedCount ?? 0,
|
|
7249
8917
|
})
|
|
7250
8918
|
: 0;
|
|
7251
8919
|
const fetchedCount =
|
|
@@ -7253,9 +8921,7 @@ function BuilderSourceContinuationBar({
|
|
|
7253
8921
|
? Math.max(
|
|
7254
8922
|
rawFetchedCount ?? 0,
|
|
7255
8923
|
bodyHydration.total,
|
|
7256
|
-
progressHighWater
|
|
7257
|
-
? progressHighWater.fetchedCount
|
|
7258
|
-
: 0,
|
|
8924
|
+
progressHighWater?.fetchedCount ?? 0,
|
|
7259
8925
|
)
|
|
7260
8926
|
: rawFetchedCount;
|
|
7261
8927
|
const hasMore = source.metadata.lastReadHasMore === true && !rowsComplete;
|
|
@@ -7502,6 +9168,105 @@ function sourceFieldIconType(
|
|
|
7502
9168
|
return "text";
|
|
7503
9169
|
}
|
|
7504
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
|
+
|
|
7505
9270
|
function SourceDetailsFieldPicker({
|
|
7506
9271
|
documentId,
|
|
7507
9272
|
source,
|
|
@@ -7586,7 +9351,6 @@ function SourceDetailsFieldPicker({
|
|
|
7586
9351
|
});
|
|
7587
9352
|
}
|
|
7588
9353
|
};
|
|
7589
|
-
|
|
7590
9354
|
return (
|
|
7591
9355
|
<div className="grid min-w-0 gap-3">
|
|
7592
9356
|
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3">
|
|
@@ -7597,6 +9361,13 @@ function SourceDetailsFieldPicker({
|
|
|
7597
9361
|
{dbText("pickDetailsBecomeColumns")}
|
|
7598
9362
|
</div>
|
|
7599
9363
|
</div>
|
|
9364
|
+
<BuilderRequiredFieldsCard
|
|
9365
|
+
documentId={documentId}
|
|
9366
|
+
source={source}
|
|
9367
|
+
canEdit={canEdit}
|
|
9368
|
+
pending={pending}
|
|
9369
|
+
onDone={onDone}
|
|
9370
|
+
/>
|
|
7600
9371
|
{fields.length === 0 ? (
|
|
7601
9372
|
<div className="break-words text-xs text-muted-foreground">
|
|
7602
9373
|
{dbText("allAvailableDetailFieldsAlreadyVisible")}
|
|
@@ -7667,10 +9438,10 @@ function SourceDetailsFieldPicker({
|
|
|
7667
9438
|
// A Builder space's data models, as drill-in rows. The attached model (if any)
|
|
7668
9439
|
// is marked; selecting a row opens that model's leaf.
|
|
7669
9440
|
function BuilderSpaceModelsView({
|
|
7670
|
-
|
|
9441
|
+
attachedModelNames,
|
|
7671
9442
|
onOpenModel,
|
|
7672
9443
|
}: {
|
|
7673
|
-
|
|
9444
|
+
attachedModelNames: string[];
|
|
7674
9445
|
onOpenModel: (model: BuilderCmsModelSummary) => void;
|
|
7675
9446
|
}) {
|
|
7676
9447
|
const modelsQuery = useBuilderCmsModels(true);
|
|
@@ -7678,22 +9449,27 @@ function BuilderSpaceModelsView({
|
|
|
7678
9449
|
const [query, setQuery] = useState("");
|
|
7679
9450
|
|
|
7680
9451
|
if (modelsQuery.isLoading) {
|
|
7681
|
-
if (
|
|
9452
|
+
if (attachedModelNames.length > 0) {
|
|
7682
9453
|
return (
|
|
7683
9454
|
<div className="grid min-w-0 gap-2">
|
|
7684
9455
|
<div className="grid min-w-0 gap-1.5">
|
|
7685
9456
|
<div className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
7686
9457
|
{dbText("alreadyAttached")}
|
|
7687
9458
|
</div>
|
|
7688
|
-
|
|
7689
|
-
<
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
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
|
+
))}
|
|
7697
9473
|
</div>
|
|
7698
9474
|
<div className="flex min-w-0 items-center gap-2 px-2 text-xs text-muted-foreground">
|
|
7699
9475
|
<Spinner className="size-3.5" />
|
|
@@ -7767,15 +9543,16 @@ function BuilderSpaceModelsView({
|
|
|
7767
9543
|
model.displayName.toLowerCase().includes(normalizedQuery) ||
|
|
7768
9544
|
model.name.toLowerCase().includes(normalizedQuery);
|
|
7769
9545
|
const filtered = models.filter(matchesQuery);
|
|
7770
|
-
const
|
|
7771
|
-
|
|
9546
|
+
const attachedModelNameSet = new Set(attachedModelNames);
|
|
9547
|
+
const attachedModels = filtered.filter((model) =>
|
|
9548
|
+
attachedModelNameSet.has(model.name),
|
|
7772
9549
|
);
|
|
7773
9550
|
const otherModels = filtered.filter(
|
|
7774
|
-
(model) =>
|
|
9551
|
+
(model) => !attachedModelNameSet.has(model.name),
|
|
7775
9552
|
);
|
|
7776
9553
|
|
|
7777
9554
|
const renderRow = (model: BuilderCmsModelSummary) => {
|
|
7778
|
-
const isAttached =
|
|
9555
|
+
const isAttached = attachedModelNameSet.has(model.name);
|
|
7779
9556
|
return (
|
|
7780
9557
|
<button
|
|
7781
9558
|
key={model.id}
|