@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
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
|
+
import { BUILDER_CMS_SAFE_WRITE_MODEL } from "@shared/api";
|
|
2
3
|
import type {
|
|
4
|
+
BuilderCmsPublicationTransitionIntent,
|
|
5
|
+
BuilderCmsWriteEffect,
|
|
3
6
|
ContentDatabaseSource,
|
|
4
|
-
ContentDatabaseSourceChangeSet,
|
|
5
7
|
ContentDatabaseSourceReviewPayload,
|
|
6
8
|
DocumentPropertyValue,
|
|
9
|
+
ExecuteBuilderSourceBatchTransition,
|
|
10
|
+
ExecuteBuilderSourceBatchResponse,
|
|
7
11
|
} from "@shared/api";
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
IconAlertTriangle,
|
|
14
|
+
IconCheck,
|
|
15
|
+
IconCloudUpload,
|
|
16
|
+
IconX,
|
|
17
|
+
} from "@tabler/icons-react";
|
|
18
|
+
import { useEffect, useMemo, useState } from "react";
|
|
9
19
|
|
|
10
20
|
import { Button } from "@/components/ui/button";
|
|
11
21
|
import {
|
|
@@ -16,54 +26,190 @@ import {
|
|
|
16
26
|
} from "@/components/ui/dialog";
|
|
17
27
|
import { Spinner } from "@/components/ui/spinner";
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
export type BuilderReviewPublicationTransitionSelection = {
|
|
30
|
+
publicationTransition: BuilderCmsPublicationTransitionIntent;
|
|
31
|
+
confirmUnpublish?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type BuilderReviewPublicationTransitionSelections = Record<
|
|
35
|
+
string,
|
|
36
|
+
BuilderReviewPublicationTransitionSelection
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export type BuilderReviewPublicationTransitions = Record<
|
|
40
|
+
string,
|
|
41
|
+
ExecuteBuilderSourceBatchTransition
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
const EFFECT_LABELS: Record<
|
|
45
|
+
BuilderCmsWriteEffect,
|
|
46
|
+
{ tag: string; sentence: string }
|
|
47
|
+
> = {
|
|
48
|
+
create_draft: { tag: "New", sentence: "Creates a new draft entry" },
|
|
49
|
+
update_in_place: { tag: "Edit", sentence: "Updates the live entry" },
|
|
50
|
+
autosave: { tag: "Draft", sentence: "Saves a draft revision" },
|
|
51
|
+
publish: { tag: "Publish", sentence: "Publishes this entry" },
|
|
52
|
+
unpublish: { tag: "Unpublish", sentence: "Unpublishes the live entry" },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function builderReviewRowEffectLabel(effect: BuilderCmsWriteEffect) {
|
|
56
|
+
return EFFECT_LABELS[effect] ?? EFFECT_LABELS.update_in_place;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The effect a row will actually run, accounting for a chosen transition. */
|
|
60
|
+
export function builderReviewEffectiveRowEffect(
|
|
61
|
+
baseEffect: BuilderCmsWriteEffect,
|
|
62
|
+
selection?: BuilderReviewPublicationTransitionSelection,
|
|
63
|
+
): BuilderCmsWriteEffect {
|
|
64
|
+
// A create has no Builder entry yet, so a publish/unpublish transition can't
|
|
65
|
+
// apply — the adapter always writes a draft (create_draft) when there's no
|
|
66
|
+
// entry id. Never let a transition relabel a create.
|
|
67
|
+
if (baseEffect === "create_draft") return "create_draft";
|
|
68
|
+
if (selection?.publicationTransition === "publish") return "publish";
|
|
69
|
+
if (selection?.publicationTransition === "unpublish") return "unpublish";
|
|
70
|
+
return baseEffect;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function builderReviewIntentSummary(
|
|
74
|
+
rows: { changeSetId: string; effect: BuilderCmsWriteEffect }[],
|
|
75
|
+
selections: BuilderReviewPublicationTransitionSelections,
|
|
76
|
+
) {
|
|
77
|
+
const counts: Record<BuilderCmsWriteEffect, number> = {
|
|
78
|
+
create_draft: 0,
|
|
79
|
+
update_in_place: 0,
|
|
80
|
+
autosave: 0,
|
|
81
|
+
publish: 0,
|
|
82
|
+
unpublish: 0,
|
|
83
|
+
};
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
counts[
|
|
86
|
+
builderReviewEffectiveRowEffect(row.effect, selections[row.changeSetId])
|
|
87
|
+
] += 1;
|
|
88
|
+
}
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
if (counts.create_draft) {
|
|
91
|
+
parts.push(
|
|
92
|
+
`${counts.create_draft} draft${counts.create_draft === 1 ? "" : "s"} to create`,
|
|
93
|
+
);
|
|
22
94
|
}
|
|
23
|
-
if (
|
|
24
|
-
|
|
95
|
+
if (counts.update_in_place) {
|
|
96
|
+
parts.push(
|
|
97
|
+
`${counts.update_in_place} update${counts.update_in_place === 1 ? "" : "s"}`,
|
|
98
|
+
);
|
|
25
99
|
}
|
|
26
|
-
|
|
100
|
+
if (counts.autosave) {
|
|
101
|
+
parts.push(
|
|
102
|
+
`${counts.autosave} draft save${counts.autosave === 1 ? "" : "s"}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (counts.publish) parts.push(`${counts.publish} to publish`);
|
|
106
|
+
if (counts.unpublish) parts.push(`${counts.unpublish} to unpublish`);
|
|
107
|
+
return parts.join(" · ") || "No changes";
|
|
27
108
|
}
|
|
28
109
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
110
|
+
export function builderReviewDestinationLine(args: {
|
|
111
|
+
rows: { changeSetId: string; effect: BuilderCmsWriteEffect }[];
|
|
112
|
+
selections: BuilderReviewPublicationTransitionSelections;
|
|
113
|
+
liveWritesEnabled: boolean;
|
|
114
|
+
}) {
|
|
115
|
+
if (!args.liveWritesEnabled) {
|
|
116
|
+
return "Checks the update only — nothing is sent to Builder.";
|
|
117
|
+
}
|
|
118
|
+
const effects = new Set(
|
|
119
|
+
args.rows.map((row) =>
|
|
120
|
+
builderReviewEffectiveRowEffect(
|
|
121
|
+
row.effect,
|
|
122
|
+
args.selections[row.changeSetId],
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
if (effects.has("unpublish")) {
|
|
127
|
+
return "Unpublishes selected entries in Builder.";
|
|
128
|
+
}
|
|
129
|
+
if (effects.has("publish")) {
|
|
130
|
+
return "Publishes selected entries in Builder.";
|
|
131
|
+
}
|
|
132
|
+
if (effects.size === 1 && effects.has("create_draft")) {
|
|
133
|
+
return args.rows.length === 1
|
|
134
|
+
? "Writes a new draft to Builder — won't publish."
|
|
135
|
+
: "Writes new drafts to Builder — nothing is published.";
|
|
136
|
+
}
|
|
137
|
+
return "Updates content in Builder — publication state is preserved.";
|
|
34
138
|
}
|
|
35
139
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
140
|
+
export function builderReviewResultStatus(status?: string): {
|
|
141
|
+
// i18n key under the `database.` namespace; resolved by the caller via t().
|
|
142
|
+
labelKey: string;
|
|
143
|
+
tone: "ok" | "warn" | "danger" | "muted";
|
|
144
|
+
} {
|
|
145
|
+
switch (status) {
|
|
146
|
+
case "succeeded":
|
|
147
|
+
return { labelKey: "pushed", tone: "ok" };
|
|
148
|
+
case "validated":
|
|
149
|
+
return { labelKey: "ready", tone: "ok" };
|
|
150
|
+
case "partial":
|
|
151
|
+
case "blocked":
|
|
152
|
+
return { labelKey: "needsAttention", tone: "warn" };
|
|
153
|
+
case "failed":
|
|
154
|
+
return { labelKey: "failedYouCanRetry", tone: "danger" };
|
|
155
|
+
case "stale":
|
|
156
|
+
return { labelKey: "needsAFreshReview", tone: "warn" };
|
|
157
|
+
case "running":
|
|
158
|
+
return { labelKey: "working", tone: "muted" };
|
|
159
|
+
case "write_disabled":
|
|
160
|
+
return { labelKey: "checksOnly", tone: "muted" };
|
|
161
|
+
default:
|
|
162
|
+
return { labelKey: "ready", tone: "muted" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resultToneClass(tone: "ok" | "warn" | "danger" | "muted") {
|
|
167
|
+
if (tone === "ok") return "text-emerald-600 dark:text-emerald-400";
|
|
168
|
+
if (tone === "warn") return "text-amber-700 dark:text-amber-300";
|
|
169
|
+
if (tone === "danger") return "text-destructive";
|
|
170
|
+
return "text-muted-foreground";
|
|
48
171
|
}
|
|
49
172
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
t: ReturnType<typeof useT>,
|
|
173
|
+
export function builderReviewPublicationTransitionsMap(
|
|
174
|
+
selections: BuilderReviewPublicationTransitionSelections,
|
|
53
175
|
) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
176
|
+
const transitions: BuilderReviewPublicationTransitions = {};
|
|
177
|
+
|
|
178
|
+
for (const [changeSetId, selection] of Object.entries(selections)) {
|
|
179
|
+
if (selection.publicationTransition === "publish") {
|
|
180
|
+
transitions[changeSetId] = { publicationTransition: "publish" };
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
selection.publicationTransition === "unpublish" &&
|
|
186
|
+
selection.confirmUnpublish === true
|
|
187
|
+
) {
|
|
188
|
+
transitions[changeSetId] = {
|
|
189
|
+
publicationTransition: "unpublish",
|
|
190
|
+
confirmUnpublish: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return transitions;
|
|
58
196
|
}
|
|
59
197
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
198
|
+
const ATTENTION_TAG_EFFECTS: ReadonlySet<BuilderCmsWriteEffect> = new Set([
|
|
199
|
+
"unpublish",
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
function rowEffectTagClass(effect: BuilderCmsWriteEffect) {
|
|
203
|
+
return ATTENTION_TAG_EFFECTS.has(effect)
|
|
204
|
+
? "rounded border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300"
|
|
205
|
+
: "rounded border border-border bg-muted/40 px-1.5 py-0.5 text-muted-foreground";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sourceValueText(value: DocumentPropertyValue) {
|
|
209
|
+
if (value === null || value === undefined || value === "") return "empty";
|
|
210
|
+
if (Array.isArray(value)) return value.join(", ") || "empty";
|
|
211
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
212
|
+
return String(value);
|
|
67
213
|
}
|
|
68
214
|
|
|
69
215
|
export function BuilderSourceReviewDialog({
|
|
@@ -72,6 +218,7 @@ export function BuilderSourceReviewDialog({
|
|
|
72
218
|
source,
|
|
73
219
|
canEdit,
|
|
74
220
|
pending,
|
|
221
|
+
batchResult = null,
|
|
75
222
|
checkedAt,
|
|
76
223
|
onClose,
|
|
77
224
|
onValidate,
|
|
@@ -81,44 +228,179 @@ export function BuilderSourceReviewDialog({
|
|
|
81
228
|
source: ContentDatabaseSource | null;
|
|
82
229
|
canEdit: boolean;
|
|
83
230
|
pending: boolean;
|
|
231
|
+
// Optional so the inline-database caller (DatabaseView) that doesn't surface
|
|
232
|
+
// batch results can still mount the dialog.
|
|
233
|
+
batchResult?: ExecuteBuilderSourceBatchResponse | null;
|
|
84
234
|
checkedAt: string | null;
|
|
85
235
|
onClose: () => void;
|
|
86
|
-
onValidate: () => void;
|
|
236
|
+
onValidate: (transitions: BuilderReviewPublicationTransitions) => void;
|
|
87
237
|
}) {
|
|
88
238
|
const t = useT();
|
|
89
239
|
const checked = !!checkedAt;
|
|
240
|
+
const safeModel =
|
|
241
|
+
source?.sourceType === "builder-cms" &&
|
|
242
|
+
source.sourceTable === BUILDER_CMS_SAFE_WRITE_MODEL;
|
|
243
|
+
const writeMode = source?.metadata.writeMode;
|
|
244
|
+
const allowPublicationTransitionControls =
|
|
245
|
+
safeModel &&
|
|
246
|
+
writeMode === "publish_updates" &&
|
|
247
|
+
source?.metadata.allowPublicationTransitions === true;
|
|
248
|
+
const reviewRows = useMemo(() => review?.rows ?? [], [review]);
|
|
249
|
+
const reviewRowIds = useMemo(
|
|
250
|
+
() => reviewRows.map((row) => row.changeSetId),
|
|
251
|
+
[reviewRows],
|
|
252
|
+
);
|
|
253
|
+
const reviewRowIdsKey = reviewRowIds.join("\u0000");
|
|
254
|
+
const [transitionSelections, setTransitionSelections] =
|
|
255
|
+
useState<BuilderReviewPublicationTransitionSelections>({});
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (!open || !allowPublicationTransitionControls) {
|
|
258
|
+
setTransitionSelections({});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const reviewRowIdSet = new Set(
|
|
263
|
+
reviewRowIdsKey ? reviewRowIdsKey.split("\u0000") : [],
|
|
264
|
+
);
|
|
265
|
+
setTransitionSelections((current) => {
|
|
266
|
+
const next: BuilderReviewPublicationTransitionSelections = {};
|
|
267
|
+
for (const [changeSetId, selection] of Object.entries(current)) {
|
|
268
|
+
if (reviewRowIdSet.has(changeSetId)) next[changeSetId] = selection;
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(next).length === Object.keys(current).length
|
|
271
|
+
? current
|
|
272
|
+
: next;
|
|
273
|
+
});
|
|
274
|
+
}, [allowPublicationTransitionControls, open, reviewRowIdsKey]);
|
|
275
|
+
const transitionMap = useMemo(
|
|
276
|
+
() => builderReviewPublicationTransitionsMap(transitionSelections),
|
|
277
|
+
[transitionSelections],
|
|
278
|
+
);
|
|
279
|
+
const intentSummary = builderReviewIntentSummary(
|
|
280
|
+
reviewRows,
|
|
281
|
+
transitionSelections,
|
|
282
|
+
);
|
|
283
|
+
const destinationLine = builderReviewDestinationLine({
|
|
284
|
+
rows: reviewRows,
|
|
285
|
+
selections: transitionSelections,
|
|
286
|
+
liveWritesEnabled: review?.liveWritesEnabled === true,
|
|
287
|
+
});
|
|
288
|
+
const hasUnconfirmedUnpublish = Object.values(transitionSelections).some(
|
|
289
|
+
(selection) =>
|
|
290
|
+
selection.publicationTransition === "unpublish" &&
|
|
291
|
+
selection.confirmUnpublish !== true,
|
|
292
|
+
);
|
|
293
|
+
const batchHasIssues =
|
|
294
|
+
!!batchResult &&
|
|
295
|
+
(batchResult.summary.blocked > 0 || batchResult.summary.failed > 0);
|
|
90
296
|
const retryable =
|
|
91
297
|
review?.result.status === "failed" ||
|
|
92
298
|
review?.result.status === "blocked" ||
|
|
93
|
-
review?.result.status === "stale"
|
|
299
|
+
review?.result.status === "stale" ||
|
|
300
|
+
batchHasIssues;
|
|
301
|
+
const unsafeLiveTarget = review?.liveWritesEnabled === true && !safeModel;
|
|
94
302
|
const disabled =
|
|
95
303
|
!canEdit ||
|
|
96
304
|
pending ||
|
|
97
305
|
(!retryable && checked) ||
|
|
98
306
|
!review ||
|
|
99
|
-
review.rows.length === 0
|
|
100
|
-
|
|
307
|
+
review.rows.length === 0 ||
|
|
308
|
+
unsafeLiveTarget ||
|
|
309
|
+
hasUnconfirmedUnpublish;
|
|
310
|
+
const rowTitleById = new Map(
|
|
311
|
+
reviewRows.map((row) => [row.changeSetId, row.title]),
|
|
312
|
+
);
|
|
313
|
+
const batchIssueResults =
|
|
314
|
+
batchResult?.results.filter((result) => result.status !== "succeeded") ??
|
|
315
|
+
[];
|
|
316
|
+
const resultStatus = builderReviewResultStatus(
|
|
317
|
+
batchResult
|
|
318
|
+
? batchHasIssues
|
|
319
|
+
? "partial"
|
|
320
|
+
: "succeeded"
|
|
321
|
+
: review?.result.status,
|
|
322
|
+
);
|
|
323
|
+
const footerHint = pending
|
|
101
324
|
? review?.liveWritesEnabled
|
|
102
|
-
?
|
|
103
|
-
:
|
|
104
|
-
:
|
|
105
|
-
?
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
325
|
+
? "Sending to Builder…"
|
|
326
|
+
: "Checking…"
|
|
327
|
+
: hasUnconfirmedUnpublish
|
|
328
|
+
? "Confirm unpublish on the selected rows first."
|
|
329
|
+
: unsafeLiveTarget
|
|
330
|
+
? `Live pushes are limited to the ${BUILDER_CMS_SAFE_WRITE_MODEL} test model.`
|
|
331
|
+
: null;
|
|
332
|
+
const effectiveEffects = new Set(
|
|
333
|
+
reviewRows.map((row) =>
|
|
334
|
+
builderReviewEffectiveRowEffect(
|
|
335
|
+
row.effect,
|
|
336
|
+
transitionSelections[row.changeSetId],
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
const pushVerb = !review?.liveWritesEnabled
|
|
341
|
+
? "Check"
|
|
342
|
+
: effectiveEffects.has("unpublish")
|
|
343
|
+
? "Unpublish"
|
|
344
|
+
: effectiveEffects.has("publish")
|
|
345
|
+
? "Publish"
|
|
346
|
+
: effectiveEffects.size === 1 && effectiveEffects.has("create_draft")
|
|
347
|
+
? reviewRows.length > 1
|
|
348
|
+
? `Create ${reviewRows.length} drafts`
|
|
349
|
+
: "Create draft"
|
|
350
|
+
: reviewRows.length > 1
|
|
351
|
+
? `Push ${reviewRows.length} updates`
|
|
352
|
+
: "Push update";
|
|
113
353
|
const buttonLabel = pending
|
|
114
354
|
? review?.liveWritesEnabled
|
|
115
|
-
?
|
|
116
|
-
:
|
|
117
|
-
: checked &&
|
|
118
|
-
?
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
355
|
+
? "Working…"
|
|
356
|
+
: "Checking…"
|
|
357
|
+
: checked && batchResult
|
|
358
|
+
? batchHasIssues
|
|
359
|
+
? "Retry"
|
|
360
|
+
: "Pushed"
|
|
361
|
+
: checked && review?.result.status === "succeeded"
|
|
362
|
+
? "Pushed"
|
|
363
|
+
: checked && !retryable
|
|
364
|
+
? "Checked"
|
|
365
|
+
: pushVerb;
|
|
366
|
+
|
|
367
|
+
function setRowPublicationTransition(
|
|
368
|
+
changeSetId: string,
|
|
369
|
+
publicationTransition: BuilderCmsPublicationTransitionIntent,
|
|
370
|
+
) {
|
|
371
|
+
setTransitionSelections((current) => {
|
|
372
|
+
const currentSelection = current[changeSetId];
|
|
373
|
+
const next = { ...current };
|
|
374
|
+
|
|
375
|
+
if (currentSelection?.publicationTransition === publicationTransition) {
|
|
376
|
+
delete next[changeSetId];
|
|
377
|
+
return next;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
next[changeSetId] = {
|
|
381
|
+
publicationTransition,
|
|
382
|
+
confirmUnpublish:
|
|
383
|
+
publicationTransition === "unpublish" ? false : undefined,
|
|
384
|
+
};
|
|
385
|
+
return next;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function setRowConfirmUnpublish(changeSetId: string, confirmed: boolean) {
|
|
390
|
+
setTransitionSelections((current) => {
|
|
391
|
+
const currentSelection = current[changeSetId];
|
|
392
|
+
if (currentSelection?.publicationTransition !== "unpublish") {
|
|
393
|
+
return current;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...current,
|
|
397
|
+
[changeSetId]: {
|
|
398
|
+
publicationTransition: "unpublish",
|
|
399
|
+
confirmUnpublish: confirmed,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
122
404
|
|
|
123
405
|
return (
|
|
124
406
|
<Dialog
|
|
@@ -140,7 +422,7 @@ export function BuilderSourceReviewDialog({
|
|
|
140
422
|
{t("database.reviewBuilderUpdate")}
|
|
141
423
|
</DialogTitle>
|
|
142
424
|
<DialogDescription className="truncate text-xs text-muted-foreground">
|
|
143
|
-
{review?.summary ??
|
|
425
|
+
{review?.summary ?? "No pending Builder changes."}
|
|
144
426
|
</DialogDescription>
|
|
145
427
|
</div>
|
|
146
428
|
<button
|
|
@@ -161,178 +443,235 @@ export function BuilderSourceReviewDialog({
|
|
|
161
443
|
{t("database.whatChanged")}
|
|
162
444
|
</div>
|
|
163
445
|
<div className="grid gap-2">
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
446
|
+
{reviewRows.map((row) => {
|
|
447
|
+
const selection = transitionSelections[row.changeSetId];
|
|
448
|
+
const effect = builderReviewEffectiveRowEffect(
|
|
449
|
+
row.effect,
|
|
450
|
+
selection,
|
|
451
|
+
);
|
|
452
|
+
const { tag, sentence } =
|
|
453
|
+
builderReviewRowEffectLabel(effect);
|
|
454
|
+
const showConflict =
|
|
455
|
+
effect !== "create_draft" &&
|
|
456
|
+
row.conflictState === "source_changed";
|
|
457
|
+
return (
|
|
458
|
+
<div
|
|
459
|
+
key={row.changeSetId}
|
|
460
|
+
className="rounded-md border border-border p-3"
|
|
461
|
+
>
|
|
462
|
+
<div className="flex min-w-0 items-start justify-between gap-3">
|
|
463
|
+
<div className="min-w-0">
|
|
464
|
+
<div className="truncate text-sm font-medium">
|
|
465
|
+
{row.title}
|
|
466
|
+
</div>
|
|
467
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
468
|
+
{sentence}
|
|
469
|
+
</div>
|
|
181
470
|
</div>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<div className="mt-3 grid gap-2">
|
|
188
|
-
{row.fieldChanges.map((field) => (
|
|
189
|
-
<div
|
|
190
|
-
key={`${row.changeSetId}-${field.localFieldKey}`}
|
|
191
|
-
className="grid gap-1 rounded border border-border/70 bg-muted/20 p-2 text-xs"
|
|
471
|
+
<span
|
|
472
|
+
className={
|
|
473
|
+
"shrink-0 text-[11px] " +
|
|
474
|
+
rowEffectTagClass(effect)
|
|
475
|
+
}
|
|
192
476
|
>
|
|
193
|
-
|
|
194
|
-
|
|
477
|
+
{tag}
|
|
478
|
+
</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="mt-3 grid gap-2">
|
|
481
|
+
{allowPublicationTransitionControls &&
|
|
482
|
+
row.effect !== "create_draft" ? (
|
|
483
|
+
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
|
484
|
+
<Button
|
|
485
|
+
type="button"
|
|
486
|
+
size="sm"
|
|
487
|
+
variant={
|
|
488
|
+
transitionSelections[row.changeSetId]
|
|
489
|
+
?.publicationTransition === "publish"
|
|
490
|
+
? "secondary"
|
|
491
|
+
: "outline"
|
|
492
|
+
}
|
|
493
|
+
className="h-7 px-2 text-xs"
|
|
494
|
+
disabled={pending}
|
|
495
|
+
aria-pressed={
|
|
496
|
+
transitionSelections[row.changeSetId]
|
|
497
|
+
?.publicationTransition === "publish"
|
|
498
|
+
}
|
|
499
|
+
onClick={() =>
|
|
500
|
+
setRowPublicationTransition(
|
|
501
|
+
row.changeSetId,
|
|
502
|
+
"publish",
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
>
|
|
506
|
+
Publish
|
|
507
|
+
</Button>
|
|
508
|
+
<Button
|
|
509
|
+
type="button"
|
|
510
|
+
size="sm"
|
|
511
|
+
variant={
|
|
512
|
+
transitionSelections[row.changeSetId]
|
|
513
|
+
?.publicationTransition === "unpublish"
|
|
514
|
+
? "destructive"
|
|
515
|
+
: "outline"
|
|
516
|
+
}
|
|
517
|
+
className="h-7 px-2 text-xs"
|
|
518
|
+
disabled={pending}
|
|
519
|
+
aria-pressed={
|
|
520
|
+
transitionSelections[row.changeSetId]
|
|
521
|
+
?.publicationTransition === "unpublish"
|
|
522
|
+
}
|
|
523
|
+
onClick={() =>
|
|
524
|
+
setRowPublicationTransition(
|
|
525
|
+
row.changeSetId,
|
|
526
|
+
"unpublish",
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
>
|
|
530
|
+
Unpublish
|
|
531
|
+
</Button>
|
|
532
|
+
{transitionSelections[row.changeSetId]
|
|
533
|
+
?.publicationTransition === "unpublish" ? (
|
|
534
|
+
<label className="flex items-center gap-1 rounded border border-destructive/30 bg-destructive/10 px-1.5 py-1 text-[11px] text-destructive">
|
|
535
|
+
<input
|
|
536
|
+
type="checkbox"
|
|
537
|
+
className="size-3 accent-destructive"
|
|
538
|
+
checked={
|
|
539
|
+
transitionSelections[row.changeSetId]
|
|
540
|
+
?.confirmUnpublish === true
|
|
541
|
+
}
|
|
542
|
+
disabled={pending}
|
|
543
|
+
onChange={(event) =>
|
|
544
|
+
setRowConfirmUnpublish(
|
|
545
|
+
row.changeSetId,
|
|
546
|
+
event.currentTarget.checked,
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
/>
|
|
550
|
+
{t("database.confirmUnpublish")}
|
|
551
|
+
</label>
|
|
552
|
+
) : null}
|
|
195
553
|
</div>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
554
|
+
) : null}
|
|
555
|
+
{row.fieldChanges.map((field) => (
|
|
556
|
+
<div
|
|
557
|
+
key={`${row.changeSetId}-${field.localFieldKey}`}
|
|
558
|
+
className="grid gap-1 rounded border border-border/70 bg-muted/20 p-2 text-xs"
|
|
559
|
+
>
|
|
560
|
+
<div className="font-medium">
|
|
561
|
+
{field.propertyName ?? field.sourceFieldKey}
|
|
204
562
|
</div>
|
|
205
|
-
<div className="
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
563
|
+
<div className="grid gap-1 text-muted-foreground sm:grid-cols-2">
|
|
564
|
+
<div className="min-w-0 break-words">
|
|
565
|
+
From: {sourceValueText(field.currentValue)}
|
|
566
|
+
</div>
|
|
567
|
+
<div className="min-w-0 break-words">
|
|
568
|
+
To: {sourceValueText(field.proposedValue)}
|
|
569
|
+
</div>
|
|
212
570
|
</div>
|
|
213
571
|
</div>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
572
|
+
))}
|
|
573
|
+
{row.bodyChange ? (
|
|
574
|
+
<div className="rounded border border-border/70 bg-muted/20 p-2 text-xs">
|
|
575
|
+
<div className="font-medium">
|
|
576
|
+
{row.bodyChange.summary}
|
|
577
|
+
</div>
|
|
578
|
+
<div className="mt-1 text-muted-foreground">
|
|
579
|
+
{t("database.builderBodyEditsNeedSaferPath")}
|
|
580
|
+
</div>
|
|
220
581
|
</div>
|
|
221
|
-
|
|
222
|
-
|
|
582
|
+
) : null}
|
|
583
|
+
{showConflict ? (
|
|
584
|
+
<div className="flex items-start gap-1.5 rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
|
|
585
|
+
<IconAlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
|
586
|
+
<span>
|
|
587
|
+
{t("database.changedInBuilderSinceSync")}
|
|
588
|
+
</span>
|
|
223
589
|
</div>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
590
|
+
) : null}
|
|
591
|
+
{effect === "unpublish" ? (
|
|
592
|
+
<div className="flex items-start gap-1.5 rounded border border-amber-300 bg-amber-50 p-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
|
|
593
|
+
<IconAlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
|
594
|
+
<span>
|
|
595
|
+
{t("database.thisUnpublishesTheLiveEntry")}
|
|
596
|
+
</span>
|
|
597
|
+
</div>
|
|
598
|
+
) : null}
|
|
599
|
+
{row.execution?.lastError ? (
|
|
600
|
+
<div className="rounded border border-destructive/30 bg-destructive/10 p-2 text-xs text-destructive">
|
|
601
|
+
{row.execution.lastError}
|
|
602
|
+
</div>
|
|
603
|
+
) : null}
|
|
604
|
+
</div>
|
|
231
605
|
</div>
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
</div>
|
|
235
|
-
</section>
|
|
236
|
-
|
|
237
|
-
<section className="grid gap-2 rounded-md border border-border p-3">
|
|
238
|
-
<div className="text-sm font-medium">
|
|
239
|
-
{t("database.whereItWillGo")}
|
|
240
|
-
</div>
|
|
241
|
-
<div className="grid gap-2 text-xs">
|
|
242
|
-
<SourceMetadataRow
|
|
243
|
-
label={t("database.source")}
|
|
244
|
-
value={review.sourceName}
|
|
245
|
-
/>
|
|
246
|
-
<SourceMetadataRow
|
|
247
|
-
label={t("database.builderModel")}
|
|
248
|
-
value={review.sourceTable}
|
|
249
|
-
/>
|
|
250
|
-
<SourceMetadataRow
|
|
251
|
-
label={t("database.pushMode")}
|
|
252
|
-
value={sourcePushModeLabel(review.pushMode, t)}
|
|
253
|
-
/>
|
|
254
|
-
<SourceMetadataRow
|
|
255
|
-
label={t("database.liveWrites")}
|
|
256
|
-
value={
|
|
257
|
-
review.liveWritesEnabled
|
|
258
|
-
? t("database.enabled")
|
|
259
|
-
: t("database.disabled")
|
|
260
|
-
}
|
|
261
|
-
/>
|
|
262
|
-
<SourceMetadataRow
|
|
263
|
-
label={t("database.readMode")}
|
|
264
|
-
value={
|
|
265
|
-
source
|
|
266
|
-
? sourceBuilderReadModeSummary(source, t)
|
|
267
|
-
: t("database.unknown")
|
|
268
|
-
}
|
|
269
|
-
/>
|
|
606
|
+
);
|
|
607
|
+
})}
|
|
270
608
|
</div>
|
|
271
609
|
</section>
|
|
272
610
|
|
|
273
|
-
<
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<div className="flex flex-wrap gap-1.5 text-xs">
|
|
278
|
-
<span className={sourceRiskClass(review.riskLevel)}>
|
|
279
|
-
{t("database.riskLabel", { level: review.riskLevel })}
|
|
280
|
-
</span>
|
|
281
|
-
{(review.riskReasons.length
|
|
282
|
-
? review.riskReasons
|
|
283
|
-
: [t("database.singleFieldDiff")]
|
|
284
|
-
).map((reason) => (
|
|
285
|
-
<span
|
|
286
|
-
key={reason}
|
|
287
|
-
className="rounded border border-border px-1.5 py-0.5 text-muted-foreground"
|
|
288
|
-
>
|
|
289
|
-
{reason}
|
|
290
|
-
</span>
|
|
291
|
-
))}
|
|
292
|
-
<span className="rounded border border-border px-1.5 py-0.5 text-muted-foreground">
|
|
293
|
-
{review.dryRunOnly
|
|
294
|
-
? t("database.checksOnly")
|
|
295
|
-
: t("database.canSendToBuilder")}
|
|
296
|
-
</span>
|
|
297
|
-
</div>
|
|
298
|
-
</section>
|
|
611
|
+
<div className="flex items-start gap-1.5 text-xs text-muted-foreground">
|
|
612
|
+
<IconCloudUpload className="mt-0.5 size-3.5 shrink-0" />
|
|
613
|
+
<span>{destinationLine}</span>
|
|
614
|
+
</div>
|
|
299
615
|
|
|
300
|
-
|
|
301
|
-
<
|
|
302
|
-
<div>
|
|
303
|
-
|
|
304
|
-
{t("database.result")}
|
|
305
|
-
</div>
|
|
306
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
307
|
-
{review.result.message}
|
|
308
|
-
</div>
|
|
616
|
+
{batchResult && batchIssueResults.length > 0 ? (
|
|
617
|
+
<section className="grid gap-1.5 rounded-md border border-amber-300 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
|
|
618
|
+
<div className="font-medium">
|
|
619
|
+
{t("database.needsAttentionBeforeFinish")}
|
|
309
620
|
</div>
|
|
310
|
-
|
|
311
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
621
|
+
{batchIssueResults.map((result) => (
|
|
622
|
+
<div key={result.changeSetId} className="break-words">
|
|
623
|
+
<span className="font-medium">
|
|
624
|
+
{rowTitleById.get(result.changeSetId) ??
|
|
625
|
+
result.changeSetId}
|
|
626
|
+
</span>
|
|
627
|
+
{" — "}
|
|
628
|
+
{result.message ?? "No details returned."}
|
|
629
|
+
</div>
|
|
630
|
+
))}
|
|
631
|
+
</section>
|
|
632
|
+
) : null}
|
|
315
633
|
</div>
|
|
316
634
|
) : (
|
|
317
635
|
<div className="rounded-md border border-border p-4 text-sm text-muted-foreground">
|
|
318
|
-
{t("database.
|
|
636
|
+
{t("database.noPendingLocalBuilderChanges")}
|
|
319
637
|
</div>
|
|
320
638
|
)}
|
|
321
639
|
</div>
|
|
322
640
|
|
|
323
641
|
<div className="flex shrink-0 items-center justify-between gap-3 border-t border-border p-3">
|
|
324
|
-
<div className="min-w-0 text-xs text-muted-foreground">
|
|
325
|
-
{
|
|
642
|
+
<div className="grid min-w-0 gap-1 text-xs text-muted-foreground">
|
|
643
|
+
{review ? (
|
|
644
|
+
<div
|
|
645
|
+
className={
|
|
646
|
+
"flex items-center gap-1.5 font-medium " +
|
|
647
|
+
resultToneClass(resultStatus.tone)
|
|
648
|
+
}
|
|
649
|
+
>
|
|
650
|
+
{checked ? (
|
|
651
|
+
resultStatus.tone === "ok" ? (
|
|
652
|
+
<IconCheck className="size-3.5 shrink-0" />
|
|
653
|
+
) : (
|
|
654
|
+
<IconAlertTriangle className="size-3.5 shrink-0" />
|
|
655
|
+
)
|
|
656
|
+
) : null}
|
|
657
|
+
<span>
|
|
658
|
+
{checked
|
|
659
|
+
? `${t(`database.${resultStatus.labelKey}`)} · ${intentSummary}`
|
|
660
|
+
: intentSummary}
|
|
661
|
+
</span>
|
|
662
|
+
</div>
|
|
663
|
+
) : null}
|
|
664
|
+
{footerHint ? <div>{footerHint}</div> : null}
|
|
326
665
|
</div>
|
|
327
666
|
<div className="flex shrink-0 items-center gap-2">
|
|
328
667
|
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
|
|
329
|
-
|
|
668
|
+
Close
|
|
330
669
|
</Button>
|
|
331
670
|
<Button
|
|
332
671
|
type="button"
|
|
333
672
|
size="sm"
|
|
334
673
|
disabled={disabled}
|
|
335
|
-
onClick={onValidate}
|
|
674
|
+
onClick={() => onValidate(transitionMap)}
|
|
336
675
|
>
|
|
337
676
|
{pending ? (
|
|
338
677
|
<Spinner className="mr-1.5 size-3.5" />
|