@agent-native/core 0.80.5 → 0.80.6
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 +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/content/AGENTS.md +45 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
- package/corpus/templates/content/app/i18n-data.ts +784 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/vite.config.ts +14 -7
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/package.json +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
|
@@ -139,8 +139,10 @@ import {
|
|
|
139
139
|
} from "@/components/ui/tooltip";
|
|
140
140
|
import {
|
|
141
141
|
useAddDatabaseItem,
|
|
142
|
+
useAddContentDatabaseSourceFieldProperty,
|
|
142
143
|
useAttachContentDatabaseSource,
|
|
143
144
|
useBuilderCmsModels,
|
|
145
|
+
useChangeContentDatabaseSourceRole,
|
|
144
146
|
useContentDatabase,
|
|
145
147
|
useContentDatabases,
|
|
146
148
|
contentDatabaseQueryKey,
|
|
@@ -166,7 +168,11 @@ import {
|
|
|
166
168
|
import { messagesByLocale } from "@/i18n-data";
|
|
167
169
|
import { cn } from "@/lib/utils";
|
|
168
170
|
|
|
169
|
-
import {
|
|
171
|
+
import { resolveBuilderCmsWriteEffect } from "../../../../actions/_builder-cms-write-adapter.js";
|
|
172
|
+
import {
|
|
173
|
+
BuilderSourceReviewDialog,
|
|
174
|
+
type BuilderReviewPublicationTransitions,
|
|
175
|
+
} from "../database-sources/BuilderSourceReviewDialog";
|
|
170
176
|
import { DocumentBlockFields } from "../DocumentBlockFields";
|
|
171
177
|
import {
|
|
172
178
|
AddProperty,
|
|
@@ -205,6 +211,7 @@ export interface DatabaseViewProps {
|
|
|
205
211
|
}
|
|
206
212
|
|
|
207
213
|
const CONTENT_DATABASE_PAGE_SIZE = 100;
|
|
214
|
+
const CONTENT_DATABASE_MAX_ITEM_LIMIT = 5_000;
|
|
208
215
|
|
|
209
216
|
export type SortDirection = ContentDatabaseSortDirection;
|
|
210
217
|
export type DatabaseSort = ContentDatabaseSort;
|
|
@@ -239,13 +246,23 @@ const DATABASE_FILTER_MODES: DatabaseFilterMode[] = ["and", "or"];
|
|
|
239
246
|
|
|
240
247
|
type DatabaseMessageKey = keyof (typeof messagesByLocale)["en-US"]["database"];
|
|
241
248
|
|
|
242
|
-
function dbText(
|
|
249
|
+
function dbText(
|
|
250
|
+
key: DatabaseMessageKey,
|
|
251
|
+
values?: Record<string, string | number>,
|
|
252
|
+
): string {
|
|
243
253
|
const locale =
|
|
244
254
|
typeof document === "undefined" ? "en-US" : document.documentElement.lang;
|
|
245
255
|
const messages =
|
|
246
256
|
messagesByLocale[locale as keyof typeof messagesByLocale] ??
|
|
247
257
|
messagesByLocale["en-US"];
|
|
248
|
-
|
|
258
|
+
const text =
|
|
259
|
+
messages.database[key] ?? messagesByLocale["en-US"].database[key];
|
|
260
|
+
if (!values) return text;
|
|
261
|
+
return Object.entries(values).reduce(
|
|
262
|
+
(current, [name, value]) =>
|
|
263
|
+
current.split(`{{${name}}}`).join(String(value)),
|
|
264
|
+
text,
|
|
265
|
+
);
|
|
249
266
|
}
|
|
250
267
|
|
|
251
268
|
type CreateDatabaseRowHandler = (
|
|
@@ -459,6 +476,7 @@ function DatabaseTable({
|
|
|
459
476
|
const database = useContentDatabase(document.id, databaseItemLimit);
|
|
460
477
|
const addItem = useAddDatabaseItem(document.id);
|
|
461
478
|
const attachSource = useAttachContentDatabaseSource(document.id);
|
|
479
|
+
const changeSourceRole = useChangeContentDatabaseSourceRole(document.id);
|
|
462
480
|
const refreshSource = useRefreshContentDatabaseSource(document.id);
|
|
463
481
|
const disconnectSource = useDisconnectContentDatabaseSource(document.id);
|
|
464
482
|
const prepareBuilderReview = usePrepareBuilderSourceReview(document.id);
|
|
@@ -470,7 +488,11 @@ function DatabaseTable({
|
|
|
470
488
|
const properties = data?.properties ?? [];
|
|
471
489
|
const items = data?.items ?? [];
|
|
472
490
|
const totalItemCount = data?.pagination?.totalItems ?? items.length;
|
|
473
|
-
const hasMoreItems =
|
|
491
|
+
const hasMoreItems =
|
|
492
|
+
data?.pagination?.hasMore === true &&
|
|
493
|
+
databaseItemLimit < CONTENT_DATABASE_MAX_ITEM_LIMIT;
|
|
494
|
+
const isLoadingMoreItems =
|
|
495
|
+
database.isFetching && data?.pagination?.limit !== databaseItemLimit;
|
|
474
496
|
const databaseId = data?.database.id ?? expectedDatabaseId;
|
|
475
497
|
const source = data?.source ?? null;
|
|
476
498
|
const sources = data?.sources ?? (source ? [source] : []);
|
|
@@ -933,7 +955,9 @@ function DatabaseTable({
|
|
|
933
955
|
updateActiveView((view) => ({ ...view, hideEmptyGroups }));
|
|
934
956
|
}
|
|
935
957
|
|
|
936
|
-
async function handleBuilderReviewPush(
|
|
958
|
+
async function handleBuilderReviewPush(
|
|
959
|
+
transitions: BuilderReviewPublicationTransitions = {},
|
|
960
|
+
) {
|
|
937
961
|
setBuilderReviewResult(null);
|
|
938
962
|
setBuilderReviewCheckedAt(null);
|
|
939
963
|
try {
|
|
@@ -951,11 +975,14 @@ function DatabaseTable({
|
|
|
951
975
|
let executedResponse: ContentDatabaseResponse | null = null;
|
|
952
976
|
for (const row of executableRows) {
|
|
953
977
|
if (!row.execution?.idempotencyKey) continue;
|
|
978
|
+
const transition = transitions[row.changeSetId];
|
|
954
979
|
executedResponse = await executeBuilderExecution.mutateAsync({
|
|
955
980
|
documentId: document.id,
|
|
956
981
|
changeSetId: row.changeSetId,
|
|
957
982
|
idempotencyKey: row.execution.idempotencyKey,
|
|
958
983
|
pushModeConfirmation: nextReview.pushMode,
|
|
984
|
+
publicationTransition: transition?.publicationTransition,
|
|
985
|
+
confirmUnpublish: transition?.confirmUnpublish,
|
|
959
986
|
});
|
|
960
987
|
}
|
|
961
988
|
const executedSource = executedResponse?.source ?? null;
|
|
@@ -1016,11 +1043,14 @@ function DatabaseTable({
|
|
|
1016
1043
|
const nextViewConfig = normalizeClientDatabaseViewConfig(
|
|
1017
1044
|
data.database.viewConfig,
|
|
1018
1045
|
);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1046
|
+
const nextKey = databaseViewStateKey(data.database.id, nextViewConfig);
|
|
1047
|
+
if (hydratedViewRef.current === nextKey) return;
|
|
1048
|
+
hydratedViewRef.current = nextKey;
|
|
1049
|
+
setViewConfig((current) =>
|
|
1050
|
+
databaseViewStateKey(data.database.id, current) === nextKey
|
|
1051
|
+
? current
|
|
1052
|
+
: nextViewConfig,
|
|
1022
1053
|
);
|
|
1023
|
-
setViewConfig(nextViewConfig);
|
|
1024
1054
|
}, [data?.database.id, data?.database.viewConfig]);
|
|
1025
1055
|
|
|
1026
1056
|
useEffect(() => {
|
|
@@ -1443,14 +1473,18 @@ function DatabaseTable({
|
|
|
1443
1473
|
type="button"
|
|
1444
1474
|
variant="outline"
|
|
1445
1475
|
size="sm"
|
|
1446
|
-
disabled={
|
|
1476
|
+
disabled={isLoadingMoreItems}
|
|
1447
1477
|
onClick={() =>
|
|
1448
|
-
setDatabaseItemLimit(
|
|
1449
|
-
(
|
|
1478
|
+
setDatabaseItemLimit((current) =>
|
|
1479
|
+
Math.min(
|
|
1480
|
+
current + CONTENT_DATABASE_PAGE_SIZE,
|
|
1481
|
+
totalItemCount,
|
|
1482
|
+
CONTENT_DATABASE_MAX_ITEM_LIMIT,
|
|
1483
|
+
),
|
|
1450
1484
|
)
|
|
1451
1485
|
}
|
|
1452
1486
|
>
|
|
1453
|
-
{
|
|
1487
|
+
{isLoadingMoreItems
|
|
1454
1488
|
? "Loading..."
|
|
1455
1489
|
: `Load more rows (${items.length} of ${totalItemCount})`}
|
|
1456
1490
|
</Button>
|
|
@@ -1494,35 +1528,53 @@ function DatabaseTable({
|
|
|
1494
1528
|
groupIds={toolbarGroups.map((group) => group.id)}
|
|
1495
1529
|
onClose={() => setSettingsOpen(false)}
|
|
1496
1530
|
onPanelChange={setSettingsPanel}
|
|
1497
|
-
onAttachBuilderSource={(model) =>
|
|
1498
|
-
attachSource.
|
|
1531
|
+
onAttachBuilderSource={(model, relationshipMode) =>
|
|
1532
|
+
attachSource.mutateAsync({
|
|
1499
1533
|
documentId: document.id,
|
|
1500
1534
|
sourceType: "builder-cms",
|
|
1501
1535
|
sourceName: model.displayName,
|
|
1502
1536
|
sourceTable: model.name,
|
|
1537
|
+
relationshipMode,
|
|
1538
|
+
mode:
|
|
1539
|
+
relationshipMode === "items"
|
|
1540
|
+
? "add"
|
|
1541
|
+
: sources.length > 0 || source
|
|
1542
|
+
? undefined
|
|
1543
|
+
: "replace",
|
|
1503
1544
|
})
|
|
1504
1545
|
}
|
|
1505
1546
|
onFederateSource={(candidate, join) =>
|
|
1506
|
-
attachSource.
|
|
1547
|
+
attachSource.mutateAsync({
|
|
1507
1548
|
documentId: document.id,
|
|
1508
1549
|
sourceType: candidate.sourceType,
|
|
1509
1550
|
sourceName: candidate.sourceName,
|
|
1510
1551
|
sourceTable: candidate.sourceTable,
|
|
1552
|
+
relationshipMode: "details",
|
|
1553
|
+
join,
|
|
1554
|
+
})
|
|
1555
|
+
}
|
|
1556
|
+
onChangeSourceRole={(sourceId, relationshipMode, join) =>
|
|
1557
|
+
changeSourceRole.mutateAsync({
|
|
1558
|
+
documentId: document.id,
|
|
1559
|
+
sourceId,
|
|
1560
|
+
relationshipMode,
|
|
1511
1561
|
join,
|
|
1512
1562
|
})
|
|
1513
1563
|
}
|
|
1514
1564
|
onDisconnectSecondary={(sourceId) =>
|
|
1515
1565
|
disconnectSource.mutate({ documentId: document.id, sourceId })
|
|
1516
1566
|
}
|
|
1517
|
-
onRefreshSource={() =>
|
|
1567
|
+
onRefreshSource={(sourceId) =>
|
|
1518
1568
|
refreshSource.mutate({
|
|
1519
1569
|
documentId: document.id,
|
|
1570
|
+
sourceId,
|
|
1520
1571
|
})
|
|
1521
1572
|
}
|
|
1522
|
-
onDisconnectSource={() =>
|
|
1573
|
+
onDisconnectSource={(sourceId) =>
|
|
1523
1574
|
disconnectSource.mutate(
|
|
1524
1575
|
{
|
|
1525
1576
|
documentId: document.id,
|
|
1577
|
+
sourceId,
|
|
1526
1578
|
},
|
|
1527
1579
|
{
|
|
1528
1580
|
onSuccess: () => {
|
|
@@ -1581,6 +1633,7 @@ function DatabaseTable({
|
|
|
1581
1633
|
}
|
|
1582
1634
|
sourceActionPending={
|
|
1583
1635
|
attachSource.isPending ||
|
|
1636
|
+
changeSourceRole.isPending ||
|
|
1584
1637
|
refreshSource.isPending ||
|
|
1585
1638
|
disconnectSource.isPending ||
|
|
1586
1639
|
prepareBuilderReview.isPending ||
|
|
@@ -1613,7 +1666,7 @@ function DatabaseTable({
|
|
|
1613
1666
|
}
|
|
1614
1667
|
checkedAt={builderReviewCheckedAt}
|
|
1615
1668
|
onClose={() => setBuilderReviewOpen(false)}
|
|
1616
|
-
onValidate={() => void handleBuilderReviewPush()}
|
|
1669
|
+
onValidate={(transitions) => void handleBuilderReviewPush(transitions)}
|
|
1617
1670
|
/>
|
|
1618
1671
|
|
|
1619
1672
|
{!database.isLoading ? (
|
|
@@ -3403,6 +3456,7 @@ type PendingSourceCandidate = {
|
|
|
3403
3456
|
sourceName: string;
|
|
3404
3457
|
sourceTable: string;
|
|
3405
3458
|
displayName: string;
|
|
3459
|
+
existingSourceId?: string;
|
|
3406
3460
|
};
|
|
3407
3461
|
|
|
3408
3462
|
type SourceNavStep =
|
|
@@ -3411,16 +3465,18 @@ type SourceNavStep =
|
|
|
3411
3465
|
| { kind: "model"; model: BuilderCmsModelSummary }
|
|
3412
3466
|
| { kind: "addSource" }
|
|
3413
3467
|
| { kind: "secondarySource"; sourceId: string; sourceName: string }
|
|
3414
|
-
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
3468
|
+
| { kind: "keyConfirm"; candidate: PendingSourceCandidate }
|
|
3469
|
+
| { kind: "fieldPicker"; sourceId: string; sourceName: string };
|
|
3415
3470
|
|
|
3416
3471
|
function sourceNavTitle(stack: SourceNavStep[]): string {
|
|
3417
3472
|
const top = stack[stack.length - 1];
|
|
3418
|
-
if (!top) return "
|
|
3473
|
+
if (!top) return dbText("sources");
|
|
3419
3474
|
if (top.kind === "provider") return "Builder";
|
|
3420
3475
|
if (top.kind === "space") return top.spaceName;
|
|
3421
|
-
if (top.kind === "addSource") return "
|
|
3476
|
+
if (top.kind === "addSource") return dbText("addASource");
|
|
3422
3477
|
if (top.kind === "secondarySource") return top.sourceName;
|
|
3423
|
-
if (top.kind === "keyConfirm") return "
|
|
3478
|
+
if (top.kind === "keyConfirm") return dbText("matchExistingItemsToDetails");
|
|
3479
|
+
if (top.kind === "fieldPicker") return dbText("chooseFields");
|
|
3424
3480
|
return top.model.displayName;
|
|
3425
3481
|
}
|
|
3426
3482
|
|
|
@@ -3480,6 +3536,7 @@ function DatabaseSettingsPanelSheet({
|
|
|
3480
3536
|
onPanelChange,
|
|
3481
3537
|
onAttachBuilderSource,
|
|
3482
3538
|
onFederateSource,
|
|
3539
|
+
onChangeSourceRole,
|
|
3483
3540
|
onDisconnectSecondary,
|
|
3484
3541
|
onRefreshSource,
|
|
3485
3542
|
onDisconnectSource,
|
|
@@ -3508,14 +3565,22 @@ function DatabaseSettingsPanelSheet({
|
|
|
3508
3565
|
groupIds: string[];
|
|
3509
3566
|
onClose: () => void;
|
|
3510
3567
|
onPanelChange: (panel: DatabaseSettingsPanel) => void;
|
|
3511
|
-
onAttachBuilderSource: (
|
|
3568
|
+
onAttachBuilderSource: (
|
|
3569
|
+
model: BuilderCmsModelSummary,
|
|
3570
|
+
relationshipMode?: "items" | "details",
|
|
3571
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3512
3572
|
onFederateSource: (
|
|
3513
3573
|
candidate: PendingSourceCandidate,
|
|
3514
3574
|
join: ContentDatabaseSourceJoinRequest,
|
|
3515
|
-
) =>
|
|
3575
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3576
|
+
onChangeSourceRole: (
|
|
3577
|
+
sourceId: string,
|
|
3578
|
+
relationshipMode: "items" | "details",
|
|
3579
|
+
join?: ContentDatabaseSourceJoinRequest,
|
|
3580
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3516
3581
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
3517
|
-
onRefreshSource: () => void;
|
|
3518
|
-
onDisconnectSource: () => void;
|
|
3582
|
+
onRefreshSource: (sourceId?: string) => void;
|
|
3583
|
+
onDisconnectSource: (sourceId?: string) => void;
|
|
3519
3584
|
onReviewBuilderUpdate: () => void;
|
|
3520
3585
|
onSetBuilderLiveWrites: (enabled: boolean) => void;
|
|
3521
3586
|
sourceActionPending: boolean;
|
|
@@ -3603,11 +3668,10 @@ function DatabaseSettingsPanelSheet({
|
|
|
3603
3668
|
canEdit={canEdit}
|
|
3604
3669
|
nav={sourceNavStack}
|
|
3605
3670
|
onNavPush={(step) => setSourceNavStack((stack) => [...stack, step])}
|
|
3671
|
+
onNavReplace={setSourceNavStack}
|
|
3606
3672
|
onAttachBuilderSource={onAttachBuilderSource}
|
|
3607
|
-
onFederateSource={
|
|
3608
|
-
|
|
3609
|
-
setSourceNavStack([]);
|
|
3610
|
-
}}
|
|
3673
|
+
onFederateSource={onFederateSource}
|
|
3674
|
+
onChangeSourceRole={onChangeSourceRole}
|
|
3611
3675
|
onDisconnectSecondary={(sourceId) => {
|
|
3612
3676
|
onDisconnectSecondary(sourceId);
|
|
3613
3677
|
setSourceNavStack([]);
|
|
@@ -3824,6 +3888,7 @@ export function buildClientBuilderReviewPayload(
|
|
|
3824
3888
|
riskLevel: changeSet.riskLevel,
|
|
3825
3889
|
riskReasons: changeSet.riskReasons,
|
|
3826
3890
|
conflictState: changeSet.conflictState,
|
|
3891
|
+
effect: resolveBuilderCmsWriteEffect({ source, changeSet }),
|
|
3827
3892
|
execution: latestExecution,
|
|
3828
3893
|
};
|
|
3829
3894
|
});
|
|
@@ -3905,8 +3970,10 @@ function DatabaseSettingsSourcePanel({
|
|
|
3905
3970
|
canEdit,
|
|
3906
3971
|
nav,
|
|
3907
3972
|
onNavPush,
|
|
3973
|
+
onNavReplace,
|
|
3908
3974
|
onAttachBuilderSource,
|
|
3909
3975
|
onFederateSource,
|
|
3976
|
+
onChangeSourceRole,
|
|
3910
3977
|
onDisconnectSecondary,
|
|
3911
3978
|
onRefreshSource,
|
|
3912
3979
|
onDisconnectSource,
|
|
@@ -3921,14 +3988,23 @@ function DatabaseSettingsSourcePanel({
|
|
|
3921
3988
|
canEdit: boolean;
|
|
3922
3989
|
nav: SourceNavStep[];
|
|
3923
3990
|
onNavPush: (step: SourceNavStep) => void;
|
|
3924
|
-
|
|
3991
|
+
onNavReplace: (stack: SourceNavStep[]) => void;
|
|
3992
|
+
onAttachBuilderSource: (
|
|
3993
|
+
model: BuilderCmsModelSummary,
|
|
3994
|
+
relationshipMode?: "items" | "details",
|
|
3995
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3925
3996
|
onFederateSource: (
|
|
3926
3997
|
candidate: PendingSourceCandidate,
|
|
3927
3998
|
join: ContentDatabaseSourceJoinRequest,
|
|
3928
|
-
) =>
|
|
3999
|
+
) => Promise<ContentDatabaseResponse>;
|
|
4000
|
+
onChangeSourceRole: (
|
|
4001
|
+
sourceId: string,
|
|
4002
|
+
relationshipMode: "items" | "details",
|
|
4003
|
+
join?: ContentDatabaseSourceJoinRequest,
|
|
4004
|
+
) => Promise<ContentDatabaseResponse>;
|
|
3929
4005
|
onDisconnectSecondary: (sourceId: string) => void;
|
|
3930
|
-
onRefreshSource: () => void;
|
|
3931
|
-
onDisconnectSource: () => void;
|
|
4006
|
+
onRefreshSource: (sourceId?: string) => void;
|
|
4007
|
+
onDisconnectSource: (sourceId?: string) => void;
|
|
3932
4008
|
onReviewBuilderUpdate: () => void;
|
|
3933
4009
|
onSetBuilderLiveWrites: (enabled: boolean) => void;
|
|
3934
4010
|
sourceActionPending: boolean;
|
|
@@ -4053,6 +4129,34 @@ function DatabaseSettingsSourcePanel({
|
|
|
4053
4129
|
source={secondary}
|
|
4054
4130
|
canEdit={canEdit}
|
|
4055
4131
|
pending={sourceActionPending}
|
|
4132
|
+
onAddDetails={() =>
|
|
4133
|
+
secondary
|
|
4134
|
+
? onNavPush({
|
|
4135
|
+
kind: "keyConfirm",
|
|
4136
|
+
candidate: {
|
|
4137
|
+
sourceType: secondary.sourceType,
|
|
4138
|
+
sourceName: secondary.sourceName,
|
|
4139
|
+
sourceTable: secondary.sourceTable,
|
|
4140
|
+
displayName: secondary.sourceName,
|
|
4141
|
+
existingSourceId: secondary.id,
|
|
4142
|
+
},
|
|
4143
|
+
})
|
|
4144
|
+
: undefined
|
|
4145
|
+
}
|
|
4146
|
+
onAddItems={async () => {
|
|
4147
|
+
if (!secondary) return;
|
|
4148
|
+
await onChangeSourceRole(secondary.id, "items");
|
|
4149
|
+
onNavReplace([]);
|
|
4150
|
+
}}
|
|
4151
|
+
onChooseFields={() =>
|
|
4152
|
+
secondary
|
|
4153
|
+
? onNavPush({
|
|
4154
|
+
kind: "fieldPicker",
|
|
4155
|
+
sourceId: secondary.id,
|
|
4156
|
+
sourceName: secondary.sourceName,
|
|
4157
|
+
})
|
|
4158
|
+
: undefined
|
|
4159
|
+
}
|
|
4056
4160
|
onDisconnect={() => onDisconnectSecondary(top.sourceId)}
|
|
4057
4161
|
/>
|
|
4058
4162
|
);
|
|
@@ -4066,7 +4170,41 @@ function DatabaseSettingsSourcePanel({
|
|
|
4066
4170
|
candidate={top.candidate}
|
|
4067
4171
|
canEdit={canEdit}
|
|
4068
4172
|
pending={sourceActionPending}
|
|
4069
|
-
onCommit={(join) =>
|
|
4173
|
+
onCommit={async (join) => {
|
|
4174
|
+
const result = top.candidate.existingSourceId
|
|
4175
|
+
? await onChangeSourceRole(
|
|
4176
|
+
top.candidate.existingSourceId,
|
|
4177
|
+
"details",
|
|
4178
|
+
join,
|
|
4179
|
+
)
|
|
4180
|
+
: await onFederateSource(top.candidate, join);
|
|
4181
|
+
const detailsSource = findDetailsSource(result, top.candidate);
|
|
4182
|
+
onNavReplace(
|
|
4183
|
+
detailsSource
|
|
4184
|
+
? [
|
|
4185
|
+
{
|
|
4186
|
+
kind: "fieldPicker",
|
|
4187
|
+
sourceId: detailsSource.id,
|
|
4188
|
+
sourceName: detailsSource.sourceName,
|
|
4189
|
+
},
|
|
4190
|
+
]
|
|
4191
|
+
: [],
|
|
4192
|
+
);
|
|
4193
|
+
}}
|
|
4194
|
+
/>
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
if (top.kind === "fieldPicker") {
|
|
4199
|
+
const pickerSource =
|
|
4200
|
+
sources.find((item) => item.id === top.sourceId) ?? null;
|
|
4201
|
+
return (
|
|
4202
|
+
<SourceDetailsFieldPicker
|
|
4203
|
+
documentId={documentId}
|
|
4204
|
+
source={pickerSource}
|
|
4205
|
+
canEdit={canEdit}
|
|
4206
|
+
pending={sourceActionPending}
|
|
4207
|
+
onDone={() => onNavReplace([])}
|
|
4070
4208
|
/>
|
|
4071
4209
|
);
|
|
4072
4210
|
}
|
|
@@ -4165,21 +4303,53 @@ function DatabaseSettingsSourcePanel({
|
|
|
4165
4303
|
</span>
|
|
4166
4304
|
</div>
|
|
4167
4305
|
</div>
|
|
4168
|
-
|
|
4169
|
-
<
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
)
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4306
|
+
{sources.length > 0 || source ? (
|
|
4307
|
+
<SourceRelationshipChoice
|
|
4308
|
+
documentId={documentId}
|
|
4309
|
+
candidate={{
|
|
4310
|
+
sourceType: "builder-cms",
|
|
4311
|
+
sourceName: model.displayName,
|
|
4312
|
+
sourceTable: model.name,
|
|
4313
|
+
displayName: model.displayName,
|
|
4314
|
+
}}
|
|
4315
|
+
canEdit={canEdit}
|
|
4316
|
+
pending={sourceActionPending}
|
|
4317
|
+
onAddDetails={() =>
|
|
4318
|
+
onNavPush({
|
|
4319
|
+
kind: "keyConfirm",
|
|
4320
|
+
candidate: {
|
|
4321
|
+
sourceType: "builder-cms",
|
|
4322
|
+
sourceName: model.displayName,
|
|
4323
|
+
sourceTable: model.name,
|
|
4324
|
+
displayName: model.displayName,
|
|
4325
|
+
},
|
|
4326
|
+
})
|
|
4327
|
+
}
|
|
4328
|
+
onAddItems={async () => {
|
|
4329
|
+
await onAttachBuilderSource(model, "items");
|
|
4330
|
+
onNavReplace([]);
|
|
4331
|
+
}}
|
|
4332
|
+
/>
|
|
4333
|
+
) : (
|
|
4334
|
+
<div>
|
|
4335
|
+
<Button
|
|
4336
|
+
type="button"
|
|
4337
|
+
size="sm"
|
|
4338
|
+
disabled={!canEdit || sourceActionPending}
|
|
4339
|
+
onClick={async () => {
|
|
4340
|
+
await onAttachBuilderSource(model);
|
|
4341
|
+
onNavReplace([]);
|
|
4342
|
+
}}
|
|
4343
|
+
>
|
|
4344
|
+
{sourceActionPending ? (
|
|
4345
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
4346
|
+
) : (
|
|
4347
|
+
<IconPlugConnected className="mr-1.5 size-3.5" />
|
|
4348
|
+
)}
|
|
4349
|
+
Attach
|
|
4350
|
+
</Button>
|
|
4351
|
+
</div>
|
|
4352
|
+
)}
|
|
4183
4353
|
</div>
|
|
4184
4354
|
);
|
|
4185
4355
|
}
|
|
@@ -4217,7 +4387,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
4217
4387
|
type="button"
|
|
4218
4388
|
className="inline-flex items-center gap-1 text-destructive hover:underline disabled:opacity-60"
|
|
4219
4389
|
disabled={!canEdit || sourceActionPending}
|
|
4220
|
-
onClick={onRefreshSource}
|
|
4390
|
+
onClick={() => onRefreshSource(source.id)}
|
|
4221
4391
|
>
|
|
4222
4392
|
<IconRefresh className="size-3" />
|
|
4223
4393
|
{dbText("couldntSyncRetry")}
|
|
@@ -4307,6 +4477,38 @@ function DatabaseSettingsSourcePanel({
|
|
|
4307
4477
|
</>
|
|
4308
4478
|
) : null}
|
|
4309
4479
|
|
|
4480
|
+
<SourceRoleCard
|
|
4481
|
+
source={source}
|
|
4482
|
+
canAddDetails={sources.some(
|
|
4483
|
+
(item) => item.id !== source.id && !sourceAddsDetails(item),
|
|
4484
|
+
)}
|
|
4485
|
+
canEdit={canEdit}
|
|
4486
|
+
pending={sourceActionPending}
|
|
4487
|
+
onAddDetails={() =>
|
|
4488
|
+
onNavPush({
|
|
4489
|
+
kind: "keyConfirm",
|
|
4490
|
+
candidate: {
|
|
4491
|
+
sourceType: source.sourceType,
|
|
4492
|
+
sourceName: source.sourceName,
|
|
4493
|
+
sourceTable: source.sourceTable,
|
|
4494
|
+
displayName: source.sourceName,
|
|
4495
|
+
existingSourceId: source.id,
|
|
4496
|
+
},
|
|
4497
|
+
})
|
|
4498
|
+
}
|
|
4499
|
+
onAddItems={async () => {
|
|
4500
|
+
await onChangeSourceRole(source.id, "items");
|
|
4501
|
+
onNavReplace([]);
|
|
4502
|
+
}}
|
|
4503
|
+
onChooseFields={() =>
|
|
4504
|
+
onNavPush({
|
|
4505
|
+
kind: "fieldPicker",
|
|
4506
|
+
sourceId: source.id,
|
|
4507
|
+
sourceName: source.sourceName,
|
|
4508
|
+
})
|
|
4509
|
+
}
|
|
4510
|
+
/>
|
|
4511
|
+
|
|
4310
4512
|
<div className="rounded-lg border border-border bg-background p-3">
|
|
4311
4513
|
<div className="text-xs font-medium">
|
|
4312
4514
|
{dbText("disconnectSource")}
|
|
@@ -4320,7 +4522,7 @@ function DatabaseSettingsSourcePanel({
|
|
|
4320
4522
|
variant="outline"
|
|
4321
4523
|
className="mt-2 h-8 text-xs text-destructive hover:text-destructive"
|
|
4322
4524
|
disabled={!canEdit || sourceActionPending}
|
|
4323
|
-
onClick={onDisconnectSource}
|
|
4525
|
+
onClick={() => onDisconnectSource(source.id)}
|
|
4324
4526
|
>
|
|
4325
4527
|
{sourceActionPending ? (
|
|
4326
4528
|
<Spinner className="mr-1 size-3.5" />
|
|
@@ -4381,13 +4583,7 @@ function SourcesListView({
|
|
|
4381
4583
|
)
|
|
4382
4584
|
}
|
|
4383
4585
|
label={connected.sourceName}
|
|
4384
|
-
value={
|
|
4385
|
-
connected.metadata.federation?.role === "secondary"
|
|
4386
|
-
? "Federated"
|
|
4387
|
-
: index === 0
|
|
4388
|
-
? "Primary"
|
|
4389
|
-
: undefined
|
|
4390
|
-
}
|
|
4586
|
+
value={sourceRoleLabel(connected, index)}
|
|
4391
4587
|
onClick={
|
|
4392
4588
|
connected.metadata.federation?.role === "secondary"
|
|
4393
4589
|
? () => onOpenSecondary(connected)
|
|
@@ -4461,7 +4657,7 @@ function CanonicalKeyConfirmView({
|
|
|
4461
4657
|
candidate: PendingSourceCandidate;
|
|
4462
4658
|
canEdit: boolean;
|
|
4463
4659
|
pending: boolean;
|
|
4464
|
-
onCommit: (join: ContentDatabaseSourceJoinRequest) => void
|
|
4660
|
+
onCommit: (join: ContentDatabaseSourceJoinRequest) => void | Promise<void>;
|
|
4465
4661
|
}) {
|
|
4466
4662
|
const suggestionQuery = useSuggestSourceJoinKey({
|
|
4467
4663
|
documentId,
|
|
@@ -4519,7 +4715,7 @@ function CanonicalKeyConfirmView({
|
|
|
4519
4715
|
return (
|
|
4520
4716
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
4521
4717
|
<Spinner className="size-3.5" />
|
|
4522
|
-
{dbText("
|
|
4718
|
+
{dbText("checkingHowTheseRecordsMatch")}
|
|
4523
4719
|
</div>
|
|
4524
4720
|
);
|
|
4525
4721
|
}
|
|
@@ -4528,7 +4724,7 @@ function CanonicalKeyConfirmView({
|
|
|
4528
4724
|
return (
|
|
4529
4725
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4530
4726
|
{suggestionQuery.data?.message ??
|
|
4531
|
-
"Couldn
|
|
4727
|
+
"Couldn't find a match field automatically."}
|
|
4532
4728
|
</div>
|
|
4533
4729
|
);
|
|
4534
4730
|
}
|
|
@@ -4540,7 +4736,7 @@ function CanonicalKeyConfirmView({
|
|
|
4540
4736
|
{candidate.displayName}
|
|
4541
4737
|
</div>
|
|
4542
4738
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4543
|
-
Match
|
|
4739
|
+
Match existing items to details. Suggested match:{" "}
|
|
4544
4740
|
<span className="font-medium text-foreground">
|
|
4545
4741
|
{suggestion.canonicalKey.label}
|
|
4546
4742
|
</span>
|
|
@@ -4630,7 +4826,7 @@ function CanonicalKeyConfirmView({
|
|
|
4630
4826
|
) : (
|
|
4631
4827
|
<IconPlugConnected className="mr-1.5 size-3.5" />
|
|
4632
4828
|
)}
|
|
4633
|
-
|
|
4829
|
+
Add details
|
|
4634
4830
|
</Button>
|
|
4635
4831
|
</div>
|
|
4636
4832
|
);
|
|
@@ -4705,11 +4901,17 @@ function SecondarySourceLeaf({
|
|
|
4705
4901
|
source,
|
|
4706
4902
|
canEdit,
|
|
4707
4903
|
pending,
|
|
4904
|
+
onAddDetails,
|
|
4905
|
+
onAddItems,
|
|
4906
|
+
onChooseFields,
|
|
4708
4907
|
onDisconnect,
|
|
4709
4908
|
}: {
|
|
4710
4909
|
source: ContentDatabaseSource | null;
|
|
4711
4910
|
canEdit: boolean;
|
|
4712
4911
|
pending: boolean;
|
|
4912
|
+
onAddDetails: () => void;
|
|
4913
|
+
onAddItems: () => void | Promise<void>;
|
|
4914
|
+
onChooseFields: () => void;
|
|
4713
4915
|
onDisconnect: () => void;
|
|
4714
4916
|
}) {
|
|
4715
4917
|
if (!source) {
|
|
@@ -4734,12 +4936,22 @@ function SecondarySourceLeaf({
|
|
|
4734
4936
|
</span>
|
|
4735
4937
|
</div>
|
|
4736
4938
|
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
4737
|
-
{
|
|
4939
|
+
{`${sourceRoleLabel(source, 0)} · ${fieldCount} field${
|
|
4940
|
+
fieldCount === 1 ? "" : "s"
|
|
4941
|
+
}`}
|
|
4738
4942
|
{federation?.canonicalKey?.label
|
|
4739
4943
|
? ` · joined on ${federation.canonicalKey.label}`
|
|
4740
4944
|
: ""}
|
|
4741
4945
|
</div>
|
|
4742
4946
|
</div>
|
|
4947
|
+
<SourceRoleCard
|
|
4948
|
+
source={source}
|
|
4949
|
+
canEdit={canEdit}
|
|
4950
|
+
pending={pending}
|
|
4951
|
+
onAddDetails={onAddDetails}
|
|
4952
|
+
onAddItems={onAddItems}
|
|
4953
|
+
onChooseFields={onChooseFields}
|
|
4954
|
+
/>
|
|
4743
4955
|
{federation ? (
|
|
4744
4956
|
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3 text-xs">
|
|
4745
4957
|
<div className="font-medium">{dbText("matchFormula")}</div>
|
|
@@ -4774,6 +4986,375 @@ function SecondarySourceLeaf({
|
|
|
4774
4986
|
);
|
|
4775
4987
|
}
|
|
4776
4988
|
|
|
4989
|
+
function sourceAddsDetails(source: ContentDatabaseSource | null | undefined) {
|
|
4990
|
+
return source?.metadata.federation?.role === "secondary";
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
function sourceRoleLabel(
|
|
4994
|
+
source: ContentDatabaseSource | null | undefined,
|
|
4995
|
+
_index: number,
|
|
4996
|
+
) {
|
|
4997
|
+
return sourceAddsDetails(source)
|
|
4998
|
+
? dbText("addingDetails")
|
|
4999
|
+
: dbText("addingItems");
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
function detailsReasonText(suggestion: SourceJoinSuggestion | null) {
|
|
5003
|
+
if (!suggestion)
|
|
5004
|
+
return dbText("recommendedWhenCollectionDescribesExistingRows");
|
|
5005
|
+
const percent = Math.round(suggestion.confidence * 100);
|
|
5006
|
+
return dbText("recommendedBecauseSampledRowsMatchOn", {
|
|
5007
|
+
field: suggestion.canonicalKey.label,
|
|
5008
|
+
percent,
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
function findDetailsSource(
|
|
5013
|
+
response: ContentDatabaseResponse,
|
|
5014
|
+
candidate: PendingSourceCandidate,
|
|
5015
|
+
) {
|
|
5016
|
+
return (
|
|
5017
|
+
(response.sources ?? []).find((source) => {
|
|
5018
|
+
if (!sourceAddsDetails(source)) return false;
|
|
5019
|
+
if (candidate.existingSourceId) {
|
|
5020
|
+
return source.id === candidate.existingSourceId;
|
|
5021
|
+
}
|
|
5022
|
+
return (
|
|
5023
|
+
source.sourceType === candidate.sourceType &&
|
|
5024
|
+
source.sourceTable === candidate.sourceTable
|
|
5025
|
+
);
|
|
5026
|
+
}) ?? null
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
function SourceRelationshipChoice({
|
|
5031
|
+
documentId,
|
|
5032
|
+
candidate,
|
|
5033
|
+
canEdit,
|
|
5034
|
+
pending,
|
|
5035
|
+
onAddDetails,
|
|
5036
|
+
onAddItems,
|
|
5037
|
+
}: {
|
|
5038
|
+
documentId: string;
|
|
5039
|
+
candidate: PendingSourceCandidate;
|
|
5040
|
+
canEdit: boolean;
|
|
5041
|
+
pending: boolean;
|
|
5042
|
+
onAddDetails: () => void;
|
|
5043
|
+
onAddItems: () => void | Promise<void>;
|
|
5044
|
+
}) {
|
|
5045
|
+
const suggestionQuery = useSuggestSourceJoinKey({
|
|
5046
|
+
documentId,
|
|
5047
|
+
candidateSourceType: candidate.sourceType,
|
|
5048
|
+
candidateSourceTable: candidate.sourceTable,
|
|
5049
|
+
enabled: true,
|
|
5050
|
+
});
|
|
5051
|
+
const suggestion = suggestionQuery.data?.suggestion ?? null;
|
|
5052
|
+
const detailsRecommended = Boolean(suggestion);
|
|
5053
|
+
return (
|
|
5054
|
+
<div className="grid min-w-0 gap-2">
|
|
5055
|
+
<button
|
|
5056
|
+
type="button"
|
|
5057
|
+
disabled={!canEdit || pending}
|
|
5058
|
+
className={cn(
|
|
5059
|
+
"grid min-w-0 gap-1 rounded-lg border p-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60",
|
|
5060
|
+
detailsRecommended
|
|
5061
|
+
? "border-foreground/30 bg-muted/30"
|
|
5062
|
+
: "border-border bg-background hover:bg-muted/50",
|
|
5063
|
+
)}
|
|
5064
|
+
onClick={onAddDetails}
|
|
5065
|
+
>
|
|
5066
|
+
<span className="grid min-w-0 gap-1">
|
|
5067
|
+
<span className="break-words text-sm font-medium leading-snug">
|
|
5068
|
+
{dbText("addDetailsToExistingItems")}
|
|
5069
|
+
</span>
|
|
5070
|
+
{detailsRecommended ? (
|
|
5071
|
+
<span className="w-fit rounded-full bg-foreground px-2 py-0.5 text-[10px] font-medium text-background">
|
|
5072
|
+
{dbText("recommended")}
|
|
5073
|
+
</span>
|
|
5074
|
+
) : null}
|
|
5075
|
+
</span>
|
|
5076
|
+
<span className="break-words text-xs text-muted-foreground">
|
|
5077
|
+
{suggestionQuery.isLoading
|
|
5078
|
+
? dbText("checkingForMatchingFields")
|
|
5079
|
+
: detailsReasonText(suggestion)}
|
|
5080
|
+
</span>
|
|
5081
|
+
</button>
|
|
5082
|
+
<button
|
|
5083
|
+
type="button"
|
|
5084
|
+
disabled={!canEdit || pending}
|
|
5085
|
+
className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-default disabled:opacity-60"
|
|
5086
|
+
onClick={onAddItems}
|
|
5087
|
+
>
|
|
5088
|
+
<span className="truncate text-sm font-medium">
|
|
5089
|
+
{dbText("addMoreItemsToThisList")}
|
|
5090
|
+
</span>
|
|
5091
|
+
<span className="break-words text-xs text-muted-foreground">
|
|
5092
|
+
{dbText("bestWhenSameKindAdditionalRows")}
|
|
5093
|
+
</span>
|
|
5094
|
+
</button>
|
|
5095
|
+
</div>
|
|
5096
|
+
);
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
function SourceRoleCard({
|
|
5100
|
+
source,
|
|
5101
|
+
canAddDetails = true,
|
|
5102
|
+
canEdit,
|
|
5103
|
+
pending,
|
|
5104
|
+
onAddDetails,
|
|
5105
|
+
onAddItems,
|
|
5106
|
+
onChooseFields,
|
|
5107
|
+
}: {
|
|
5108
|
+
source: ContentDatabaseSource;
|
|
5109
|
+
canAddDetails?: boolean;
|
|
5110
|
+
canEdit: boolean;
|
|
5111
|
+
pending: boolean;
|
|
5112
|
+
onAddDetails: () => void;
|
|
5113
|
+
onAddItems: () => void | Promise<void>;
|
|
5114
|
+
onChooseFields: () => void;
|
|
5115
|
+
}) {
|
|
5116
|
+
const addingDetails = sourceAddsDetails(source);
|
|
5117
|
+
return (
|
|
5118
|
+
<div className="grid min-w-0 gap-2 rounded-lg border border-border bg-background p-3">
|
|
5119
|
+
<div className="grid min-w-0 gap-0.5">
|
|
5120
|
+
<div className="text-xs font-medium">{dbText("sourceRole")}</div>
|
|
5121
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5122
|
+
{addingDetails
|
|
5123
|
+
? dbText("addingDetailsMatchedOn", {
|
|
5124
|
+
field:
|
|
5125
|
+
source.metadata.federation?.canonicalKey?.label ??
|
|
5126
|
+
dbText("aField"),
|
|
5127
|
+
})
|
|
5128
|
+
: dbText("addingItemsAsRows")}
|
|
5129
|
+
</div>
|
|
5130
|
+
</div>
|
|
5131
|
+
<div className="flex min-w-0 flex-wrap gap-2">
|
|
5132
|
+
{addingDetails ? (
|
|
5133
|
+
<>
|
|
5134
|
+
<Button
|
|
5135
|
+
type="button"
|
|
5136
|
+
size="sm"
|
|
5137
|
+
variant="outline"
|
|
5138
|
+
className="h-8 text-xs"
|
|
5139
|
+
disabled={!canEdit || pending}
|
|
5140
|
+
onClick={onChooseFields}
|
|
5141
|
+
>
|
|
5142
|
+
{dbText("chooseFields")}
|
|
5143
|
+
</Button>
|
|
5144
|
+
<Button
|
|
5145
|
+
type="button"
|
|
5146
|
+
size="sm"
|
|
5147
|
+
variant="outline"
|
|
5148
|
+
className="h-8 text-xs"
|
|
5149
|
+
disabled={!canEdit || pending}
|
|
5150
|
+
onClick={onAddItems}
|
|
5151
|
+
>
|
|
5152
|
+
{dbText("addAsItems")}
|
|
5153
|
+
</Button>
|
|
5154
|
+
</>
|
|
5155
|
+
) : canAddDetails ? (
|
|
5156
|
+
<Button
|
|
5157
|
+
type="button"
|
|
5158
|
+
size="sm"
|
|
5159
|
+
variant="outline"
|
|
5160
|
+
className="h-8 text-xs"
|
|
5161
|
+
disabled={!canEdit || pending}
|
|
5162
|
+
onClick={onAddDetails}
|
|
5163
|
+
>
|
|
5164
|
+
{dbText("addDetailsInstead")}
|
|
5165
|
+
</Button>
|
|
5166
|
+
) : (
|
|
5167
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5168
|
+
{dbText("addAnotherItemSourceBeforeChangingToDetails")}
|
|
5169
|
+
</div>
|
|
5170
|
+
)}
|
|
5171
|
+
</div>
|
|
5172
|
+
</div>
|
|
5173
|
+
);
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
const DETAIL_FIELD_NAME_HINTS = [
|
|
5177
|
+
"name",
|
|
5178
|
+
"title",
|
|
5179
|
+
"bio",
|
|
5180
|
+
"description",
|
|
5181
|
+
"image",
|
|
5182
|
+
"photo",
|
|
5183
|
+
"avatar",
|
|
5184
|
+
"url",
|
|
5185
|
+
"handle",
|
|
5186
|
+
"email",
|
|
5187
|
+
];
|
|
5188
|
+
|
|
5189
|
+
function shouldPreselectDetailField(
|
|
5190
|
+
field: ContentDatabaseSource["fields"][number],
|
|
5191
|
+
) {
|
|
5192
|
+
const key = `${field.sourceFieldKey} ${field.sourceFieldLabel}`.toLowerCase();
|
|
5193
|
+
if (
|
|
5194
|
+
field.propertyId ||
|
|
5195
|
+
field.mappingType === "system" ||
|
|
5196
|
+
field.mappingType === "title"
|
|
5197
|
+
) {
|
|
5198
|
+
return false;
|
|
5199
|
+
}
|
|
5200
|
+
if (/(\b|\.|_)(id|created|updated|published|rev)(\b|\.|_)/i.test(key)) {
|
|
5201
|
+
return false;
|
|
5202
|
+
}
|
|
5203
|
+
return DETAIL_FIELD_NAME_HINTS.some((hint) => key.includes(hint));
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
function SourceDetailsFieldPicker({
|
|
5207
|
+
documentId,
|
|
5208
|
+
source,
|
|
5209
|
+
canEdit,
|
|
5210
|
+
pending,
|
|
5211
|
+
onDone,
|
|
5212
|
+
}: {
|
|
5213
|
+
documentId: string;
|
|
5214
|
+
source: ContentDatabaseSource | null;
|
|
5215
|
+
canEdit: boolean;
|
|
5216
|
+
pending: boolean;
|
|
5217
|
+
onDone: () => void;
|
|
5218
|
+
}) {
|
|
5219
|
+
const addField = useAddContentDatabaseSourceFieldProperty(documentId);
|
|
5220
|
+
const fields = useMemo(
|
|
5221
|
+
() =>
|
|
5222
|
+
(source?.fields ?? []).filter(
|
|
5223
|
+
(field) =>
|
|
5224
|
+
!field.propertyId &&
|
|
5225
|
+
field.mappingType !== "system" &&
|
|
5226
|
+
field.mappingType !== "title",
|
|
5227
|
+
),
|
|
5228
|
+
[source],
|
|
5229
|
+
);
|
|
5230
|
+
const defaultSelectedIds = useMemo(
|
|
5231
|
+
() =>
|
|
5232
|
+
fields
|
|
5233
|
+
.filter(shouldPreselectDetailField)
|
|
5234
|
+
.slice(0, 8)
|
|
5235
|
+
.map((field) => field.id),
|
|
5236
|
+
[fields],
|
|
5237
|
+
);
|
|
5238
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
5239
|
+
|
|
5240
|
+
useEffect(() => {
|
|
5241
|
+
setSelectedIds(defaultSelectedIds);
|
|
5242
|
+
}, [defaultSelectedIds]);
|
|
5243
|
+
|
|
5244
|
+
if (!source) {
|
|
5245
|
+
return (
|
|
5246
|
+
<div className="min-w-0 break-words text-xs text-muted-foreground">
|
|
5247
|
+
{dbText("thisSourceIsNoLongerConnected")}
|
|
5248
|
+
</div>
|
|
5249
|
+
);
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
const selected = new Set(selectedIds);
|
|
5253
|
+
const toggleField = (fieldId: string) => {
|
|
5254
|
+
setSelectedIds((current) =>
|
|
5255
|
+
current.includes(fieldId)
|
|
5256
|
+
? current.filter((id) => id !== fieldId)
|
|
5257
|
+
: [...current, fieldId],
|
|
5258
|
+
);
|
|
5259
|
+
};
|
|
5260
|
+
const addSelected = async () => {
|
|
5261
|
+
const sourceFieldIds = fields
|
|
5262
|
+
.filter((field) => selected.has(field.id))
|
|
5263
|
+
.map((field) => field.id);
|
|
5264
|
+
if (sourceFieldIds.length === 0) {
|
|
5265
|
+
onDone();
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
try {
|
|
5269
|
+
for (const sourceFieldId of sourceFieldIds) {
|
|
5270
|
+
await addField.mutateAsync({ documentId, sourceFieldId });
|
|
5271
|
+
}
|
|
5272
|
+
toast.success(dbText("detailFieldsAdded"), {
|
|
5273
|
+
description:
|
|
5274
|
+
sourceFieldIds.length === 1
|
|
5275
|
+
? dbText("addedOneFieldFromSource")
|
|
5276
|
+
: dbText("addedFieldsFromSource", { count: sourceFieldIds.length }),
|
|
5277
|
+
});
|
|
5278
|
+
onDone();
|
|
5279
|
+
} catch (error) {
|
|
5280
|
+
toast.error(dbText("fieldsWereNotAdded"), {
|
|
5281
|
+
description:
|
|
5282
|
+
error instanceof Error ? error.message : dbText("tryAgain"),
|
|
5283
|
+
});
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
|
|
5287
|
+
return (
|
|
5288
|
+
<div className="grid min-w-0 gap-3">
|
|
5289
|
+
<div className="grid min-w-0 gap-1 rounded-lg border border-border bg-background p-3">
|
|
5290
|
+
<div className="truncate text-sm font-medium" title={source.sourceName}>
|
|
5291
|
+
{source.sourceName}
|
|
5292
|
+
</div>
|
|
5293
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5294
|
+
{dbText("pickDetailsBecomeColumns")}
|
|
5295
|
+
</div>
|
|
5296
|
+
</div>
|
|
5297
|
+
{fields.length === 0 ? (
|
|
5298
|
+
<div className="break-words text-xs text-muted-foreground">
|
|
5299
|
+
{dbText("allAvailableDetailFieldsAlreadyVisible")}
|
|
5300
|
+
</div>
|
|
5301
|
+
) : (
|
|
5302
|
+
<div className="grid min-w-0 gap-1">
|
|
5303
|
+
{fields.map((field) => (
|
|
5304
|
+
<button
|
|
5305
|
+
key={field.id}
|
|
5306
|
+
type="button"
|
|
5307
|
+
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
5308
|
+
onClick={() => toggleField(field.id)}
|
|
5309
|
+
>
|
|
5310
|
+
<span
|
|
5311
|
+
className={cn(
|
|
5312
|
+
"flex size-4 shrink-0 items-center justify-center rounded border",
|
|
5313
|
+
selected.has(field.id)
|
|
5314
|
+
? "border-[#2383e2] bg-[#2383e2] text-white"
|
|
5315
|
+
: "border-muted-foreground/40 text-transparent",
|
|
5316
|
+
)}
|
|
5317
|
+
>
|
|
5318
|
+
<IconCheck className="size-3" />
|
|
5319
|
+
</span>
|
|
5320
|
+
<span className="min-w-0 flex-1 truncate">
|
|
5321
|
+
{field.sourceFieldLabel}
|
|
5322
|
+
</span>
|
|
5323
|
+
<span className="shrink-0 text-[11px] text-muted-foreground">
|
|
5324
|
+
{field.sourceFieldType}
|
|
5325
|
+
</span>
|
|
5326
|
+
</button>
|
|
5327
|
+
))}
|
|
5328
|
+
</div>
|
|
5329
|
+
)}
|
|
5330
|
+
<div className="flex justify-end gap-2">
|
|
5331
|
+
<Button
|
|
5332
|
+
type="button"
|
|
5333
|
+
size="sm"
|
|
5334
|
+
variant="outline"
|
|
5335
|
+
disabled={addField.isPending}
|
|
5336
|
+
onClick={onDone}
|
|
5337
|
+
>
|
|
5338
|
+
Done
|
|
5339
|
+
</Button>
|
|
5340
|
+
<Button
|
|
5341
|
+
type="button"
|
|
5342
|
+
size="sm"
|
|
5343
|
+
disabled={!canEdit || pending || addField.isPending}
|
|
5344
|
+
onClick={addSelected}
|
|
5345
|
+
>
|
|
5346
|
+
{addField.isPending ? (
|
|
5347
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
5348
|
+
) : (
|
|
5349
|
+
<IconPlus className="mr-1.5 size-3.5" />
|
|
5350
|
+
)}
|
|
5351
|
+
Add selected
|
|
5352
|
+
</Button>
|
|
5353
|
+
</div>
|
|
5354
|
+
</div>
|
|
5355
|
+
);
|
|
5356
|
+
}
|
|
5357
|
+
|
|
4777
5358
|
// A Builder space's data models, as drill-in rows. The attached model (if any)
|
|
4778
5359
|
// is marked; selecting a row opens that model's leaf.
|
|
4779
5360
|
function BuilderSpaceModelsView({
|