@agent-native/core 0.133.2 → 0.133.3

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.
Files changed (58) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/use-action.ts +6 -0
  5. package/corpus/templates/content/actions/_batch-utils.ts +33 -0
  6. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -81
  7. package/corpus/templates/content/actions/_content-files.ts +31 -21
  8. package/corpus/templates/content/actions/_database-source-utils.ts +653 -155
  9. package/corpus/templates/content/actions/_database-utils.ts +535 -41
  10. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +291 -240
  11. package/corpus/templates/content/actions/add-database-item.ts +15 -1
  12. package/corpus/templates/content/actions/attach-content-database-source.ts +156 -58
  13. package/corpus/templates/content/actions/bind-content-database-source-field.ts +2 -2
  14. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +4 -1
  15. package/corpus/templates/content/actions/delete-database-items.ts +8 -2
  16. package/corpus/templates/content/actions/disconnect-content-database-source.ts +2 -2
  17. package/corpus/templates/content/actions/duplicate-database-item.ts +15 -1
  18. package/corpus/templates/content/actions/duplicate-database-items.ts +19 -1
  19. package/corpus/templates/content/actions/execute-builder-source-execution.ts +2 -1
  20. package/corpus/templates/content/actions/get-content-database.ts +12 -64
  21. package/corpus/templates/content/actions/move-database-item.ts +4 -1
  22. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +1 -1
  23. package/corpus/templates/content/actions/prepare-builder-source-review.ts +4 -8
  24. package/corpus/templates/content/actions/preview-content-database-source-attach.ts +92 -0
  25. package/corpus/templates/content/actions/process-builder-body-hydration.ts +2 -1
  26. package/corpus/templates/content/actions/query-content-database-items.ts +64 -0
  27. package/corpus/templates/content/actions/refresh-content-database-source.ts +7 -0
  28. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +1 -1
  29. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +1 -1
  30. package/corpus/templates/content/actions/stage-builder-revision.ts +1 -1
  31. package/corpus/templates/content/actions/update-content-database-personal-view.ts +29 -3
  32. package/corpus/templates/content/actions/update-content-database-view.ts +1 -1
  33. package/corpus/templates/content/actions/validate-builder-source-execution.ts +1 -1
  34. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +3 -22
  35. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +267 -105
  36. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +82 -1
  37. package/corpus/templates/content/app/hooks/use-content-database.ts +285 -24
  38. package/corpus/templates/content/app/hooks/use-document-properties.ts +7 -6
  39. package/corpus/templates/content/app/i18n-data.ts +10 -0
  40. package/corpus/templates/content/changelog/2026-07-29-large-databases-keep-useful-rows-visible.md +6 -0
  41. package/corpus/templates/content/changelog/2026-07-30-builder-source-columns-now-appear-immediately-when-connected.md +6 -0
  42. package/corpus/templates/content/changelog/2026-07-30-large-builder-backed-tables-show-useful-rows-sooner-and-fini.md +6 -0
  43. package/corpus/templates/content/changelog/2026-08-01-large-builder-databases-now-show-rows-immediately-and-finish.md +6 -0
  44. package/corpus/templates/content/parity/matrix.md +2 -1
  45. package/corpus/templates/content/parity/matrix.ts +25 -0
  46. package/corpus/templates/content/shared/api.ts +54 -4
  47. package/corpus/templates/content/shared/database-query.ts +359 -0
  48. package/dist/client/use-action.d.ts.map +1 -1
  49. package/dist/client/use-action.js +6 -0
  50. package/dist/client/use-action.js.map +1 -1
  51. package/dist/notifications/routes.d.ts +1 -1
  52. package/dist/observability/routes.d.ts +3 -3
  53. package/dist/progress/routes.d.ts +1 -1
  54. package/dist/resources/handlers.d.ts +1 -1
  55. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  56. package/dist/server/transcribe-voice.d.ts +1 -1
  57. package/package.json +1 -1
  58. package/src/client/use-action.ts +6 -0
@@ -15,6 +15,7 @@ import {
15
15
  type BuilderCmsModelSummary,
16
16
  type ContentDatabaseItem,
17
17
  type ContentDatabaseResponse,
18
+ type ContentDatabaseSourceAttachmentResult,
18
19
  type ContentDatabaseSource,
19
20
  type ContentDatabaseSourceChangeSet,
20
21
  type ContentDatabaseSourceJoinRequest,
@@ -34,6 +35,7 @@ import {
34
35
  type ContentDatabaseRowDensity,
35
36
  type ContentDatabaseSort,
36
37
  type ContentDatabaseSortDirection,
38
+ type ContentDatabaseTableQuery,
37
39
  type ContentDatabaseViewType,
38
40
  type Document,
39
41
  type DocumentProperty,
@@ -42,6 +44,7 @@ import {
42
44
  type DocumentPropertyValue,
43
45
  } from "@shared/api";
44
46
  import { contentDatabaseFormQuestions } from "@shared/database-form";
47
+ import { applyContentDatabaseTableQuery } from "@shared/database-query";
45
48
  import {
46
49
  type DocumentPropertyOptionColor,
47
50
  countWords,
@@ -165,6 +168,7 @@ import {
165
168
  useAddDatabaseItem,
166
169
  useAddContentDatabaseSourceFieldProperty,
167
170
  useAttachContentDatabaseSource,
171
+ useBuilderCmsAttachPreview,
168
172
  useBuilderCmsModels,
169
173
  useCancelPreparedBuilderSourceUpdate,
170
174
  useChangeContentDatabaseSourceRole,
@@ -188,6 +192,7 @@ import {
188
192
  useSuggestSourceJoinKey,
189
193
  useUpdateContentDatabasePersonalView,
190
194
  useUpdateContentDatabaseView,
195
+ writeBuilderAttachPreviewToCache,
191
196
  } from "@/hooks/use-content-database";
192
197
  import {
193
198
  useContentSpaces,
@@ -441,6 +446,7 @@ export function databaseCreatedItemForImmediatePreview(
441
446
  now?: string;
442
447
  },
443
448
  ): ContentDatabaseItem | null {
449
+ if (response.createdItem) return response.createdItem;
444
450
  const returnedItem = response.items.find(
445
451
  (item) => item.id === response.createdItemId,
446
452
  );
@@ -769,15 +775,51 @@ function DatabaseTable({
769
775
  SELECTED_CONTENT_SPACE_STORAGE_KEY,
770
776
  null,
771
777
  );
778
+ const [searchQuery, setSearchQuery] = useState("");
779
+ const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
780
+ defaultDatabaseViewConfig(),
781
+ );
782
+ const activeView = useMemo(
783
+ () => activeDatabaseView(viewConfig),
784
+ [viewConfig],
785
+ );
786
+ const tableQuery = useMemo<ContentDatabaseTableQuery | undefined>(() => {
787
+ if (
788
+ activeView.type !== "table" ||
789
+ activeDatabaseConstraintCount(
790
+ searchQuery,
791
+ activeView.sorts,
792
+ activeView.filters,
793
+ ) === 0
794
+ ) {
795
+ return undefined;
796
+ }
797
+ return {
798
+ search: searchQuery,
799
+ filters: activeView.filters,
800
+ sorts: activeView.sorts,
801
+ filterMode: activeView.filterMode ?? "and",
802
+ };
803
+ }, [activeView, searchQuery]);
772
804
  const [manualDatabaseItemLimit, setManualDatabaseItemLimit] = useState(
773
805
  CONTENT_DATABASE_PAGE_SIZE,
774
806
  );
775
807
  const [databaseRequestItemLimit, setDatabaseRequestItemLimit] = useState(
776
808
  CONTENT_DATABASE_PAGE_SIZE,
777
809
  );
778
- const database = useContentDatabase(document.id, databaseRequestItemLimit);
810
+ const database = useContentDatabase(
811
+ document.id,
812
+ databaseRequestItemLimit,
813
+ tableQuery,
814
+ );
815
+ // A deleted/missing database resolves to the unavailable union (no
816
+ // `database` field) — treat it as no data; the inline-block wrapper owns
817
+ // the user-facing "Database unavailable" state.
818
+ const data = isContentDatabaseUnavailable(database.data)
819
+ ? undefined
820
+ : database.data;
779
821
  const addItem = useAddDatabaseItem(document.id);
780
- const attachSource = useAttachContentDatabaseSource(document.id);
822
+ const attachSource = useAttachContentDatabaseSource(document.id, data);
781
823
  const changeSourceRole = useChangeContentDatabaseSourceRole(document.id);
782
824
  const refreshSource = useRefreshContentDatabaseSource(document.id);
783
825
  const disconnectSource = useDisconnectContentDatabaseSource(document.id);
@@ -794,12 +836,8 @@ function DatabaseTable({
794
836
  document.id,
795
837
  );
796
838
  const updateView = useUpdateContentDatabaseView(document.id);
797
- // A deleted/missing database resolves to the unavailable union (no
798
- // `database` field) treat it as no data; the inline-block wrapper owns
799
- // the user-facing "Database unavailable" state.
800
- const data = isContentDatabaseUnavailable(database.data)
801
- ? undefined
802
- : database.data;
839
+ const attachPreviewActive = Boolean(data?.attachPreview);
840
+ const effectiveCanEdit = canEdit && !attachPreviewActive;
803
841
  const isWorkspaceCatalog = data?.database.systemRole === "workspaces";
804
842
  const isCreatingDatabaseItem = addItem.isPending;
805
843
  const isDatabaseInitialLoading = database.isLoading && !data;
@@ -841,7 +879,6 @@ function DatabaseTable({
841
879
  string | null
842
880
  >(null);
843
881
  const [searchOpen, setSearchOpen] = useState(false);
844
- const [searchQuery, setSearchQuery] = useState("");
845
882
  const [settingsOpen, setSettingsOpen] = useState(false);
846
883
  const [inlineFilterControlsOpen, setInlineFilterControlsOpen] =
847
884
  useState(false);
@@ -876,19 +913,12 @@ function DatabaseTable({
876
913
  useState<Record<string, string> | null>(null);
877
914
  const [settingsPanel, setSettingsPanel] =
878
915
  useState<DatabaseSettingsPanel>("main");
879
- const [viewConfig, setViewConfig] = useState<ContentDatabaseViewConfig>(
880
- defaultDatabaseViewConfig(),
881
- );
882
916
  const [savedViewConfig, setSavedViewConfig] =
883
917
  useState<ContentDatabaseViewConfig>(defaultDatabaseViewConfig());
884
918
  const [personalQueryDirty, setPersonalQueryDirty] = useState(false);
885
919
  const [dateViewMonth, setDateViewMonth] = useState(() =>
886
920
  startOfMonth(new Date()),
887
921
  );
888
- const activeView = useMemo(
889
- () => activeDatabaseView(viewConfig),
890
- [viewConfig],
891
- );
892
922
  const orderedProperties = useMemo(
893
923
  () => orderDatabasePropertiesForView(properties, activeView),
894
924
  [properties, activeView],
@@ -996,10 +1026,12 @@ function DatabaseTable({
996
1026
  visibleFilters,
997
1027
  );
998
1028
  const requiresCompleteClientDataset =
999
- activeConstraintCount > 0 ||
1000
- !!databaseGroupProperty ||
1001
- activeView.type === "calendar" ||
1002
- activeView.type === "timeline";
1029
+ databaseViewRequiresCompleteClientDataset({
1030
+ viewType: activeView.type,
1031
+ activeConstraintCount,
1032
+ grouped: !!databaseGroupProperty,
1033
+ tableQueryMode: data?.tableQueryMode,
1034
+ });
1003
1035
  const clientQueryExpandedItemLimit = databaseClientQueryExpandedItemLimit(
1004
1036
  requiresCompleteClientDataset,
1005
1037
  manualDatabaseItemLimit,
@@ -1011,7 +1043,19 @@ function DatabaseTable({
1011
1043
  data?.pagination?.limit ?? items.length,
1012
1044
  );
1013
1045
  const isDatabaseViewLoading =
1014
- isDatabaseInitialLoading || isClientQueryExpansionPending;
1046
+ isDatabaseInitialLoading ||
1047
+ isClientQueryExpansionPending ||
1048
+ (database.isFetching && Boolean(tableQuery));
1049
+ useEffect(() => {
1050
+ if (isDatabaseViewLoading) return;
1051
+ window.document.documentElement.dataset.contentDatabaseRowsVisibleDocumentId =
1052
+ document.id;
1053
+ window.dispatchEvent(
1054
+ new CustomEvent("content-database-rows-visible", {
1055
+ detail: { documentId: document.id },
1056
+ }),
1057
+ );
1058
+ }, [document.id, isDatabaseViewLoading]);
1015
1059
  useEffect(() => {
1016
1060
  if (clientQueryExpandedItemLimit === databaseRequestItemLimit) return;
1017
1061
  setDatabaseRequestItemLimit(clientQueryExpandedItemLimit);
@@ -1072,6 +1116,8 @@ function DatabaseTable({
1072
1116
  () => builderReviewableChangeSets(builderReviewSource),
1073
1117
  [builderReviewSource],
1074
1118
  );
1119
+ const builderReviewCountIsComplete =
1120
+ databaseSourceChangeSetsAreComplete(builderReviewSource);
1075
1121
  const builderReviewPreview = useMemo(
1076
1122
  () =>
1077
1123
  builderReviewSource && builderReviewChangeSets.length > 0
@@ -1082,11 +1128,12 @@ function DatabaseTable({
1082
1128
  : null,
1083
1129
  [builderReviewChangeSets, builderReviewSource],
1084
1130
  );
1085
- const activeBuilderReview =
1086
- builderReviewResult ??
1087
- (builderReviewOpen
1088
- ? (completeBuilderReviewPreview?.review ?? null)
1089
- : builderReviewPreview);
1131
+ const activeBuilderReview = databaseActiveBuilderReview({
1132
+ prepared: builderReviewResult,
1133
+ complete: completeBuilderReviewPreview?.review ?? null,
1134
+ page: builderReviewPreview,
1135
+ open: builderReviewOpen,
1136
+ });
1090
1137
  const sourcePendingOperations: DatabaseSourcePendingOperations = {
1091
1138
  attach: attachSource.isPending,
1092
1139
  changeRole: changeSourceRole.isPending,
@@ -1135,6 +1182,7 @@ function DatabaseTable({
1135
1182
  onError?: () => void,
1136
1183
  expectedBuilderContinuationOffset?: number,
1137
1184
  onSuccess?: () => void,
1185
+ fullRefresh = false,
1138
1186
  ) => {
1139
1187
  if (!acquireDatabaseSourceOperation(refreshSourceInFlightRef, sourceId)) {
1140
1188
  return false;
@@ -1144,6 +1192,8 @@ function DatabaseTable({
1144
1192
  documentId: document.id,
1145
1193
  sourceId,
1146
1194
  expectedBuilderContinuationOffset,
1195
+ fullRefresh: fullRefresh || undefined,
1196
+ finishBuilderPagination: fullRefresh || undefined,
1147
1197
  },
1148
1198
  {
1149
1199
  onError,
@@ -1190,19 +1240,35 @@ function DatabaseTable({
1190
1240
  ) {
1191
1241
  return false;
1192
1242
  }
1193
- processBuilderBodies.mutate(
1194
- { sourceId, ...request },
1195
- {
1196
- onSuccess: options.onSuccess,
1197
- onError: options.onError,
1198
- onSettled: () => {
1199
- releaseDatabaseSourceOperation(
1200
- hydrationSourceInFlightRef,
1201
- sourceId,
1202
- );
1243
+ const pump = () => {
1244
+ let result: ProcessBuilderBodyHydrationResponse | null = null;
1245
+ processBuilderBodies.mutate(
1246
+ { sourceId, ...request },
1247
+ {
1248
+ onSuccess: (nextResult) => {
1249
+ result = nextResult;
1250
+ options.onSuccess?.(nextResult);
1251
+ },
1252
+ onError: options.onError,
1253
+ onSettled: () => {
1254
+ if (
1255
+ !request.documentId &&
1256
+ result &&
1257
+ builderBodyHydrationMutationMadeProgress(result) &&
1258
+ result.remaining > 0
1259
+ ) {
1260
+ pump();
1261
+ return;
1262
+ }
1263
+ releaseDatabaseSourceOperation(
1264
+ hydrationSourceInFlightRef,
1265
+ sourceId,
1266
+ );
1267
+ },
1203
1268
  },
1204
- },
1205
- );
1269
+ );
1270
+ };
1271
+ pump();
1206
1272
  return true;
1207
1273
  },
1208
1274
  [processBuilderBodies.mutate],
@@ -1246,6 +1312,7 @@ function DatabaseTable({
1246
1312
  () => handleBuilderContinuationError(continuationKey),
1247
1313
  candidate.metadata.lastReadNextOffset,
1248
1314
  () => handleBuilderContinuationSuccess(continuationKey),
1315
+ true,
1249
1316
  )
1250
1317
  ) {
1251
1318
  autoContinueBuilderSourceRef.current.delete(continuationKey);
@@ -1325,6 +1392,7 @@ function DatabaseTable({
1325
1392
  () => handleBuilderContinuationError(continuationKey),
1326
1393
  candidate.metadata.lastReadNextOffset,
1327
1394
  () => handleBuilderContinuationSuccess(continuationKey),
1395
+ true,
1328
1396
  )
1329
1397
  ) {
1330
1398
  builderContinuationWatchdogRef.current.set(
@@ -2264,7 +2332,7 @@ function DatabaseTable({
2264
2332
  : viewConfig;
2265
2333
  const nextKey = databaseViewStateKey(databaseId, sharedViewConfig);
2266
2334
  if (databaseViewStateKey(databaseId, savedViewConfig) === nextKey) return;
2267
- if (!canEdit) return;
2335
+ if (!effectiveCanEdit) return;
2268
2336
  if (saveViewTimerRef.current) {
2269
2337
  clearTimeout(saveViewTimerRef.current);
2270
2338
  }
@@ -2344,7 +2412,7 @@ function DatabaseTable({
2344
2412
  <div className="mb-1 flex min-h-8 flex-wrap items-center justify-between gap-x-3 gap-y-1 pb-1">
2345
2413
  <DatabaseViewTabs
2346
2414
  viewConfig={viewConfig}
2347
- canEdit={canEdit}
2415
+ canEdit={effectiveCanEdit}
2348
2416
  onViewConfigChange={setViewConfig}
2349
2417
  />
2350
2418
  <div className="flex max-w-full flex-wrap items-center justify-end gap-1">
@@ -2460,12 +2528,16 @@ function DatabaseTable({
2460
2528
  size="sm"
2461
2529
  aria-label={
2462
2530
  builderReviewChangeSets.length > 0
2463
- ? `Database settings, ${builderReviewChangeSets.length} Builder update pending`
2531
+ ? builderReviewCountIsComplete
2532
+ ? `Database settings, ${builderReviewChangeSets.length} Builder update pending`
2533
+ : "Database settings, Builder updates pending"
2464
2534
  : "Database settings"
2465
2535
  }
2466
2536
  title={
2467
2537
  builderReviewChangeSets.length > 0
2468
- ? `${builderReviewChangeSets.length} Builder update pending`
2538
+ ? builderReviewCountIsComplete
2539
+ ? `${builderReviewChangeSets.length} Builder update pending`
2540
+ : "Builder updates pending"
2469
2541
  : "Database settings"
2470
2542
  }
2471
2543
  className={cn(
@@ -2485,12 +2557,24 @@ function DatabaseTable({
2485
2557
  >
2486
2558
  <IconAdjustmentsHorizontal className="size-3.5" />
2487
2559
  {builderReviewChangeSets.length > 0 ? (
2488
- <span className="absolute -right-0.5 -top-0.5 flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-foreground px-1 text-[9px] leading-none text-background">
2489
- {formatCompactCountBadge(builderReviewChangeSets.length)}
2490
- </span>
2560
+ builderReviewCountIsComplete ? (
2561
+ <span className="absolute -right-0.5 -top-0.5 flex h-3.5 min-w-3.5 shrink-0 items-center justify-center rounded-full bg-foreground px-1 text-[9px] leading-none text-background">
2562
+ {formatCompactCountBadge(builderReviewChangeSets.length)}
2563
+ </span>
2564
+ ) : (
2565
+ <span className="absolute right-0 top-0 size-2 rounded-full bg-foreground" />
2566
+ )
2491
2567
  ) : null}
2492
2568
  </Button>
2493
- {canEdit && isWorkspaceCatalog ? (
2569
+ {data?.attachPreview ? (
2570
+ <span className="rounded border border-border px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
2571
+ {data.attachPreview.complete &&
2572
+ typeof data.attachPreview.importedItemCount === "number"
2573
+ ? `${data.attachPreview.importedItemCount} rows fetched`
2574
+ : dbText("attachingReadOnly")}
2575
+ </span>
2576
+ ) : null}
2577
+ {effectiveCanEdit && isWorkspaceCatalog ? (
2494
2578
  <WorkspaceSourceMenu
2495
2579
  align="end"
2496
2580
  propertyValues={workspaceCreationPropertyValues}
@@ -2504,7 +2588,7 @@ function DatabaseTable({
2504
2588
  New
2505
2589
  </Button>
2506
2590
  </WorkspaceSourceMenu>
2507
- ) : canEdit ? (
2591
+ ) : effectiveCanEdit ? (
2508
2592
  <Button
2509
2593
  type="button"
2510
2594
  size="sm"
@@ -2582,7 +2666,7 @@ function DatabaseTable({
2582
2666
  <BuilderSourceContinuationBar
2583
2667
  key={builderSource.id}
2584
2668
  source={builderSource}
2585
- canEdit={canEdit}
2669
+ canEdit={effectiveCanEdit}
2586
2670
  pending={
2587
2671
  sourcePendingOperations.refreshSourceId === builderSource.id
2588
2672
  }
@@ -2607,6 +2691,7 @@ function DatabaseTable({
2607
2691
  () => handleBuilderContinuationError(continuationKey),
2608
2692
  builderSource.metadata.lastReadNextOffset,
2609
2693
  () => handleBuilderContinuationSuccess(continuationKey),
2694
+ true,
2610
2695
  );
2611
2696
  }}
2612
2697
  />
@@ -2620,7 +2705,7 @@ function DatabaseTable({
2620
2705
  databaseTitle={data?.database.title ?? document.title}
2621
2706
  view={activeView}
2622
2707
  properties={orderedProperties}
2623
- canEdit={canEdit}
2708
+ canEdit={effectiveCanEdit}
2624
2709
  />
2625
2710
  ) : activeView.type === "board" ? (
2626
2711
  <DatabaseBoardView
@@ -2630,7 +2715,7 @@ function DatabaseTable({
2630
2715
  items={visibleItems}
2631
2716
  groupProperty={boardGroupProperty}
2632
2717
  databaseDocumentId={document.id}
2633
- canEdit={canEdit}
2718
+ canEdit={effectiveCanEdit}
2634
2719
  canCreateItems={!isWorkspaceCatalog}
2635
2720
  isLoading={isDatabaseViewLoading}
2636
2721
  isCreating={isCreatingDatabaseItem || setProperty.isPending}
@@ -2659,7 +2744,7 @@ function DatabaseTable({
2659
2744
  groupableProperties={orderedProperties}
2660
2745
  items={visibleItems}
2661
2746
  databaseDocumentId={document.id}
2662
- canEdit={canEdit}
2747
+ canEdit={effectiveCanEdit}
2663
2748
  canCreateItems={!isWorkspaceCatalog}
2664
2749
  isLoading={isDatabaseViewLoading}
2665
2750
  isCreating={isCreatingDatabaseItem}
@@ -2683,7 +2768,7 @@ function DatabaseTable({
2683
2768
  groupableProperties={orderedProperties}
2684
2769
  items={visibleItems}
2685
2770
  databaseDocumentId={document.id}
2686
- canEdit={canEdit}
2771
+ canEdit={effectiveCanEdit}
2687
2772
  canCreateItems={!isWorkspaceCatalog}
2688
2773
  isLoading={isDatabaseViewLoading}
2689
2774
  isCreating={isCreatingDatabaseItem}
@@ -2708,7 +2793,7 @@ function DatabaseTable({
2708
2793
  properties={orderedProperties}
2709
2794
  items={visibleItems}
2710
2795
  databaseDocumentId={document.id}
2711
- canEdit={canEdit}
2796
+ canEdit={effectiveCanEdit}
2712
2797
  canCreateItems={!isWorkspaceCatalog}
2713
2798
  isLoading={isDatabaseViewLoading}
2714
2799
  isCreating={isCreatingDatabaseItem || setProperty.isPending}
@@ -2736,7 +2821,7 @@ function DatabaseTable({
2736
2821
  properties={orderedProperties}
2737
2822
  items={visibleItems}
2738
2823
  databaseDocumentId={document.id}
2739
- canEdit={canEdit}
2824
+ canEdit={effectiveCanEdit}
2740
2825
  canCreateItems={!isWorkspaceCatalog}
2741
2826
  isLoading={isDatabaseViewLoading}
2742
2827
  isCreating={isCreatingDatabaseItem || setProperty.isPending}
@@ -2773,7 +2858,7 @@ function DatabaseTable({
2773
2858
  source={source}
2774
2859
  sources={sources}
2775
2860
  databaseDocumentId={document.id}
2776
- canEdit={canEdit}
2861
+ canEdit={effectiveCanEdit}
2777
2862
  workspaceCreationPropertyValues={workspaceCreationPropertyValues}
2778
2863
  isLoading={isDatabaseViewLoading}
2779
2864
  isCreating={isCreatingDatabaseItem}
@@ -2894,6 +2979,7 @@ function DatabaseTable({
2894
2979
  sourceType: "builder-cms",
2895
2980
  sourceName: model.displayName,
2896
2981
  sourceTable: model.name,
2982
+ builderFieldPaths: model.fields.map((field) => field.name),
2897
2983
  relationshipMode,
2898
2984
  mode:
2899
2985
  relationshipMode === "items"
@@ -3896,13 +3982,18 @@ export function databaseBulkScalarInputState(
3896
3982
  type DuplicatedDatabaseItemResponse = {
3897
3983
  items: ContentDatabaseResponse["items"];
3898
3984
  duplicatedItemId?: ContentDatabaseResponse["duplicatedItemId"];
3985
+ duplicatedItems?: ContentDatabaseResponse["duplicatedItems"];
3899
3986
  };
3900
3987
 
3901
3988
  export function databaseDuplicatedItemFromResponse(
3902
3989
  response: DuplicatedDatabaseItemResponse,
3903
3990
  ) {
3904
3991
  return (
3905
- response.items.find((item) => item.id === response.duplicatedItemId) ?? null
3992
+ response.duplicatedItems?.find(
3993
+ (item) => item.id === response.duplicatedItemId,
3994
+ ) ??
3995
+ response.items.find((item) => item.id === response.duplicatedItemId) ??
3996
+ null
3906
3997
  );
3907
3998
  }
3908
3999
 
@@ -5697,7 +5788,7 @@ function DatabaseTableView({
5697
5788
  ) : null}
5698
5789
  </div>
5699
5790
 
5700
- {isLoading ? (
5791
+ {databaseTableShouldShowBlockingLoader(isLoading, items.length) ? (
5701
5792
  <div className="flex h-16 items-center gap-2 border-t border-border px-2 text-sm text-muted-foreground">
5702
5793
  <Spinner className="size-4" />
5703
5794
  {dbText("loadingDatabase")}
@@ -5863,6 +5954,29 @@ function DatabaseTableView({
5863
5954
  );
5864
5955
  }
5865
5956
 
5957
+ export function databaseTableShouldShowBlockingLoader(
5958
+ isLoading: boolean,
5959
+ itemCount: number,
5960
+ ) {
5961
+ return isLoading && itemCount === 0;
5962
+ }
5963
+
5964
+ export function databaseViewRequiresCompleteClientDataset(args: {
5965
+ viewType: ContentDatabaseViewType;
5966
+ activeConstraintCount: number;
5967
+ grouped: boolean;
5968
+ tableQueryMode?: ContentDatabaseResponse["tableQueryMode"];
5969
+ }) {
5970
+ return (
5971
+ (args.viewType === "table" && args.tableQueryMode === "client-required") ||
5972
+ (args.viewType !== "table" &&
5973
+ (args.activeConstraintCount > 0 ||
5974
+ args.grouped ||
5975
+ args.viewType === "calendar" ||
5976
+ args.viewType === "timeline"))
5977
+ );
5978
+ }
5979
+
5866
5980
  function DatabaseActiveConstraintsBar({
5867
5981
  documentId,
5868
5982
  items,
@@ -7305,11 +7419,11 @@ function DatabaseSettingsPanelSheet({
7305
7419
  onAttachBuilderSource: (
7306
7420
  model: BuilderCmsModelSummary,
7307
7421
  relationshipMode?: "items" | "details",
7308
- ) => Promise<ContentDatabaseResponse>;
7422
+ ) => Promise<ContentDatabaseSourceAttachmentResult>;
7309
7423
  onFederateSource: (
7310
7424
  candidate: PendingSourceCandidate,
7311
7425
  join: ContentDatabaseSourceJoinRequest,
7312
- ) => Promise<ContentDatabaseResponse>;
7426
+ ) => Promise<ContentDatabaseSourceAttachmentResult>;
7313
7427
  onChangeSourceRole: (
7314
7428
  sourceId: string,
7315
7429
  relationshipMode: "items" | "details",
@@ -7333,10 +7447,44 @@ function DatabaseSettingsPanelSheet({
7333
7447
  onHideEmptyGroupsChange: (hideEmptyGroups: boolean) => void;
7334
7448
  onGroupsCollapsedChange: (groupIds: string[], collapsed: boolean) => void;
7335
7449
  }) {
7450
+ const queryClient = useQueryClient();
7336
7451
  // Local drill-down path *within* the Source(s) panel. Kept here (not in the
7337
7452
  // flat panel enum) because the levels are dynamic — space/model names aren't
7338
7453
  // known at compile time. The sheet's back button pops this stack first.
7339
7454
  const [sourceNavStack, setSourceNavStack] = useState<SourceNavStep[]>([]);
7455
+ const sourceNavTop = sourceNavStack[sourceNavStack.length - 1];
7456
+ const previewModel =
7457
+ sourceNavTop?.kind === "model" ? (sourceNavTop.model ?? null) : null;
7458
+ const previewFieldPaths = useMemo(
7459
+ () => previewModel?.fields.map((field) => field.name),
7460
+ [previewModel],
7461
+ );
7462
+ const previewAlreadyAttached = previewModel
7463
+ ? Boolean(
7464
+ databaseAttachedBuilderSource(sources, source, {
7465
+ modelName: previewModel.name,
7466
+ }),
7467
+ )
7468
+ : false;
7469
+ const builderAttachPreview = useBuilderCmsAttachPreview({
7470
+ documentId,
7471
+ sourceTable: previewModel?.name ?? null,
7472
+ fieldPaths: previewFieldPaths,
7473
+ enabled:
7474
+ open &&
7475
+ panel === "source" &&
7476
+ canEdit &&
7477
+ Boolean(previewModel) &&
7478
+ !previewAlreadyAttached,
7479
+ });
7480
+ useEffect(() => {
7481
+ if (!sourceActionPending || !builderAttachPreview.data) return;
7482
+ writeBuilderAttachPreviewToCache(
7483
+ queryClient,
7484
+ documentId,
7485
+ builderAttachPreview.data,
7486
+ );
7487
+ }, [builderAttachPreview.data, documentId, queryClient, sourceActionPending]);
7340
7488
  useEffect(() => {
7341
7489
  // Always re-enter the Sources panel at its root, and don't retain a path
7342
7490
  // across close/reopen.
@@ -7425,6 +7573,7 @@ function DatabaseSettingsPanelSheet({
7425
7573
  onReviewBuilderUpdate={onReviewBuilderUpdate}
7426
7574
  onSetBuilderLiveWrites={onSetBuilderLiveWrites}
7427
7575
  sourceActionPending={sourceActionPending}
7576
+ builderAttachPreviewPending={builderAttachPreview.isFetching}
7428
7577
  sourcePendingOperations={sourcePendingOperations}
7429
7578
  />
7430
7579
  ) : panel === "layout" ? (
@@ -7488,7 +7637,9 @@ function DatabaseSettingsMainPanel({
7488
7637
  onPanelChange: (panel: DatabaseSettingsPanel) => void;
7489
7638
  }) {
7490
7639
  const groupLabel = activeView.groupByPropertyId ? "On" : "";
7491
- const sourceBadgeCount = builderReviewableChangeSets(source).length;
7640
+ const sourceBadgeCount = databaseSourceChangeSetsAreComplete(source)
7641
+ ? builderReviewableChangeSets(source).length
7642
+ : undefined;
7492
7643
  return (
7493
7644
  <div className="grid gap-3">
7494
7645
  <div className="flex h-9 items-center gap-2 rounded-md border border-border bg-background px-2">
@@ -7550,6 +7701,23 @@ export function builderReviewableChangeSets(
7550
7701
  );
7551
7702
  }
7552
7703
 
7704
+ export function databaseActiveBuilderReview(args: {
7705
+ prepared: ContentDatabaseSourceReviewPayload | null;
7706
+ complete: ContentDatabaseSourceReviewPayload | null;
7707
+ page: ContentDatabaseSourceReviewPayload | null;
7708
+ open: boolean;
7709
+ }) {
7710
+ return (
7711
+ args.prepared ?? (args.open ? (args.complete ?? args.page) : args.page)
7712
+ );
7713
+ }
7714
+
7715
+ export function databaseSourceChangeSetsAreComplete(
7716
+ source: ContentDatabaseSource | null,
7717
+ ) {
7718
+ return source?.projection?.changeSets !== "page";
7719
+ }
7720
+
7553
7721
  function sourceReviewRiskRank(
7554
7722
  risk: ContentDatabaseSourceReviewPayload["riskLevel"],
7555
7723
  ) {
@@ -7807,6 +7975,7 @@ function DatabaseSettingsSourcePanel({
7807
7975
  onReviewBuilderUpdate,
7808
7976
  onSetBuilderLiveWrites,
7809
7977
  sourceActionPending,
7978
+ builderAttachPreviewPending,
7810
7979
  sourcePendingOperations,
7811
7980
  }: {
7812
7981
  source: ContentDatabaseSource | null;
@@ -7823,11 +7992,11 @@ function DatabaseSettingsSourcePanel({
7823
7992
  onAttachBuilderSource: (
7824
7993
  model: BuilderCmsModelSummary,
7825
7994
  relationshipMode?: "items" | "details",
7826
- ) => Promise<ContentDatabaseResponse>;
7995
+ ) => Promise<ContentDatabaseSourceAttachmentResult>;
7827
7996
  onFederateSource: (
7828
7997
  candidate: PendingSourceCandidate,
7829
7998
  join: ContentDatabaseSourceJoinRequest,
7830
- ) => Promise<ContentDatabaseResponse>;
7999
+ ) => Promise<ContentDatabaseSourceAttachmentResult>;
7831
8000
  onChangeSourceRole: (
7832
8001
  sourceId: string,
7833
8002
  relationshipMode: "items" | "details",
@@ -7840,13 +8009,17 @@ function DatabaseSettingsSourcePanel({
7840
8009
  onReviewBuilderUpdate: (sourceId: string) => void;
7841
8010
  onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
7842
8011
  sourceActionPending: boolean;
8012
+ builderAttachPreviewPending: boolean;
7843
8013
  sourcePendingOperations: DatabaseSourcePendingOperations;
7844
8014
  }) {
7845
8015
  const { isCodeMode } = useCodeMode();
7846
8016
  const navigate = useNavigate();
7847
8017
  const builderSources = databaseAttachedBuilderSources(sources, source);
7848
8018
  const builderStatus = useBuilderStatus();
8019
+ const builderAttachPending =
8020
+ sourceActionPending || builderAttachPreviewPending;
7849
8021
  const builderConfigured = builderStatus.status?.configured === true;
8022
+ const builderModelsQuery = useBuilderCmsModels(builderConfigured);
7850
8023
  const builderOrgName = builderStatus.status?.orgName ?? null;
7851
8024
  // Real space name(s) from the Admin API, falling back to the generic org
7852
8025
  // name (then a constant) so the drill-down never renders a blank label.
@@ -7873,11 +8046,15 @@ function DatabaseSettingsSourcePanel({
7873
8046
  sources={sources}
7874
8047
  builderConfigured={builderConfigured}
7875
8048
  builderSpaceLabel={builderSpaceLabel}
7876
- reviewableCount={builderSources.reduce(
7877
- (total, candidate) =>
7878
- total + builderReviewableChangeSets(candidate).length,
7879
- 0,
7880
- )}
8049
+ reviewableCount={
8050
+ builderSources.every(databaseSourceChangeSetsAreComplete)
8051
+ ? builderSources.reduce(
8052
+ (total, candidate) =>
8053
+ total + builderReviewableChangeSets(candidate).length,
8054
+ 0,
8055
+ )
8056
+ : undefined
8057
+ }
7881
8058
  onOpenBuilder={(builderSource) =>
7882
8059
  onNavPush(
7883
8060
  builderSource
@@ -8003,6 +8180,11 @@ function DatabaseSettingsSourcePanel({
8003
8180
  join,
8004
8181
  )
8005
8182
  : await onFederateSource(top.candidate, join);
8183
+ if ("responseProjection" in result) {
8184
+ throw new Error(
8185
+ "Detail-source attachment returned an item-source acknowledgement.",
8186
+ );
8187
+ }
8006
8188
  const detailsSource = findDetailsSource(result, top.candidate);
8007
8189
  onNavReplace(
8008
8190
  detailsSource
@@ -8095,6 +8277,7 @@ function DatabaseSettingsSourcePanel({
8095
8277
  return (
8096
8278
  <BuilderSpaceModelsView
8097
8279
  attachedModelNames={databaseAttachedBuilderModelNames(sources, source)}
8280
+ modelsQuery={builderModelsQuery}
8098
8281
  onOpenModel={(model) => {
8099
8282
  const attached = databaseAttachedBuilderSource(sources, source, {
8100
8283
  modelName: model.name,
@@ -8144,7 +8327,7 @@ function DatabaseSettingsSourcePanel({
8144
8327
  displayName: model.displayName,
8145
8328
  }}
8146
8329
  canEdit={canEdit}
8147
- pending={sourceActionPending}
8330
+ pending={builderAttachPending}
8148
8331
  onAddDetails={() =>
8149
8332
  onNavPush({
8150
8333
  kind: "keyConfirm",
@@ -8166,7 +8349,7 @@ function DatabaseSettingsSourcePanel({
8166
8349
  <Button
8167
8350
  type="button"
8168
8351
  size="sm"
8169
- disabled={!canEdit || sourceActionPending}
8352
+ disabled={!canEdit || builderAttachPending}
8170
8353
  onClick={async () => {
8171
8354
  try {
8172
8355
  await onAttachBuilderSource(model);
@@ -8181,7 +8364,7 @@ function DatabaseSettingsSourcePanel({
8181
8364
  }
8182
8365
  }}
8183
8366
  >
8184
- {sourceActionPending ? (
8367
+ {builderAttachPending ? (
8185
8368
  <Spinner className="mr-1.5 size-3.5" />
8186
8369
  ) : (
8187
8370
  <IconPlugConnected className="mr-1.5 size-3.5" />
@@ -8195,6 +8378,8 @@ function DatabaseSettingsSourcePanel({
8195
8378
  }
8196
8379
 
8197
8380
  const selectedSource = selectedBuilderSource;
8381
+ const selectedSourceChangeSetsComplete =
8382
+ databaseSourceChangeSetsAreComplete(selectedSource);
8198
8383
  const reviewableBuilderChangeSets =
8199
8384
  builderReviewableChangeSets(selectedSource);
8200
8385
  const outboundChangeSets = reviewableBuilderChangeSets;
@@ -8408,11 +8593,13 @@ function DatabaseSettingsSourcePanel({
8408
8593
  <div className="flex items-center justify-between gap-3">
8409
8594
  <div className="min-w-0">
8410
8595
  <div className="text-sm font-medium">
8411
- {conflictChangeSets.length > 0
8596
+ {selectedSourceChangeSetsComplete &&
8597
+ conflictChangeSets.length > 0
8412
8598
  ? `${conflictChangeSets.length} change${
8413
8599
  conflictChangeSets.length === 1 ? "" : "s"
8414
8600
  } need review`
8415
- : reviewableBuilderChangeSets.length > 0
8601
+ : selectedSourceChangeSetsComplete &&
8602
+ reviewableBuilderChangeSets.length > 0
8416
8603
  ? `${reviewableBuilderChangeSets.length} change${
8417
8604
  reviewableBuilderChangeSets.length === 1 ? "" : "s"
8418
8605
  } ready to push`
@@ -8562,7 +8749,7 @@ function SourcesListView({
8562
8749
  sources: ContentDatabaseSource[];
8563
8750
  builderConfigured: boolean;
8564
8751
  builderSpaceLabel: string | null;
8565
- reviewableCount: number;
8752
+ reviewableCount?: number;
8566
8753
  onOpenBuilder: (source?: ContentDatabaseSource) => void;
8567
8754
  onOpenNotion: () => void;
8568
8755
  onOpenLocalFolder: () => void;
@@ -9789,12 +9976,13 @@ function SourceDetailsFieldPicker({
9789
9976
  // is marked; selecting a row opens that model's leaf.
9790
9977
  function BuilderSpaceModelsView({
9791
9978
  attachedModelNames,
9979
+ modelsQuery,
9792
9980
  onOpenModel,
9793
9981
  }: {
9794
9982
  attachedModelNames: string[];
9983
+ modelsQuery: ReturnType<typeof useBuilderCmsModels>;
9795
9984
  onOpenModel: (model: BuilderCmsModelSummary) => void;
9796
9985
  }) {
9797
- const modelsQuery = useBuilderCmsModels(true);
9798
9986
  const models = modelsQuery.data?.models ?? [];
9799
9987
  const [query, setQuery] = useState("");
9800
9988
 
@@ -16362,37 +16550,11 @@ export function applyDatabaseView(
16362
16550
  sorts: DatabaseSort[],
16363
16551
  filterMode: DatabaseFilterMode = "and",
16364
16552
  ) {
16365
- const query = searchQuery.trim().toLowerCase();
16366
- const searched = query
16367
- ? items.filter((item) =>
16368
- databaseItemSearchText(item, properties).toLowerCase().includes(query),
16369
- )
16370
- : items;
16371
- const activeFilters = filters.filter(isActiveFilter);
16372
- const filtered = activeFilters.length
16373
- ? searched.filter((item) =>
16374
- databaseItemMatchesFilterTree(
16375
- item,
16376
- properties,
16377
- activeFilters,
16378
- filterMode,
16379
- ),
16380
- )
16381
- : searched;
16382
-
16383
- if (sorts.length === 0) return filtered;
16384
-
16385
- return [...filtered].sort((a, b) => {
16386
- for (const sort of sorts) {
16387
- const comparison = compareDatabaseSortValues(
16388
- databaseItemSortValue(a, properties, sort.key),
16389
- databaseItemSortValue(b, properties, sort.key),
16390
- );
16391
- if (comparison !== 0) {
16392
- return sort.direction === "asc" ? comparison : -comparison;
16393
- }
16394
- }
16395
- return 0;
16553
+ return applyContentDatabaseTableQuery(items, properties, {
16554
+ search: searchQuery,
16555
+ filters,
16556
+ sorts,
16557
+ filterMode,
16396
16558
  });
16397
16559
  }
16398
16560