strapi-plugin-bulk-updater 1.0.0

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.
@@ -0,0 +1,2272 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import React, { useState, useEffect } from "react";
3
+ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
+ import { Box, Flex, Typography, Badge, Button, SingleSelect, SingleSelectOption, TextInput, Checkbox, Table, Thead, Tr, Tbody, Td, Alert, Th } from "@strapi/design-system";
5
+ import { ArrowClockwise, Play } from "@strapi/icons";
6
+ function getAvailableColumnsForCollection(col, selectedAttribute) {
7
+ const cols = [
8
+ {
9
+ key: "select",
10
+ label: "Select",
11
+ isPermanent: true,
12
+ defaultVisible: true
13
+ },
14
+ {
15
+ key: "identifier",
16
+ label: "Identifier",
17
+ sortable: !col.isSingleType,
18
+ defaultVisible: true
19
+ }
20
+ ];
21
+ const candidateAttrs = col.attributes || [];
22
+ candidateAttrs.forEach((attr) => {
23
+ if (attr === "name" || attr === "title" || attr === selectedAttribute) {
24
+ return;
25
+ }
26
+ const formattedLabel = attr.charAt(0).toUpperCase() + attr.slice(1);
27
+ cols.push({
28
+ key: `attr_${attr}`,
29
+ label: formattedLabel,
30
+ sortable: !col.isSingleType,
31
+ defaultVisible: ["department", "email", "location"].includes(attr)
32
+ });
33
+ });
34
+ cols.push({
35
+ key: "target_attr",
36
+ label: `Current "${selectedAttribute}"`,
37
+ sortable: !col.isSingleType && col.attributes.includes(selectedAttribute),
38
+ defaultVisible: true
39
+ });
40
+ cols.push({
41
+ key: "status",
42
+ label: "Status",
43
+ sortable: false,
44
+ defaultVisible: true
45
+ });
46
+ return cols;
47
+ }
48
+ function renderHeaderCell(colDef, col, currentSort, selectedAttribute, onSort) {
49
+ if (colDef.key === "select") {
50
+ return /* @__PURE__ */ jsx(Th, { style: { width: 48 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Select" }) }, "select");
51
+ }
52
+ const isCurrentSorted = colDef.key === "identifier" && (currentSort.field === "name" || currentSort.field === "title") || colDef.key === "target_attr" && currentSort.field === selectedAttribute || colDef.key.startsWith("attr_") && currentSort.field === colDef.key.replace("attr_", "");
53
+ const sortArrow = isCurrentSorted ? currentSort.order === "asc" ? "▲" : "▼" : "⇅";
54
+ const handleClick = () => {
55
+ if (!colDef.sortable) return;
56
+ if (colDef.key === "identifier") {
57
+ const field = col.attributes.includes("name") ? "name" : col.attributes.includes("title") ? "title" : "createdAt";
58
+ onSort(col.uid, field);
59
+ } else if (colDef.key === "target_attr") {
60
+ onSort(col.uid, selectedAttribute);
61
+ } else if (colDef.key.startsWith("attr_")) {
62
+ onSort(col.uid, colDef.key.replace("attr_", ""));
63
+ }
64
+ };
65
+ return /* @__PURE__ */ jsx(
66
+ Th,
67
+ {
68
+ style: { cursor: colDef.sortable ? "pointer" : "default", userSelect: "none" },
69
+ onClick: handleClick,
70
+ children: /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
71
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: colDef.label }),
72
+ colDef.sortable && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: sortArrow })
73
+ ] })
74
+ },
75
+ colDef.key
76
+ );
77
+ }
78
+ function formatDisplayValue(val) {
79
+ if (val === void 0 || val === null || val === "") {
80
+ return "(empty / not set)";
81
+ }
82
+ if (Array.isArray(val)) {
83
+ if (val.length === 0) return "(empty / unlinked)";
84
+ return val.map(
85
+ (item) => typeof item === "object" && item !== null ? item.name || item.title || item.email || item.username || item.documentId || item.id : String(item)
86
+ ).join(", ");
87
+ }
88
+ if (typeof val === "object" && val !== null) {
89
+ if (val.isRelation && val.display) return val.display;
90
+ return val.name || val.title || val.email || val.username || val.documentId || val.id || JSON.stringify(val);
91
+ }
92
+ return String(val);
93
+ }
94
+ function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAttr, isSelected, onToggleSelect) {
95
+ switch (colDef.key) {
96
+ case "select":
97
+ return /* @__PURE__ */ jsx(
98
+ Checkbox,
99
+ {
100
+ checked: isSelected,
101
+ onCheckedChange: onToggleSelect
102
+ }
103
+ );
104
+ case "identifier": {
105
+ const mainText = entry.name || entry.title || `Entry #${entry.id || entry.documentId}`;
106
+ return /* @__PURE__ */ jsxs(Box, { children: [
107
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: mainText }),
108
+ entry.email && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", display: "block", children: entry.email })
109
+ ] });
110
+ }
111
+ case "target_attr": {
112
+ const val = entry[selectedAttribute];
113
+ const displayStr = formatDisplayValue(val);
114
+ const isUnset = displayStr === "(empty / not set)" || displayStr === "(empty / unlinked)";
115
+ return /* @__PURE__ */ jsx(
116
+ Typography,
117
+ {
118
+ textColor: isUnset ? "neutral500" : "neutral800",
119
+ style: {
120
+ fontStyle: isUnset ? "italic" : "normal",
121
+ display: "inline-block",
122
+ maxWidth: 260,
123
+ overflow: "hidden",
124
+ textOverflow: "ellipsis",
125
+ whiteSpace: "nowrap"
126
+ },
127
+ children: displayStr
128
+ }
129
+ );
130
+ }
131
+ case "status": {
132
+ if (!hasAttr) {
133
+ return /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral200", textColor: "neutral600", children: "Ignored" });
134
+ }
135
+ return /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
136
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: "Supported" }),
137
+ isMatch && /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "Matches Filter" })
138
+ ] });
139
+ }
140
+ default: {
141
+ const attrName = colDef.key.replace("attr_", "");
142
+ const rawVal = entry[attrName];
143
+ const displayStr = formatDisplayValue(rawVal);
144
+ return /* @__PURE__ */ jsx(
145
+ Typography,
146
+ {
147
+ textColor: "neutral700",
148
+ style: { maxWidth: 200, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
149
+ children: displayStr === "(empty / not set)" ? "-" : displayStr
150
+ }
151
+ );
152
+ }
153
+ }
154
+ }
155
+ const BulkUpdater = () => {
156
+ const { get, post } = useFetchClient();
157
+ const { toggleNotification } = useNotification();
158
+ const getInitialTab = () => {
159
+ try {
160
+ const params = new URLSearchParams(window.location.search);
161
+ const t = params.get("tab");
162
+ if (t === "audit" || t === "editor") return t;
163
+ } catch {
164
+ }
165
+ return "editor";
166
+ };
167
+ const [activeTab, setActiveTabState] = useState(getInitialTab);
168
+ const setActiveTab = (tab) => {
169
+ setActiveTabState(tab);
170
+ try {
171
+ const url = new URL(window.location.href);
172
+ url.searchParams.set("tab", tab);
173
+ window.history.replaceState({}, "", url.toString());
174
+ } catch {
175
+ }
176
+ };
177
+ const [loading, setLoading] = useState(true);
178
+ const [updating, setUpdating] = useState(false);
179
+ const [attributes, setAttributes] = useState([]);
180
+ const [collections, setCollections] = useState([]);
181
+ const [license, setLicense] = useState(null);
182
+ const [auditEnabled, setAuditEnabled] = useState(false);
183
+ const [executionWindow, setExecutionWindow] = useState(null);
184
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
185
+ const [pendingBatches, setPendingBatches] = useState([]);
186
+ const [pendingBatchesLoading, setPendingBatchesLoading] = useState(false);
187
+ const [approvingBatchId, setApprovingBatchId] = useState(null);
188
+ const [rejectingBatchId, setRejectingBatchId] = useState(null);
189
+ const [executingNowBatchId, setExecutingNowBatchId] = useState(null);
190
+ const [makerNotesInput, setMakerNotesInput] = useState("");
191
+ const [publishOnApproval, setPublishOnApproval] = useState(true);
192
+ const [selectedAttribute, setSelectedAttribute] = useState("metaDescription");
193
+ const [attributeValue, setAttributeValue] = useState("");
194
+ const [selectedEntries, setSelectedEntries] = useState({});
195
+ const [relationsMeta, setRelationsMeta] = useState({});
196
+ const [relationOptions, setRelationOptions] = useState([]);
197
+ const [relationOptionsLoading, setRelationOptionsLoading] = useState(false);
198
+ const [selectedRelationDocId, setSelectedRelationDocId] = useState("");
199
+ const [relationMode, setRelationMode] = useState("set");
200
+ const fetchRelationOptions = async (targetUid) => {
201
+ try {
202
+ setRelationOptionsLoading(true);
203
+ const res = await get(`/bulk-updater/relation-options?targetUid=${encodeURIComponent(targetUid)}`);
204
+ if (res.data && res.data.success) {
205
+ setRelationOptions(res.data.options || []);
206
+ }
207
+ } catch {
208
+ setRelationOptions([]);
209
+ } finally {
210
+ setRelationOptionsLoading(false);
211
+ }
212
+ };
213
+ const [useRegex, setUseRegex] = useState(false);
214
+ const [regexPattern, setRegexPattern] = useState("");
215
+ const [filterOperator, setFilterOperator] = useState("none");
216
+ const [filterQuery, setFilterQuery] = useState("");
217
+ const [hideNonMatching, setHideNonMatching] = useState(false);
218
+ const [lastResults, setLastResults] = useState(null);
219
+ const [updateSummary, setUpdateSummary] = useState(null);
220
+ const [collectionPages, setCollectionPages] = useState({});
221
+ const [collectionSearch, setCollectionSearch] = useState("");
222
+ const [visibleTypeUids, setVisibleTypeUids] = useState({});
223
+ const [defaultPageSize, setDefaultPageSize] = useState(10);
224
+ const [collectionSorts, setCollectionSorts] = useState({});
225
+ const [collectionColumnVisibility, setCollectionColumnVisibility] = useState({});
226
+ const [openColumnPickerUid, setOpenColumnPickerUid] = useState(null);
227
+ const toggleColumn = (colUid, colKey, availableCols) => {
228
+ setCollectionColumnVisibility((prev) => {
229
+ const current = prev[colUid] || availableCols.filter((c) => c.defaultVisible).map((c) => c.key);
230
+ let next;
231
+ if (current.includes(colKey)) {
232
+ const remaining = current.filter((k) => k !== colKey);
233
+ if (remaining.filter((k) => k !== "select").length === 0) {
234
+ return prev;
235
+ }
236
+ next = remaining;
237
+ } else {
238
+ next = [...current, colKey];
239
+ }
240
+ return { ...prev, [colUid]: next };
241
+ });
242
+ };
243
+ const showAllColumns = (colUid, availableCols) => {
244
+ setCollectionColumnVisibility((prev) => ({
245
+ ...prev,
246
+ [colUid]: availableCols.map((c) => c.key)
247
+ }));
248
+ };
249
+ const resetDefaultColumns = (colUid, availableCols) => {
250
+ setCollectionColumnVisibility((prev) => ({
251
+ ...prev,
252
+ [colUid]: availableCols.filter((c) => c.defaultVisible).map((c) => c.key)
253
+ }));
254
+ };
255
+ const [auditHistory, setAuditHistory] = useState([]);
256
+ const [auditLoading, setAuditLoading] = useState(false);
257
+ const [rollingBackBatchId, setRollingBackBatchId] = useState(null);
258
+ const [expandedDiffBatchId, setExpandedDiffBatchId] = useState(null);
259
+ const [auditNotice, setAuditNotice] = useState(null);
260
+ const latestActiveBatch = auditHistory.find((rec) => !rec.isRolledBack);
261
+ const fetchModels = async () => {
262
+ try {
263
+ setLoading(true);
264
+ const res = await get("/bulk-updater/models");
265
+ if (res.data && res.data.success) {
266
+ setAttributes(res.data.attributes || []);
267
+ const cols = res.data.collections || [];
268
+ setCollections(cols);
269
+ setLicense(res.data.license || null);
270
+ setAuditEnabled(Boolean(res.data.auditEnabled));
271
+ if (res.data.executionWindow) {
272
+ setExecutionWindow(res.data.executionWindow);
273
+ }
274
+ const pages = {};
275
+ const initialVisibility = {};
276
+ const initialSorts = {};
277
+ cols.forEach((col) => {
278
+ const total = col.totalEntries !== void 0 ? col.totalEntries : col.entries ? col.entries.length : 0;
279
+ const initialPageSize = col.isSingleType ? 1 : 10;
280
+ pages[col.uid] = {
281
+ page: 1,
282
+ pageSize: initialPageSize,
283
+ total,
284
+ pageCount: col.isSingleType ? 1 : Math.ceil(total / initialPageSize) || 1,
285
+ entries: (col.entries || []).slice(0, initialPageSize),
286
+ loading: false
287
+ };
288
+ initialVisibility[col.uid] = true;
289
+ initialSorts[col.uid] = { field: "createdAt", order: "desc" };
290
+ });
291
+ setCollectionPages(pages);
292
+ setVisibleTypeUids(initialVisibility);
293
+ setCollectionSorts(initialSorts);
294
+ if (res.data.relationsMeta) {
295
+ setRelationsMeta(res.data.relationsMeta);
296
+ }
297
+ const activeAttr = selectedAttribute || res.data.attributes && res.data.attributes[0];
298
+ if (!selectedAttribute && activeAttr) {
299
+ setSelectedAttribute(activeAttr);
300
+ }
301
+ if (activeAttr && res.data.relationsMeta && res.data.relationsMeta[activeAttr]) {
302
+ fetchRelationOptions(res.data.relationsMeta[activeAttr].targetUid);
303
+ }
304
+ }
305
+ } catch (err) {
306
+ toggleNotification({
307
+ type: "danger",
308
+ message: err.message || "Failed to load collections data"
309
+ });
310
+ } finally {
311
+ setLoading(false);
312
+ }
313
+ };
314
+ const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder) => {
315
+ const currentPageState = collectionPages[colUid];
316
+ const size = customPageSize || currentPageState?.pageSize || defaultPageSize;
317
+ const currentSort = collectionSorts[colUid] || { field: "createdAt", order: "desc" };
318
+ const sortField = customSortField !== void 0 ? customSortField : currentSort.field;
319
+ const sortOrder = customSortOrder !== void 0 ? customSortOrder : currentSort.order;
320
+ setCollectionPages((prev) => ({
321
+ ...prev,
322
+ [colUid]: {
323
+ ...prev[colUid] || { page: 1, pageSize: size, total: 0, pageCount: 1, entries: [] },
324
+ loading: true
325
+ }
326
+ }));
327
+ try {
328
+ const params = new URLSearchParams({
329
+ page: String(targetPage),
330
+ pageSize: String(size)
331
+ });
332
+ if (selectedAttribute) params.set("attribute", selectedAttribute);
333
+ if (filterOperator && filterOperator !== "none") {
334
+ params.set("filterOperator", filterOperator);
335
+ if (filterQuery) params.set("filterQuery", filterQuery);
336
+ }
337
+ if (sortField) {
338
+ params.set("sortField", sortField);
339
+ params.set("sortOrder", sortOrder);
340
+ }
341
+ const res = await get(`/bulk-updater/collections/${colUid}/entries?${params.toString()}`);
342
+ if (res.data && res.data.success) {
343
+ setCollectionPages((prev) => ({
344
+ ...prev,
345
+ [colUid]: {
346
+ page: res.data.pagination.page,
347
+ pageSize: res.data.pagination.pageSize,
348
+ total: res.data.pagination.total,
349
+ pageCount: res.data.pagination.pageCount,
350
+ entries: res.data.entries || [],
351
+ loading: false
352
+ }
353
+ }));
354
+ }
355
+ } catch (err) {
356
+ toggleNotification({
357
+ type: "danger",
358
+ message: `Failed to load entries for ${colUid}: ${err.message}`
359
+ });
360
+ setCollectionPages((prev) => ({
361
+ ...prev,
362
+ [colUid]: {
363
+ ...prev[colUid] || {},
364
+ loading: false
365
+ }
366
+ }));
367
+ }
368
+ };
369
+ const fetchAuditHistory = async () => {
370
+ try {
371
+ setAuditLoading(true);
372
+ const res = await get("/bulk-updater/audit/history");
373
+ if (res.data) {
374
+ if (res.data.enabled) {
375
+ setAuditHistory(res.data.history || []);
376
+ setAuditNotice(null);
377
+ } else {
378
+ setAuditHistory([]);
379
+ setAuditNotice(res.data.message || "Audit trail is not configured.");
380
+ }
381
+ }
382
+ } catch (err) {
383
+ toggleNotification({
384
+ type: "danger",
385
+ message: err.message || "Failed to fetch audit history."
386
+ });
387
+ } finally {
388
+ setAuditLoading(false);
389
+ }
390
+ };
391
+ const fetchPendingBatches = async () => {
392
+ try {
393
+ setPendingBatchesLoading(true);
394
+ const res = await get("/bulk-updater/pending-batches");
395
+ if (res.data && res.data.success) {
396
+ setPendingBatches(res.data.batches || []);
397
+ if (res.data.executionWindow) {
398
+ setExecutionWindow(res.data.executionWindow);
399
+ }
400
+ }
401
+ } catch {
402
+ } finally {
403
+ setPendingBatchesLoading(false);
404
+ }
405
+ };
406
+ useEffect(() => {
407
+ fetchModels();
408
+ fetchAuditHistory();
409
+ fetchPendingBatches();
410
+ }, []);
411
+ useEffect(() => {
412
+ if (activeTab === "audit") {
413
+ fetchAuditHistory();
414
+ fetchPendingBatches();
415
+ }
416
+ }, [activeTab]);
417
+ const KEY_SEP = "@@@";
418
+ const handleAttributeChange = (newAttr) => {
419
+ if (newAttr !== selectedAttribute) {
420
+ setSelectedAttribute(newAttr);
421
+ setSelectedEntries({});
422
+ setFilterOperator("none");
423
+ setFilterQuery("");
424
+ setHideNonMatching(false);
425
+ setSelectedRelationDocId("");
426
+ setAttributeValue("");
427
+ if (relationsMeta[newAttr]) {
428
+ fetchRelationOptions(relationsMeta[newAttr].targetUid);
429
+ } else {
430
+ setRelationOptions([]);
431
+ }
432
+ collections.forEach((col) => {
433
+ loadCollectionPage(col.uid, 1);
434
+ });
435
+ }
436
+ };
437
+ const filteredCollections = collections.filter(
438
+ (c) => visibleTypeUids[c.uid] !== false && (!collectionSearch.trim() || c.displayName.toLowerCase().includes(collectionSearch.toLowerCase().trim()) || c.uid.toLowerCase().includes(collectionSearch.toLowerCase().trim()))
439
+ );
440
+ const handleGlobalPageSizeChange = (newSize) => {
441
+ const capped = Math.min(100, Math.max(1, newSize));
442
+ setDefaultPageSize(capped);
443
+ collections.forEach((col) => {
444
+ if (!col.isSingleType) {
445
+ loadCollectionPage(col.uid, 1, capped);
446
+ }
447
+ });
448
+ };
449
+ const handleCollectionPageSizeChange = (colUid, newSize) => {
450
+ const capped = Math.min(100, Math.max(1, newSize));
451
+ loadCollectionPage(colUid, 1, capped);
452
+ };
453
+ const handleCollectionSortChange = (colUid, field, order) => {
454
+ setCollectionSorts((prev) => ({
455
+ ...prev,
456
+ [colUid]: { field, order }
457
+ }));
458
+ loadCollectionPage(colUid, 1, void 0, field, order);
459
+ };
460
+ const handleHeaderSortClick = (colUid, field) => {
461
+ const current = collectionSorts[colUid] || { field: "createdAt", order: "desc" };
462
+ const nextOrder = current.field === field && current.order === "asc" ? "desc" : "asc";
463
+ handleCollectionSortChange(colUid, field, nextOrder);
464
+ };
465
+ const toggleEntry = (uid, documentId) => {
466
+ const key = `${uid}${KEY_SEP}${documentId}`;
467
+ setSelectedEntries((prev) => ({
468
+ ...prev,
469
+ [key]: !prev[key]
470
+ }));
471
+ };
472
+ const entryMatchesFilter = (entry, attr) => {
473
+ if (filterOperator === "none") return true;
474
+ const val = entry[attr];
475
+ const displayStr = formatDisplayValue(val);
476
+ const isValEmpty = val === void 0 || val === null || displayStr.trim() === "" || displayStr.trim().toLowerCase() === "(not set)" || displayStr.trim().toLowerCase() === "(empty / not set)" || displayStr.trim().toLowerCase() === "(empty / unlinked)";
477
+ switch (filterOperator) {
478
+ case "empty":
479
+ return isValEmpty;
480
+ case "not_empty":
481
+ return !isValEmpty;
482
+ case "equals":
483
+ return !isValEmpty && displayStr.trim().toLowerCase() === filterQuery.trim().toLowerCase();
484
+ case "contains":
485
+ return !isValEmpty && displayStr.toLowerCase().includes(filterQuery.trim().toLowerCase());
486
+ case "starts_with":
487
+ return !isValEmpty && displayStr.trim().toLowerCase().startsWith(filterQuery.trim().toLowerCase());
488
+ default:
489
+ return true;
490
+ }
491
+ };
492
+ const getVisibleEntries = (col) => {
493
+ const pageState = collectionPages[col.uid];
494
+ const entries = pageState ? pageState.entries : col.entries || [];
495
+ if (!hideNonMatching || filterOperator === "none") {
496
+ return entries;
497
+ }
498
+ return entries.filter((e) => entryMatchesFilter(e, selectedAttribute));
499
+ };
500
+ const matchingEntriesCount = React.useMemo(() => {
501
+ if (filterOperator === "none") return 0;
502
+ let count = 0;
503
+ filteredCollections.forEach((col) => {
504
+ if (col.attributes.includes(selectedAttribute)) {
505
+ const pageState = collectionPages[col.uid];
506
+ const entries = pageState ? pageState.entries : col.entries || [];
507
+ entries.forEach((entry) => {
508
+ if (entryMatchesFilter(entry, selectedAttribute)) {
509
+ count++;
510
+ }
511
+ });
512
+ }
513
+ });
514
+ return count;
515
+ }, [filteredCollections, collectionPages, selectedAttribute, filterOperator, filterQuery]);
516
+ const selectAllMatching = () => {
517
+ const updated = { ...selectedEntries };
518
+ filteredCollections.forEach((col) => {
519
+ if (col.attributes.includes(selectedAttribute)) {
520
+ const pageState = collectionPages[col.uid];
521
+ const entries = pageState ? pageState.entries : col.entries || [];
522
+ entries.forEach((e) => {
523
+ if (entryMatchesFilter(e, selectedAttribute)) {
524
+ updated[`${col.uid}${KEY_SEP}${e.documentId}`] = true;
525
+ }
526
+ });
527
+ }
528
+ });
529
+ setSelectedEntries(updated);
530
+ };
531
+ const deselectAllMatching = () => {
532
+ const updated = { ...selectedEntries };
533
+ filteredCollections.forEach((col) => {
534
+ if (col.attributes.includes(selectedAttribute)) {
535
+ const pageState = collectionPages[col.uid];
536
+ const entries = pageState ? pageState.entries : col.entries || [];
537
+ entries.forEach((e) => {
538
+ if (entryMatchesFilter(e, selectedAttribute)) {
539
+ delete updated[`${col.uid}${KEY_SEP}${e.documentId}`];
540
+ }
541
+ });
542
+ }
543
+ });
544
+ setSelectedEntries(updated);
545
+ };
546
+ const clearFilter = () => {
547
+ setFilterOperator("none");
548
+ setFilterQuery("");
549
+ setHideNonMatching(false);
550
+ collections.forEach((col) => {
551
+ loadCollectionPage(col.uid, 1);
552
+ });
553
+ };
554
+ const isCollectionFullySelected = (col) => {
555
+ const entries = getVisibleEntries(col);
556
+ if (!entries.length) return false;
557
+ return entries.every((e) => selectedEntries[`${col.uid}${KEY_SEP}${e.documentId}`]);
558
+ };
559
+ const isCollectionPartiallySelected = (col) => {
560
+ const entries = getVisibleEntries(col);
561
+ if (!entries.length) return false;
562
+ const selectedCount = entries.filter(
563
+ (e) => selectedEntries[`${col.uid}${KEY_SEP}${e.documentId}`]
564
+ ).length;
565
+ return selectedCount > 0 && selectedCount < entries.length;
566
+ };
567
+ const toggleCollection = (col) => {
568
+ const entries = getVisibleEntries(col);
569
+ const willSelectAll = !isCollectionFullySelected(col);
570
+ const updated = { ...selectedEntries };
571
+ entries.forEach((e) => {
572
+ const key = `${col.uid}${KEY_SEP}${e.documentId}`;
573
+ if (willSelectAll) {
574
+ updated[key] = true;
575
+ } else {
576
+ delete updated[key];
577
+ }
578
+ });
579
+ setSelectedEntries(updated);
580
+ };
581
+ const selectAll = () => {
582
+ const updated = { ...selectedEntries };
583
+ filteredCollections.forEach((col) => {
584
+ const entries = getVisibleEntries(col);
585
+ entries.forEach((e) => {
586
+ updated[`${col.uid}${KEY_SEP}${e.documentId}`] = true;
587
+ });
588
+ });
589
+ setSelectedEntries(updated);
590
+ };
591
+ const deselectAll = () => {
592
+ setSelectedEntries({});
593
+ };
594
+ const getSelectedBreakdown = () => {
595
+ const breakdown = {};
596
+ Object.keys(selectedEntries).forEach((key) => {
597
+ if (selectedEntries[key]) {
598
+ const parts = key.split(KEY_SEP);
599
+ if (parts.length === 2) {
600
+ const uid = parts[0];
601
+ const col = collections.find((c) => c.uid === uid);
602
+ const name = col?.displayName || uid;
603
+ breakdown[name] = (breakdown[name] || 0) + 1;
604
+ }
605
+ }
606
+ });
607
+ return breakdown;
608
+ };
609
+ const handleOpenConfirmModal = () => {
610
+ if (!selectedAttribute) {
611
+ toggleNotification({
612
+ type: "warning",
613
+ message: "Please select an attribute from the dropdown"
614
+ });
615
+ return;
616
+ }
617
+ if (useRegex && !license?.isPro) {
618
+ toggleNotification({
619
+ type: "warning",
620
+ message: "Regex replacement is a Pro feature. Please activate your license in Settings."
621
+ });
622
+ return;
623
+ }
624
+ const totalSelected = Object.values(selectedEntries).filter(Boolean).length;
625
+ if (totalSelected === 0) {
626
+ toggleNotification({
627
+ type: "warning",
628
+ message: "Please select at least one collection type or entry to update"
629
+ });
630
+ return;
631
+ }
632
+ setShowConfirmModal(true);
633
+ };
634
+ const executeBatchWithMode = async (executionMode, forceImmediate = false) => {
635
+ const targets = [];
636
+ Object.keys(selectedEntries).forEach((key) => {
637
+ if (selectedEntries[key]) {
638
+ const parts = key.split(KEY_SEP);
639
+ if (parts.length === 2) {
640
+ targets.push({ uid: parts[0], documentId: parts[1] });
641
+ }
642
+ }
643
+ });
644
+ try {
645
+ setUpdating(true);
646
+ setShowConfirmModal(false);
647
+ setLastResults(null);
648
+ const payload = {
649
+ attribute: selectedAttribute,
650
+ value: attributeValue,
651
+ selectedEntries: targets,
652
+ isRegex: useRegex,
653
+ regexPattern,
654
+ executionMode,
655
+ makerNotes: makerNotesInput,
656
+ forceImmediate,
657
+ publishOnApproval
658
+ };
659
+ const res = await post("/bulk-updater/batch-update", payload);
660
+ if (res.data && res.data.success) {
661
+ if (res.data.pending) {
662
+ toggleNotification({
663
+ type: "success",
664
+ message: res.data.message || "Batch submitted successfully."
665
+ });
666
+ await fetchPendingBatches();
667
+ setActiveTab("audit");
668
+ } else {
669
+ setLastResults(res.data.results || []);
670
+ setUpdateSummary({
671
+ updatedCount: res.data.updatedCount || 0,
672
+ skippedCount: res.data.skippedCount || 0,
673
+ batchId: res.data.batchId
674
+ });
675
+ toggleNotification({
676
+ type: "success",
677
+ message: `Bulk update complete! Batch ID: ${res.data.batchId}. Updated: ${res.data.updatedCount}, Skipped: ${res.data.skippedCount}`
678
+ });
679
+ await fetchModels();
680
+ }
681
+ } else {
682
+ toggleNotification({
683
+ type: "danger",
684
+ message: res.data?.error || "Batch update encountered an error"
685
+ });
686
+ }
687
+ } catch (err) {
688
+ toggleNotification({
689
+ type: "danger",
690
+ message: err.response?.data?.error || err.message || "Batch update failed"
691
+ });
692
+ } finally {
693
+ setUpdating(false);
694
+ setMakerNotesInput("");
695
+ }
696
+ };
697
+ const handleApproveBatch = async (batchId) => {
698
+ try {
699
+ setApprovingBatchId(batchId);
700
+ const res = await post(`/bulk-updater/batches/${batchId}/approve`, { executeImmediately: true });
701
+ if (res.data && res.data.success) {
702
+ toggleNotification({
703
+ type: "success",
704
+ message: res.data.message || `Batch ${batchId} approved and executed successfully!`
705
+ });
706
+ await fetchPendingBatches();
707
+ await fetchAuditHistory();
708
+ await fetchModels();
709
+ } else {
710
+ toggleNotification({
711
+ type: "danger",
712
+ message: res.data?.error || "Failed to approve batch"
713
+ });
714
+ }
715
+ } catch (err) {
716
+ toggleNotification({
717
+ type: "danger",
718
+ message: err.response?.data?.error || err.message || "Failed to approve batch"
719
+ });
720
+ } finally {
721
+ setApprovingBatchId(null);
722
+ }
723
+ };
724
+ const handleRejectBatch = async (batchId) => {
725
+ const reason = window.prompt("Enter reason for rejection:", "Does not meet release criteria") || "Rejected by Checker";
726
+ try {
727
+ setRejectingBatchId(batchId);
728
+ const res = await post(`/bulk-updater/batches/${batchId}/reject`, { reason });
729
+ if (res.data && res.data.success) {
730
+ toggleNotification({
731
+ type: "info",
732
+ message: `Batch ${batchId} rejected: ${reason}`
733
+ });
734
+ await fetchPendingBatches();
735
+ } else {
736
+ toggleNotification({
737
+ type: "danger",
738
+ message: res.data?.error || "Failed to reject batch"
739
+ });
740
+ }
741
+ } catch (err) {
742
+ toggleNotification({
743
+ type: "danger",
744
+ message: err.response?.data?.error || err.message || "Failed to reject batch"
745
+ });
746
+ } finally {
747
+ setRejectingBatchId(null);
748
+ }
749
+ };
750
+ const handleExecuteNow = async (batchId) => {
751
+ try {
752
+ setExecutingNowBatchId(batchId);
753
+ const res = await post(`/bulk-updater/batches/${batchId}/execute-now`);
754
+ if (res.data && res.data.success) {
755
+ toggleNotification({
756
+ type: "success",
757
+ message: `Batch ${batchId} executed immediately!`
758
+ });
759
+ await fetchPendingBatches();
760
+ await fetchAuditHistory();
761
+ await fetchModels();
762
+ } else {
763
+ toggleNotification({
764
+ type: "danger",
765
+ message: res.data?.error || "Failed to execute batch"
766
+ });
767
+ }
768
+ } catch (err) {
769
+ toggleNotification({
770
+ type: "danger",
771
+ message: err.response?.data?.error || err.message || "Failed to execute batch"
772
+ });
773
+ } finally {
774
+ setExecutingNowBatchId(null);
775
+ }
776
+ };
777
+ const handleRollback = async (batchId) => {
778
+ if (!window.confirm(`Are you sure you want to roll back all changes made in batch "${batchId}"?`)) {
779
+ return;
780
+ }
781
+ try {
782
+ setRollingBackBatchId(batchId);
783
+ const res = await post(`/bulk-updater/audit/${batchId}/rollback`);
784
+ if (res.data && res.data.success) {
785
+ toggleNotification({
786
+ type: "success",
787
+ message: `Batch "${batchId}" rolled back successfully! Reverted ${res.data.revertedCount} entries.`
788
+ });
789
+ await fetchAuditHistory();
790
+ await fetchModels();
791
+ } else {
792
+ toggleNotification({
793
+ type: "danger",
794
+ message: res.data?.error || "Rollback failed."
795
+ });
796
+ }
797
+ } catch (err) {
798
+ toggleNotification({
799
+ type: "danger",
800
+ message: err.response?.data?.error || err.message || "Rollback encountered an error."
801
+ });
802
+ } finally {
803
+ setRollingBackBatchId(null);
804
+ }
805
+ };
806
+ const totalSelectedCount = Object.values(selectedEntries).filter(Boolean).length;
807
+ const rootRef = React.useRef(null);
808
+ useEffect(() => {
809
+ if (rootRef.current && rootRef.current.parentElement) {
810
+ rootRef.current.parentElement.style.minWidth = "0";
811
+ rootRef.current.parentElement.style.maxWidth = "100%";
812
+ }
813
+ }, []);
814
+ return /* @__PURE__ */ jsxs(
815
+ Box,
816
+ {
817
+ ref: rootRef,
818
+ className: "bulk-updater-root",
819
+ padding: 8,
820
+ background: "neutral100",
821
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
822
+ children: [
823
+ /* @__PURE__ */ jsx("style", { children: `
824
+ /* Prevent page-level horizontal overflow under any circumstance */
825
+ html, body {
826
+ overflow-x: hidden !important;
827
+ max-width: 100vw !important;
828
+ }
829
+ div:has(> .bulk-updater-root) {
830
+ min-width: 0 !important;
831
+ max-width: 100% !important;
832
+ width: 100% !important;
833
+ flex: 1 1 0% !important;
834
+ overflow-x: clip !important;
835
+ }
836
+ .bulk-updater-root {
837
+ min-width: 0 !important;
838
+ max-width: 100% !important;
839
+ width: 100% !important;
840
+ box-sizing: border-box !important;
841
+ overflow-x: clip !important;
842
+ }
843
+ /* All tables stretch to take the full available container space */
844
+ .bulk-updater-root table {
845
+ width: 100% !important;
846
+ min-width: 0 !important;
847
+ }
848
+ .bulk-updater-root div:has(> table) {
849
+ width: 100% !important;
850
+ max-width: 100% !important;
851
+ min-width: 0 !important;
852
+ }
853
+ /* Table scroll container keeps horizontal scroll strictly at the table level */
854
+ .bulk-updater-table-container {
855
+ width: 100% !important;
856
+ max-width: 100% !important;
857
+ min-width: 0 !important;
858
+ overflow-x: auto !important;
859
+ -webkit-overflow-scrolling: touch;
860
+ }
861
+ .bulk-updater-table-container::-webkit-scrollbar {
862
+ height: 8px;
863
+ }
864
+ .bulk-updater-table-container::-webkit-scrollbar-track {
865
+ background: rgba(120, 120, 150, 0.1);
866
+ border-radius: 4px;
867
+ }
868
+ .bulk-updater-table-container::-webkit-scrollbar-thumb {
869
+ background: rgba(120, 120, 150, 0.4);
870
+ border-radius: 4px;
871
+ }
872
+ .bulk-updater-table-container::-webkit-scrollbar-thumb:hover {
873
+ background: rgba(120, 120, 150, 0.7);
874
+ }
875
+ ` }),
876
+ /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "flex-start", gap: 2, marginBottom: 6, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", width: "100%", children: [
877
+ /* @__PURE__ */ jsxs(Box, { children: [
878
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
879
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", fontWeight: "bold", children: "Bulk Attribute Updater" }),
880
+ /* @__PURE__ */ jsx(
881
+ Badge,
882
+ {
883
+ backgroundColor: license?.isPro ? "success100" : "neutral200",
884
+ textColor: license?.isPro ? "success700" : "neutral700",
885
+ children: license?.isPro ? `${license.tier.toUpperCase()} EDITION` : "COMMUNITY EDITION"
886
+ }
887
+ ),
888
+ auditEnabled && /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "AUDIT & ROLLBACK ACTIVE" })
889
+ ] }),
890
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: "Cross-collection batch attribute management with declarative audit trail and one-click rollback." })
891
+ ] }),
892
+ /* @__PURE__ */ jsx(Flex, { gap: 2, children: /* @__PURE__ */ jsx(
893
+ Button,
894
+ {
895
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
896
+ variant: "tertiary",
897
+ onClick: activeTab === "editor" ? fetchModels : fetchAuditHistory,
898
+ loading: loading || auditLoading,
899
+ children: "Refresh"
900
+ }
901
+ ) })
902
+ ] }) }),
903
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, marginBottom: 4, wrap: "wrap", justifyContent: "space-between", alignItems: "center", children: [
904
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
905
+ /* @__PURE__ */ jsx(
906
+ Button,
907
+ {
908
+ variant: activeTab === "editor" ? "default" : "tertiary",
909
+ onClick: () => setActiveTab("editor"),
910
+ size: "M",
911
+ children: "Bulk Updater Workspace"
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsxs(
915
+ Button,
916
+ {
917
+ variant: activeTab === "audit" ? "default" : "tertiary",
918
+ onClick: () => setActiveTab("audit"),
919
+ size: "M",
920
+ children: [
921
+ "Audit Trail & Approvals",
922
+ pendingBatches.filter((b) => b.status === "pending_approval" || b.status === "queued_offpeak").length > 0 ? /* @__PURE__ */ jsxs(Badge, { backgroundColor: "warning100", textColor: "warning700", style: { marginLeft: 8 }, children: [
923
+ pendingBatches.filter((b) => b.status === "pending_approval" || b.status === "queued_offpeak").length,
924
+ " PENDING"
925
+ ] }) : auditHistory.length > 0 ? ` (${auditHistory.length})` : ""
926
+ ]
927
+ }
928
+ )
929
+ ] }),
930
+ executionWindow && /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsx(
931
+ Badge,
932
+ {
933
+ backgroundColor: executionWindow.isBusinessHours ? "warning100" : "success100",
934
+ textColor: executionWindow.isBusinessHours ? "warning700" : "success700",
935
+ children: executionWindow.isBusinessHours ? `PEAK HOURS (${executionWindow.businessHours?.start} - ${executionWindow.businessHours?.end} ${executionWindow.timeZone || ""}) • OFF-PEAK QUEUE ACTIVE` : `OFF-PEAK WINDOW ACTIVE (${executionWindow.timeZone || ""}) • DIRECT EXECUTION PERMITTED`
936
+ }
937
+ ) })
938
+ ] }),
939
+ activeTab === "editor" && latestActiveBatch && !updateSummary && /* @__PURE__ */ jsx(
940
+ Box,
941
+ {
942
+ padding: 3,
943
+ paddingLeft: 4,
944
+ paddingRight: 4,
945
+ background: "neutral0",
946
+ shadow: "filterShadow",
947
+ hasRadius: true,
948
+ marginBottom: 4,
949
+ style: { borderLeft: "4px solid #4945ff" },
950
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, children: [
951
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
952
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "primary100", textColor: "primary700", children: "ACTIVE BATCH" }),
953
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral700", children: [
954
+ /* @__PURE__ */ jsx("code", { children: latestActiveBatch.batchId }),
955
+ " (",
956
+ latestActiveBatch.recordCount,
957
+ " entries updated by ",
958
+ latestActiveBatch.userEmail,
959
+ ")"
960
+ ] })
961
+ ] }),
962
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
963
+ /* @__PURE__ */ jsx(
964
+ Button,
965
+ {
966
+ variant: "danger-light",
967
+ size: "S",
968
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
969
+ onClick: () => handleRollback(latestActiveBatch.batchId),
970
+ loading: rollingBackBatchId === latestActiveBatch.batchId,
971
+ children: "Rollback This Batch"
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx(
975
+ Button,
976
+ {
977
+ variant: "tertiary",
978
+ size: "S",
979
+ onClick: () => setActiveTab("audit"),
980
+ children: "View Rollback History →"
981
+ }
982
+ )
983
+ ] })
984
+ ] })
985
+ }
986
+ ),
987
+ activeTab === "editor" && /* @__PURE__ */ jsxs(Fragment, { children: [
988
+ /* @__PURE__ */ jsxs(
989
+ Box,
990
+ {
991
+ padding: 6,
992
+ background: "neutral0",
993
+ shadow: "filterShadow",
994
+ hasRadius: true,
995
+ marginBottom: 6,
996
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
997
+ children: [
998
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", marginBottom: 4, fontWeight: "semiBold", children: "1. Configure Attribute Update" }),
999
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-end", wrap: "wrap", children: [
1000
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 260 }, children: /* @__PURE__ */ jsx(
1001
+ SingleSelect,
1002
+ {
1003
+ label: "Attribute to Update",
1004
+ placeholder: "Select an attribute...",
1005
+ value: selectedAttribute,
1006
+ onChange: (val) => handleAttributeChange(String(val)),
1007
+ children: attributes.map((attr) => {
1008
+ const rel = relationsMeta[attr];
1009
+ return /* @__PURE__ */ jsxs(SingleSelectOption, { value: attr, children: [
1010
+ attr,
1011
+ " ",
1012
+ rel ? `(relation ➔ ${rel.targetDisplayName})` : ""
1013
+ ] }, attr);
1014
+ })
1015
+ }
1016
+ ) }),
1017
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 340 }, children: relationsMeta[selectedAttribute] ? /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, style: { width: "100%" }, children: [
1018
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
1019
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral700", children: [
1020
+ "Target ",
1021
+ relationsMeta[selectedAttribute].targetDisplayName,
1022
+ " Relation:"
1023
+ ] }),
1024
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: [
1025
+ relationsMeta[selectedAttribute].relationType,
1026
+ " → ",
1027
+ relationsMeta[selectedAttribute].targetDisplayName
1028
+ ] }),
1029
+ relationsMeta[selectedAttribute].isMulti && /* @__PURE__ */ jsx(Badge, { backgroundColor: "primary100", textColor: "primary700", children: "Multi-Relation" })
1030
+ ] }),
1031
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { width: "100%" }, children: [
1032
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxs(
1033
+ SingleSelect,
1034
+ {
1035
+ label: `Choose ${relationsMeta[selectedAttribute].targetDisplayName}`,
1036
+ placeholder: relationOptionsLoading ? "Loading entities..." : `Pick a ${relationsMeta[selectedAttribute].targetDisplayName}...`,
1037
+ value: selectedRelationDocId,
1038
+ onChange: (val) => {
1039
+ const docId = String(val);
1040
+ setSelectedRelationDocId(docId);
1041
+ if (docId === "__clear__") {
1042
+ setAttributeValue(JSON.stringify({ mode: "clear", label: "(clear relation)" }));
1043
+ } else {
1044
+ const opt = relationOptions.find((o) => o.documentId === docId);
1045
+ setAttributeValue(JSON.stringify({ mode: relationMode, documentIds: [docId], label: opt?.label || docId }));
1046
+ }
1047
+ },
1048
+ children: [
1049
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "__clear__", children: "-- Clear / Unlink Relation --" }),
1050
+ relationOptions.map((opt) => /* @__PURE__ */ jsx(SingleSelectOption, { value: opt.documentId, children: opt.label }, opt.documentId))
1051
+ ]
1052
+ }
1053
+ ) }),
1054
+ relationsMeta[selectedAttribute].isMulti && /* @__PURE__ */ jsx(Box, { style: { width: 170 }, children: /* @__PURE__ */ jsxs(
1055
+ SingleSelect,
1056
+ {
1057
+ label: "Action Mode",
1058
+ value: relationMode,
1059
+ onChange: (val) => {
1060
+ const m = String(val);
1061
+ setRelationMode(m);
1062
+ if (selectedRelationDocId) {
1063
+ if (selectedRelationDocId === "__clear__" || m === "clear") {
1064
+ setAttributeValue(JSON.stringify({ mode: "clear", label: "(clear relation)" }));
1065
+ } else {
1066
+ const opt = relationOptions.find((o) => o.documentId === selectedRelationDocId);
1067
+ setAttributeValue(JSON.stringify({ mode: m, documentIds: [selectedRelationDocId], label: opt?.label || selectedRelationDocId }));
1068
+ }
1069
+ }
1070
+ },
1071
+ children: [
1072
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "set", children: "Set / Overwrite" }),
1073
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "connect", children: "Add / Connect" }),
1074
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "disconnect", children: "Remove / Disconnect" }),
1075
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "clear", children: "Clear All" })
1076
+ ]
1077
+ }
1078
+ ) })
1079
+ ] })
1080
+ ] }) : /* @__PURE__ */ jsx(
1081
+ TextInput,
1082
+ {
1083
+ label: useRegex ? "Replacement Value" : "New Attribute Value",
1084
+ name: "attributeValue",
1085
+ placeholder: useRegex ? "Replace matched pattern with..." : "Enter the value to apply...",
1086
+ value: attributeValue,
1087
+ onChange: (e) => setAttributeValue(e.target.value)
1088
+ }
1089
+ ) }),
1090
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(
1091
+ Button,
1092
+ {
1093
+ startIcon: /* @__PURE__ */ jsx(Play, {}),
1094
+ variant: "default",
1095
+ size: "L",
1096
+ onClick: handleOpenConfirmModal,
1097
+ loading: updating,
1098
+ disabled: loading || totalSelectedCount === 0,
1099
+ children: [
1100
+ "Update Selected (",
1101
+ totalSelectedCount,
1102
+ ")"
1103
+ ]
1104
+ }
1105
+ ) })
1106
+ ] }),
1107
+ /* @__PURE__ */ jsxs(Box, { marginTop: 4, padding: 4, background: "neutral100", hasRadius: true, children: [
1108
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1109
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1110
+ /* @__PURE__ */ jsx(
1111
+ Checkbox,
1112
+ {
1113
+ checked: useRegex,
1114
+ onCheckedChange: (checked) => setUseRegex(Boolean(checked)),
1115
+ children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: "Enable Regex Find & Replace" })
1116
+ }
1117
+ ),
1118
+ /* @__PURE__ */ jsx(
1119
+ Badge,
1120
+ {
1121
+ backgroundColor: license?.isPro ? "success100" : "warning100",
1122
+ textColor: license?.isPro ? "success700" : "warning700",
1123
+ children: license?.isPro ? "PRO UNLOCKED" : "PRO FEATURE"
1124
+ }
1125
+ )
1126
+ ] }),
1127
+ useRegex && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsx(
1128
+ TextInput,
1129
+ {
1130
+ placeholder: "Regex pattern (e.g. \\b(OldName)\\b)...",
1131
+ value: regexPattern,
1132
+ onChange: (e) => setRegexPattern(e.target.value),
1133
+ disabled: !license?.isPro
1134
+ }
1135
+ ) })
1136
+ ] }),
1137
+ !license?.isPro && useRegex && /* @__PURE__ */ jsx(Box, { marginTop: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning700", children: "Regex replacement is locked to Pro license holders. Visit Settings > Bulk Updater to activate your key." }) })
1138
+ ] }),
1139
+ selectedAttribute && /* @__PURE__ */ jsx(Box, { marginTop: 4, padding: 3, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
1140
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "Attribute Info:" }),
1141
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", children: [
1142
+ "Targeting attribute ",
1143
+ /* @__PURE__ */ jsx("strong", { children: selectedAttribute }),
1144
+ "."
1145
+ ] }),
1146
+ collections.map((c) => {
1147
+ const hasAttr = c.attributes.includes(selectedAttribute);
1148
+ return /* @__PURE__ */ jsxs(
1149
+ Badge,
1150
+ {
1151
+ backgroundColor: hasAttr ? "success100" : "neutral200",
1152
+ textColor: hasAttr ? "success700" : "neutral600",
1153
+ children: [
1154
+ c.displayName,
1155
+ ": ",
1156
+ hasAttr ? "Attribute exists" : "Not in schema"
1157
+ ]
1158
+ },
1159
+ c.uid
1160
+ );
1161
+ })
1162
+ ] }) })
1163
+ ]
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsxs(
1167
+ Box,
1168
+ {
1169
+ padding: 6,
1170
+ background: "neutral0",
1171
+ shadow: "filterShadow",
1172
+ hasRadius: true,
1173
+ marginBottom: 6,
1174
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
1175
+ children: [
1176
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, wrap: "wrap", gap: 3, children: [
1177
+ /* @__PURE__ */ jsxs(Box, { children: [
1178
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", fontWeight: "semiBold", children: "2. Select Targets" }),
1179
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Pick entire collection types or individual entries to apply the update to." })
1180
+ ] }),
1181
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1182
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "S", onClick: selectAll, children: "Select All" }),
1183
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: deselectAll, children: "Deselect All" })
1184
+ ] })
1185
+ ] }),
1186
+ /* @__PURE__ */ jsx(
1187
+ Box,
1188
+ {
1189
+ padding: 4,
1190
+ background: "neutral100",
1191
+ hasRadius: true,
1192
+ marginBottom: 5,
1193
+ style: { width: "100%", maxWidth: "100%", boxSizing: "border-box" },
1194
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
1195
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, children: [
1196
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1197
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: "Find Targets by Current Value:" }),
1198
+ filterOperator !== "none" && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
1199
+ matchingEntriesCount,
1200
+ " matching ",
1201
+ matchingEntriesCount === 1 ? "entry" : "entries"
1202
+ ] })
1203
+ ] }),
1204
+ filterOperator !== "none" && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Filter" })
1205
+ ] }),
1206
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-end", wrap: "wrap", children: [
1207
+ /* @__PURE__ */ jsx(Box, { style: { width: 240, minWidth: 200 }, children: /* @__PURE__ */ jsxs(
1208
+ SingleSelect,
1209
+ {
1210
+ label: `Filter condition for "${selectedAttribute}"`,
1211
+ value: filterOperator,
1212
+ onChange: (val) => setFilterOperator(String(val)),
1213
+ children: [
1214
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "none", children: "Show All (No Filter)" }),
1215
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "empty", children: "Is Empty or Null" }),
1216
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "not_empty", children: "Is Not Empty" }),
1217
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "equals", children: "Equals Exact Text" }),
1218
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "contains", children: "Contains Substring" }),
1219
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "starts_with", children: "Starts With" })
1220
+ ]
1221
+ }
1222
+ ) }),
1223
+ (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 220 }, children: /* @__PURE__ */ jsx(
1224
+ TextInput,
1225
+ {
1226
+ label: "Value to Search",
1227
+ placeholder: "Type value to match...",
1228
+ value: filterQuery,
1229
+ onChange: (e) => setFilterQuery(e.target.value)
1230
+ }
1231
+ ) }),
1232
+ filterOperator !== "none" && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1233
+ /* @__PURE__ */ jsxs(
1234
+ Button,
1235
+ {
1236
+ variant: "default",
1237
+ size: "S",
1238
+ onClick: selectAllMatching,
1239
+ disabled: matchingEntriesCount === 0,
1240
+ children: [
1241
+ "Select Matching (",
1242
+ matchingEntriesCount,
1243
+ ")"
1244
+ ]
1245
+ }
1246
+ ),
1247
+ /* @__PURE__ */ jsx(
1248
+ Button,
1249
+ {
1250
+ variant: "tertiary",
1251
+ size: "S",
1252
+ onClick: deselectAllMatching,
1253
+ disabled: matchingEntriesCount === 0,
1254
+ children: "Deselect Matching"
1255
+ }
1256
+ )
1257
+ ] })
1258
+ ] }),
1259
+ filterOperator !== "none" && /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsx(
1260
+ Checkbox,
1261
+ {
1262
+ checked: hideNonMatching,
1263
+ onCheckedChange: (checked) => setHideNonMatching(Boolean(checked)),
1264
+ children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral700", children: "Only display matching entries in tables (hide non-matching)" })
1265
+ }
1266
+ ) })
1267
+ ] })
1268
+ }
1269
+ ),
1270
+ collections.length > 0 && /* @__PURE__ */ jsxs(
1271
+ Box,
1272
+ {
1273
+ padding: 4,
1274
+ background: "neutral100",
1275
+ hasRadius: true,
1276
+ marginBottom: 4,
1277
+ style: { width: "100%", maxWidth: "100%", boxSizing: "border-box" },
1278
+ children: [
1279
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, marginBottom: 3, children: [
1280
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1281
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: "Filter Visible Content Types:" }),
1282
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
1283
+ filteredCollections.length,
1284
+ " of ",
1285
+ collections.length,
1286
+ " visible"
1287
+ ] })
1288
+ ] }),
1289
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1290
+ /* @__PURE__ */ jsx(
1291
+ Button,
1292
+ {
1293
+ variant: "tertiary",
1294
+ size: "S",
1295
+ onClick: () => {
1296
+ const allVis = {};
1297
+ collections.forEach((c) => allVis[c.uid] = true);
1298
+ setVisibleTypeUids(allVis);
1299
+ },
1300
+ children: "Show All Types"
1301
+ }
1302
+ ),
1303
+ /* @__PURE__ */ jsx(
1304
+ Button,
1305
+ {
1306
+ variant: "tertiary",
1307
+ size: "S",
1308
+ onClick: () => {
1309
+ const allVis = {};
1310
+ collections.forEach((c) => allVis[c.uid] = false);
1311
+ setVisibleTypeUids(allVis);
1312
+ },
1313
+ children: "Hide All Types"
1314
+ }
1315
+ )
1316
+ ] })
1317
+ ] }),
1318
+ /* @__PURE__ */ jsx(Flex, { wrap: "wrap", gap: 2, alignItems: "center", children: collections.map((col) => {
1319
+ const isVisible = visibleTypeUids[col.uid] !== false;
1320
+ return /* @__PURE__ */ jsx(
1321
+ Box,
1322
+ {
1323
+ padding: 2,
1324
+ paddingLeft: 3,
1325
+ paddingRight: 3,
1326
+ background: isVisible ? "neutral0" : "neutral200",
1327
+ hasRadius: true,
1328
+ borderStyle: "solid",
1329
+ borderWidth: "1px",
1330
+ borderColor: isVisible ? "primary200" : "neutral300",
1331
+ children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1332
+ /* @__PURE__ */ jsx(
1333
+ Checkbox,
1334
+ {
1335
+ checked: isVisible,
1336
+ onCheckedChange: (checked) => setVisibleTypeUids((prev) => ({ ...prev, [col.uid]: Boolean(checked) })),
1337
+ children: /* @__PURE__ */ jsx(Typography, { fontWeight: isVisible ? "semiBold" : "regular", children: col.displayName })
1338
+ }
1339
+ ),
1340
+ col.isSingleType ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", size: "S", children: "Single Type" }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", size: "S", children: "Collection" })
1341
+ ] })
1342
+ },
1343
+ col.uid
1344
+ );
1345
+ }) })
1346
+ ]
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, wrap: "wrap", gap: 3, children: [
1350
+ collections.length > 1 && /* @__PURE__ */ jsx(Box, { style: { flex: 1, maxWidth: 360, minWidth: 220 }, children: /* @__PURE__ */ jsx(
1351
+ TextInput,
1352
+ {
1353
+ label: "Search Content Types",
1354
+ placeholder: "Search by name or UID...",
1355
+ value: collectionSearch,
1356
+ onChange: (e) => setCollectionSearch(e.target.value)
1357
+ }
1358
+ ) }),
1359
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { marginLeft: "auto" }, children: [
1360
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Default page size (max 100):" }),
1361
+ /* @__PURE__ */ jsx(Box, { style: { width: 120 }, children: /* @__PURE__ */ jsxs(
1362
+ SingleSelect,
1363
+ {
1364
+ size: "S",
1365
+ value: String(defaultPageSize),
1366
+ onChange: (val) => handleGlobalPageSizeChange(Number(val)),
1367
+ children: [
1368
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "5", children: "5 / page" }),
1369
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "10", children: "10 / page" }),
1370
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "25", children: "25 / page" }),
1371
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "50", children: "50 / page" }),
1372
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100 / page" })
1373
+ ]
1374
+ }
1375
+ ) })
1376
+ ] })
1377
+ ] }),
1378
+ filteredCollections.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: 6, background: "neutral100", hasRadius: true, style: { textAlign: "center" }, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "No content types match the visibility or search filters." }) }) : filteredCollections.map((col) => {
1379
+ const pageState = collectionPages[col.uid];
1380
+ const entries = pageState ? pageState.entries : col.entries || [];
1381
+ const totalEntries = pageState ? pageState.total : col.totalEntries !== void 0 ? col.totalEntries : entries.length;
1382
+ const page = pageState ? pageState.page : 1;
1383
+ const pageCount = pageState ? pageState.pageCount : 1;
1384
+ const pageSize = pageState ? pageState.pageSize : defaultPageSize;
1385
+ const hasAttr = col.attributes.includes(selectedAttribute);
1386
+ const visibleEntries = getVisibleEntries(col);
1387
+ const matchingColCount = entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length;
1388
+ const isFullySelected = isCollectionFullySelected(col);
1389
+ const isPartiallySelected = isCollectionPartiallySelected(col);
1390
+ const currentSort = collectionSorts[col.uid] || { field: "createdAt", order: "desc" };
1391
+ const availableColumns = getAvailableColumnsForCollection(col, selectedAttribute);
1392
+ const savedColKeys = collectionColumnVisibility[col.uid];
1393
+ const activeColumns = savedColKeys ? availableColumns.filter((c) => c.isPermanent || savedColKeys.includes(c.key)) : availableColumns.filter((c) => c.defaultVisible);
1394
+ return /* @__PURE__ */ jsxs(
1395
+ Box,
1396
+ {
1397
+ marginBottom: 5,
1398
+ padding: 4,
1399
+ hasRadius: true,
1400
+ borderStyle: "solid",
1401
+ borderWidth: "1px",
1402
+ borderColor: "neutral200",
1403
+ background: "neutral0",
1404
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
1405
+ children: [
1406
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, wrap: "wrap", gap: 2, children: [
1407
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
1408
+ /* @__PURE__ */ jsx(
1409
+ Checkbox,
1410
+ {
1411
+ checked: isFullySelected,
1412
+ indeterminate: isPartiallySelected ? true : void 0,
1413
+ onCheckedChange: () => toggleCollection(col),
1414
+ children: /* @__PURE__ */ jsxs(Typography, { variant: "beta", fontWeight: "bold", children: [
1415
+ col.displayName,
1416
+ " ",
1417
+ col.isSingleType ? "(Single Type)" : "Collection"
1418
+ ] })
1419
+ }
1420
+ ),
1421
+ col.isSingleType ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "Single Type" }) : /* @__PURE__ */ jsx(
1422
+ Badge,
1423
+ {
1424
+ backgroundColor: hasAttr ? "success100" : "warning100",
1425
+ textColor: hasAttr ? "success700" : "warning700",
1426
+ children: hasAttr ? `Schema has "${selectedAttribute}"` : `Schema lacks "${selectedAttribute}" (will skip)`
1427
+ }
1428
+ )
1429
+ ] }),
1430
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
1431
+ !col.isSingleType && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1432
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Sort:" }),
1433
+ /* @__PURE__ */ jsx(Box, { style: { width: 190 }, children: /* @__PURE__ */ jsxs(
1434
+ SingleSelect,
1435
+ {
1436
+ size: "S",
1437
+ value: `${currentSort.field}:${currentSort.order}`,
1438
+ onChange: (val) => {
1439
+ const [f, o] = String(val).split(":");
1440
+ handleCollectionSortChange(col.uid, f, o);
1441
+ },
1442
+ children: [
1443
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "createdAt:desc", children: "Created: Newest" }),
1444
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "createdAt:asc", children: "Created: Oldest" }),
1445
+ hasAttr && /* @__PURE__ */ jsxs(Fragment, { children: [
1446
+ /* @__PURE__ */ jsxs(SingleSelectOption, { value: `${selectedAttribute}:asc`, children: [
1447
+ selectedAttribute,
1448
+ " (A → Z)"
1449
+ ] }),
1450
+ /* @__PURE__ */ jsxs(SingleSelectOption, { value: `${selectedAttribute}:desc`, children: [
1451
+ selectedAttribute,
1452
+ " (Z → A)"
1453
+ ] })
1454
+ ] }),
1455
+ col.attributes.includes("name") && /* @__PURE__ */ jsxs(Fragment, { children: [
1456
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "name:asc", children: "Name (A → Z)" }),
1457
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "name:desc", children: "Name (Z → A)" })
1458
+ ] }),
1459
+ col.attributes.includes("title") && /* @__PURE__ */ jsxs(Fragment, { children: [
1460
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "title:asc", children: "Title (A → Z)" }),
1461
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "title:desc", children: "Title (Z → A)" })
1462
+ ] })
1463
+ ]
1464
+ }
1465
+ ) })
1466
+ ] }),
1467
+ /* @__PURE__ */ jsxs(
1468
+ Button,
1469
+ {
1470
+ variant: openColumnPickerUid === col.uid ? "secondary" : "tertiary",
1471
+ size: "S",
1472
+ onClick: () => setOpenColumnPickerUid(openColumnPickerUid === col.uid ? null : col.uid),
1473
+ children: [
1474
+ "Columns (",
1475
+ activeColumns.filter((c) => !c.isPermanent).length,
1476
+ "/",
1477
+ availableColumns.filter((c) => !c.isPermanent).length,
1478
+ ")"
1479
+ ]
1480
+ }
1481
+ ),
1482
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: col.isSingleType ? "1 single entry" : filterOperator !== "none" ? `${matchingColCount} matching of ${totalEntries} entries` : `${totalEntries} entries` })
1483
+ ] })
1484
+ ] }),
1485
+ openColumnPickerUid === col.uid && /* @__PURE__ */ jsx(
1486
+ Box,
1487
+ {
1488
+ padding: 3,
1489
+ marginBottom: 3,
1490
+ background: "neutral100",
1491
+ hasRadius: true,
1492
+ borderColor: "neutral200",
1493
+ style: { border: "1px solid #dcdce4" },
1494
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1495
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
1496
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral700", children: [
1497
+ "Columns for ",
1498
+ col.displayName,
1499
+ ":"
1500
+ ] }),
1501
+ availableColumns.filter((c) => !c.isPermanent).map((c) => {
1502
+ const isChecked = activeColumns.some((ac) => ac.key === c.key);
1503
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1504
+ Checkbox,
1505
+ {
1506
+ checked: isChecked,
1507
+ onCheckedChange: () => toggleColumn(col.uid, c.key, availableColumns),
1508
+ children: /* @__PURE__ */ jsx(Typography, { variant: "pi", children: c.label })
1509
+ }
1510
+ ) }, c.key);
1511
+ })
1512
+ ] }),
1513
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1514
+ /* @__PURE__ */ jsx(
1515
+ Button,
1516
+ {
1517
+ variant: "tertiary",
1518
+ size: "S",
1519
+ onClick: () => showAllColumns(col.uid, availableColumns),
1520
+ children: "Show All"
1521
+ }
1522
+ ),
1523
+ /* @__PURE__ */ jsx(
1524
+ Button,
1525
+ {
1526
+ variant: "tertiary",
1527
+ size: "S",
1528
+ onClick: () => resetDefaultColumns(col.uid, availableColumns),
1529
+ children: "Reset"
1530
+ }
1531
+ ),
1532
+ /* @__PURE__ */ jsx(
1533
+ Button,
1534
+ {
1535
+ variant: "ghost",
1536
+ size: "S",
1537
+ onClick: () => setOpenColumnPickerUid(null),
1538
+ children: "Close"
1539
+ }
1540
+ )
1541
+ ] })
1542
+ ] })
1543
+ }
1544
+ ),
1545
+ /* @__PURE__ */ jsx(Box, { className: "bulk-updater-table-container", style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" }, children: /* @__PURE__ */ jsxs(Table, { colCount: activeColumns.length, rowCount: visibleEntries.length + 1, style: { width: "100%" }, children: [
1546
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsx(Tr, { children: activeColumns.map(
1547
+ (colDef) => renderHeaderCell(colDef, col, currentSort, selectedAttribute, handleHeaderSortClick)
1548
+ ) }) }),
1549
+ /* @__PURE__ */ jsx(Tbody, { children: visibleEntries.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: activeColumns.length, children: /* @__PURE__ */ jsx(Box, { padding: 4, style: { textAlign: "center" }, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No entries in this content type match the active filter criteria." }) }) }) }) : visibleEntries.map((entry) => {
1550
+ const key = `${col.uid}${KEY_SEP}${entry.documentId}`;
1551
+ const isSelected = Boolean(selectedEntries[key]);
1552
+ const isMatch = filterOperator !== "none" && entryMatchesFilter(entry, selectedAttribute);
1553
+ return /* @__PURE__ */ jsx(
1554
+ Tr,
1555
+ {
1556
+ style: {
1557
+ backgroundColor: isSelected ? "rgba(73, 69, 255, 0.14)" : isMatch ? "rgba(46, 213, 115, 0.08)" : void 0,
1558
+ boxShadow: isSelected ? "inset 3px 0 0 0 #4945ff" : isMatch ? "inset 3px 0 0 0 #2ed573" : void 0
1559
+ },
1560
+ children: activeColumns.map((colDef) => /* @__PURE__ */ jsx(Td, { children: renderCellContent(
1561
+ colDef,
1562
+ entry,
1563
+ col,
1564
+ selectedAttribute,
1565
+ isMatch,
1566
+ hasAttr,
1567
+ isSelected,
1568
+ () => toggleEntry(col.uid, entry.documentId)
1569
+ ) }, colDef.key))
1570
+ },
1571
+ key
1572
+ );
1573
+ }) })
1574
+ ] }) }),
1575
+ !col.isSingleType && /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginTop: 4, wrap: "wrap", gap: 3, children: [
1576
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
1577
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
1578
+ "Showing ",
1579
+ totalEntries === 0 ? 0 : Math.min((page - 1) * pageSize + 1, totalEntries),
1580
+ " to ",
1581
+ Math.min(page * pageSize, totalEntries),
1582
+ " of ",
1583
+ totalEntries,
1584
+ " entries"
1585
+ ] }),
1586
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
1587
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Show:" }),
1588
+ /* @__PURE__ */ jsx(Box, { style: { width: 80 }, children: /* @__PURE__ */ jsxs(
1589
+ SingleSelect,
1590
+ {
1591
+ size: "S",
1592
+ value: String(pageSize),
1593
+ onChange: (val) => handleCollectionPageSizeChange(col.uid, Number(val)),
1594
+ children: [
1595
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "5", children: "5" }),
1596
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "10", children: "10" }),
1597
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "25", children: "25" }),
1598
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "50", children: "50" }),
1599
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100" })
1600
+ ]
1601
+ }
1602
+ ) }),
1603
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "per page" })
1604
+ ] })
1605
+ ] }),
1606
+ pageCount > 1 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1607
+ /* @__PURE__ */ jsx(
1608
+ Button,
1609
+ {
1610
+ variant: "tertiary",
1611
+ size: "S",
1612
+ disabled: page <= 1 || pageState?.loading,
1613
+ onClick: () => loadCollectionPage(col.uid, page - 1),
1614
+ children: "← Previous"
1615
+ }
1616
+ ),
1617
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: [
1618
+ "Page ",
1619
+ page,
1620
+ " of ",
1621
+ pageCount
1622
+ ] }),
1623
+ /* @__PURE__ */ jsx(
1624
+ Button,
1625
+ {
1626
+ variant: "tertiary",
1627
+ size: "S",
1628
+ disabled: page >= pageCount || pageState?.loading,
1629
+ onClick: () => loadCollectionPage(col.uid, page + 1),
1630
+ children: "Next →"
1631
+ }
1632
+ )
1633
+ ] })
1634
+ ] })
1635
+ ]
1636
+ },
1637
+ col.uid
1638
+ );
1639
+ })
1640
+ ]
1641
+ }
1642
+ ),
1643
+ updateSummary && /* @__PURE__ */ jsxs(
1644
+ Box,
1645
+ {
1646
+ padding: 6,
1647
+ background: "neutral0",
1648
+ shadow: "filterShadow",
1649
+ hasRadius: true,
1650
+ marginBottom: 6,
1651
+ children: [
1652
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", fontWeight: "semiBold", marginBottom: 3, children: "3. Execution Results" }),
1653
+ /* @__PURE__ */ jsx(
1654
+ Alert,
1655
+ {
1656
+ title: "Batch Update Completed",
1657
+ variant: updateSummary.updatedCount > 0 ? "success" : "default",
1658
+ closeLabel: "Close",
1659
+ onClose: () => setUpdateSummary(null),
1660
+ marginBottom: 4,
1661
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 3, width: "100%", children: [
1662
+ /* @__PURE__ */ jsxs(Typography, { variant: "omega", children: [
1663
+ "Successfully updated ",
1664
+ /* @__PURE__ */ jsx("strong", { children: updateSummary.updatedCount }),
1665
+ " records. Skipped ",
1666
+ /* @__PURE__ */ jsx("strong", { children: updateSummary.skippedCount }),
1667
+ " records.",
1668
+ updateSummary.batchId && /* @__PURE__ */ jsxs("span", { children: [
1669
+ " ",
1670
+ "Transaction Batch ID: ",
1671
+ /* @__PURE__ */ jsx("code", { children: updateSummary.batchId }),
1672
+ " (logged to audit trail)."
1673
+ ] })
1674
+ ] }),
1675
+ updateSummary.batchId && /* @__PURE__ */ jsxs(Flex, { gap: 2, marginTop: 1, children: [
1676
+ /* @__PURE__ */ jsx(
1677
+ Button,
1678
+ {
1679
+ variant: "danger-light",
1680
+ size: "S",
1681
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
1682
+ onClick: () => handleRollback(updateSummary.batchId),
1683
+ loading: rollingBackBatchId === updateSummary.batchId,
1684
+ children: "Rollback This Batch"
1685
+ }
1686
+ ),
1687
+ /* @__PURE__ */ jsx(
1688
+ Button,
1689
+ {
1690
+ variant: "tertiary",
1691
+ size: "S",
1692
+ onClick: () => setActiveTab("audit"),
1693
+ children: "View in Audit Trail & Rollback History →"
1694
+ }
1695
+ )
1696
+ ] })
1697
+ ] })
1698
+ }
1699
+ ),
1700
+ lastResults && lastResults.length > 0 && /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: lastResults.length + 1, children: [
1701
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1702
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Collection" }) }),
1703
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Entry" }) }),
1704
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
1705
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Details" }) })
1706
+ ] }) }),
1707
+ /* @__PURE__ */ jsx(Tbody, { children: lastResults.map((res, idx) => /* @__PURE__ */ jsxs(Tr, { children: [
1708
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: res.collectionName }) }),
1709
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: res.label }) }),
1710
+ /* @__PURE__ */ jsxs(Td, { children: [
1711
+ res.status === "updated" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: "Updated" }),
1712
+ res.status === "skipped" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: "Skipped" }),
1713
+ res.status === "error" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger700", children: "Error" })
1714
+ ] }),
1715
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral700", children: res.message }) })
1716
+ ] }, idx)) })
1717
+ ] })
1718
+ ]
1719
+ }
1720
+ )
1721
+ ] }),
1722
+ activeTab === "audit" && /* @__PURE__ */ jsxs(
1723
+ Box,
1724
+ {
1725
+ padding: 6,
1726
+ background: "neutral0",
1727
+ shadow: "filterShadow",
1728
+ hasRadius: true,
1729
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
1730
+ children: [
1731
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, children: [
1732
+ /* @__PURE__ */ jsxs(Box, { children: [
1733
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", fontWeight: "semiBold", children: "Audit Trail & Version History" }),
1734
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Deterministic record of all batch updates, timestamps, admin authorship, and one-click rollback." })
1735
+ ] }),
1736
+ /* @__PURE__ */ jsx(
1737
+ Button,
1738
+ {
1739
+ variant: "tertiary",
1740
+ size: "S",
1741
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
1742
+ onClick: () => {
1743
+ fetchAuditHistory();
1744
+ fetchPendingBatches();
1745
+ },
1746
+ loading: auditLoading || pendingBatchesLoading,
1747
+ children: "Refresh History"
1748
+ }
1749
+ )
1750
+ ] }),
1751
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 6, padding: 5, background: "neutral100", hasRadius: true, style: { border: "1px solid #e6e6ef" }, children: [
1752
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, children: [
1753
+ /* @__PURE__ */ jsxs(Box, { children: [
1754
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1755
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", fontWeight: "bold", children: "Pending Approvals & Queued Batches" }),
1756
+ pendingBatches.filter((b) => b.status === "pending_approval" || b.status === "queued_offpeak").length > 0 && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "warning100", textColor: "warning700", children: [
1757
+ pendingBatches.filter((b) => b.status === "pending_approval" || b.status === "queued_offpeak").length,
1758
+ " Action Required"
1759
+ ] })
1760
+ ] }),
1761
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Four-Eyes Maker-Checker authorization queue and off-peak scheduled maintenance batches." })
1762
+ ] }),
1763
+ /* @__PURE__ */ jsx(
1764
+ Button,
1765
+ {
1766
+ variant: "tertiary",
1767
+ size: "S",
1768
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
1769
+ onClick: fetchPendingBatches,
1770
+ loading: pendingBatchesLoading,
1771
+ children: "Refresh Queue"
1772
+ }
1773
+ )
1774
+ ] }),
1775
+ pendingBatches.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral0", hasRadius: true, style: { textAlign: "center" }, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "No batches pending approval or queued for off-peak execution." }) }) : /* @__PURE__ */ jsx(Box, { className: "bulk-updater-table-container", style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" }, children: /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: pendingBatches.length + 1, style: { width: "100%" }, children: [
1776
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1777
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Batch ID & Created" }) }),
1778
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 130 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Maker" }) }),
1779
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 200 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Target Details" }) }),
1780
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 150 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
1781
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 180 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Notes" }) }),
1782
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 180, textAlign: "right" }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
1783
+ ] }) }),
1784
+ /* @__PURE__ */ jsx(Tbody, { children: pendingBatches.map((batch) => /* @__PURE__ */ jsxs(Tr, { children: [
1785
+ /* @__PURE__ */ jsxs(Td, { children: [
1786
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: batch.id }),
1787
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", display: "block", children: new Date(batch.createdAt).toLocaleString() })
1788
+ ] }),
1789
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "omega", children: batch.createdBy?.email || "admin" }) }),
1790
+ /* @__PURE__ */ jsxs(Td, { children: [
1791
+ /* @__PURE__ */ jsxs(Typography, { variant: "omega", fontWeight: "semiBold", children: [
1792
+ batch.summary.attribute,
1793
+ ': "',
1794
+ formatDisplayValue(batch.summary.value),
1795
+ '"'
1796
+ ] }),
1797
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", marginTop: 1, children: [
1798
+ /* @__PURE__ */ jsx(
1799
+ Badge,
1800
+ {
1801
+ backgroundColor: batch.summary.publishOnApproval === false ? "warning100" : "success100",
1802
+ textColor: batch.summary.publishOnApproval === false ? "warning700" : "success700",
1803
+ children: batch.summary.publishOnApproval === false ? "DRAFT ONLY" : "AUTO-PUBLISH"
1804
+ }
1805
+ ),
1806
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
1807
+ "• ",
1808
+ batch.summary.totalTargets,
1809
+ " records across ",
1810
+ Object.keys(batch.summary.collectionBreakdown || {}).length,
1811
+ " collections"
1812
+ ] })
1813
+ ] })
1814
+ ] }),
1815
+ /* @__PURE__ */ jsxs(Td, { children: [
1816
+ batch.status === "pending_approval" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: "PENDING CHECKER APPROVAL" }),
1817
+ batch.status === "queued_offpeak" && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
1818
+ "QUEUED OFF-PEAK (",
1819
+ batch.scheduledFor || "18:00",
1820
+ ")"
1821
+ ] }),
1822
+ batch.status === "approved" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: "APPROVED" }),
1823
+ batch.status === "rejected" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger700", children: "REJECTED" }),
1824
+ batch.status === "executed" && /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral200", textColor: "neutral700", children: "EXECUTED" })
1825
+ ] }),
1826
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral700", children: [
1827
+ batch.makerNotes || "—",
1828
+ batch.approval?.rejectionReason && /* @__PURE__ */ jsxs("span", { style: { color: "#d9534f", display: "block" }, children: [
1829
+ "Rejection Reason: ",
1830
+ batch.approval.rejectionReason
1831
+ ] })
1832
+ ] }) }),
1833
+ /* @__PURE__ */ jsx(Td, { style: { textAlign: "right" }, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, justifyContent: "flex-end", children: [
1834
+ batch.status === "pending_approval" && /* @__PURE__ */ jsxs(Fragment, { children: [
1835
+ /* @__PURE__ */ jsx(
1836
+ Button,
1837
+ {
1838
+ size: "S",
1839
+ variant: "success-light",
1840
+ onClick: () => handleApproveBatch(batch.id),
1841
+ loading: approvingBatchId === batch.id,
1842
+ children: "Approve & Execute"
1843
+ }
1844
+ ),
1845
+ /* @__PURE__ */ jsx(
1846
+ Button,
1847
+ {
1848
+ size: "S",
1849
+ variant: "danger-light",
1850
+ onClick: () => handleRejectBatch(batch.id),
1851
+ loading: rejectingBatchId === batch.id,
1852
+ children: "Reject"
1853
+ }
1854
+ )
1855
+ ] }),
1856
+ batch.status === "queued_offpeak" && /* @__PURE__ */ jsx(
1857
+ Button,
1858
+ {
1859
+ size: "S",
1860
+ variant: "default",
1861
+ onClick: () => handleExecuteNow(batch.id),
1862
+ loading: executingNowBatchId === batch.id,
1863
+ children: "Run Now"
1864
+ }
1865
+ )
1866
+ ] }) })
1867
+ ] }, batch.id)) })
1868
+ ] }) })
1869
+ ] }),
1870
+ auditNotice && /* @__PURE__ */ jsx(Alert, { variant: "warning", title: "Audit Configuration Status", marginBottom: 4, children: auditNotice }),
1871
+ auditHistory.length === 0 && !auditLoading && /* @__PURE__ */ jsxs(Box, { padding: 8, background: "neutral100", hasRadius: true, style: { textAlign: "center" }, children: [
1872
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral600", children: "No batch operations recorded in the audit trail yet." }),
1873
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", display: "block", marginTop: 2, children: "When you execute a bulk update in the editor, its transactional diff will be logged here." })
1874
+ ] }),
1875
+ auditHistory.length > 0 && /* @__PURE__ */ jsx(Box, { className: "bulk-updater-table-container", style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" }, children: /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: auditHistory.length + 1, style: { width: "100%" }, children: [
1876
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1877
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 140 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Date & Time" }) }),
1878
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 130 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Performed By" }) }),
1879
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Attribute & Value" }) }),
1880
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 90 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Impact" }) }),
1881
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 120 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
1882
+ /* @__PURE__ */ jsx(Th, { style: { minWidth: 160, textAlign: "right" }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
1883
+ ] }) }),
1884
+ /* @__PURE__ */ jsx(Tbody, { children: auditHistory.map((rec) => {
1885
+ const isExpanded = expandedDiffBatchId === rec.batchId;
1886
+ const dateStr = new Date(rec.executedAt).toLocaleString();
1887
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
1888
+ /* @__PURE__ */ jsxs(
1889
+ Tr,
1890
+ {
1891
+ style: {
1892
+ backgroundColor: rec.isRolledBack ? "rgba(255, 255, 255, 0.04)" : void 0,
1893
+ opacity: rec.isRolledBack ? 0.85 : 1
1894
+ },
1895
+ children: [
1896
+ /* @__PURE__ */ jsxs(Td, { style: { minWidth: 140 }, children: [
1897
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: dateStr }),
1898
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", display: "block", style: { wordBreak: "break-all" }, children: [
1899
+ "Batch ID: ",
1900
+ rec.batchId
1901
+ ] })
1902
+ ] }),
1903
+ /* @__PURE__ */ jsxs(Td, { style: { minWidth: 130 }, children: [
1904
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: rec.userName }),
1905
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", display: "block", style: { wordBreak: "break-all" }, children: rec.userEmail })
1906
+ ] }),
1907
+ /* @__PURE__ */ jsx(Td, { style: { minWidth: 160 }, children: /* @__PURE__ */ jsxs(
1908
+ Typography,
1909
+ {
1910
+ style: {
1911
+ display: "inline-block",
1912
+ maxWidth: "100%",
1913
+ overflow: "hidden",
1914
+ textOverflow: "ellipsis",
1915
+ whiteSpace: "nowrap"
1916
+ },
1917
+ title: `${rec.attribute} → "${formatDisplayValue(rec.value)}"`,
1918
+ children: [
1919
+ /* @__PURE__ */ jsx("strong", { children: rec.attribute }),
1920
+ " → “",
1921
+ formatDisplayValue(rec.value),
1922
+ "”"
1923
+ ]
1924
+ }
1925
+ ) }),
1926
+ /* @__PURE__ */ jsxs(Td, { style: { minWidth: 90, whiteSpace: "nowrap" }, children: [
1927
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: "success100", textColor: "success700", children: [
1928
+ rec.updatedCount,
1929
+ " updated"
1930
+ ] }),
1931
+ rec.skippedCount > 0 && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "warning100", textColor: "warning700", style: { marginLeft: 6 }, children: [
1932
+ rec.skippedCount,
1933
+ " skipped"
1934
+ ] })
1935
+ ] }),
1936
+ /* @__PURE__ */ jsx(Td, { style: { minWidth: 120 }, children: rec.isRolledBack ? /* @__PURE__ */ jsxs(Box, { children: [
1937
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral200", textColor: "neutral700", children: "ROLLED BACK" }),
1938
+ rec.rolledBackAt && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", display: "block", style: { wordBreak: "break-word", marginTop: 2 }, children: [
1939
+ new Date(rec.rolledBackAt).toLocaleTimeString(),
1940
+ " by ",
1941
+ rec.rolledBackBy || "Admin"
1942
+ ] })
1943
+ ] }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: "ACTIVE (APPLIED)" }) }),
1944
+ /* @__PURE__ */ jsx(Td, { style: { minWidth: 160, textAlign: "right", whiteSpace: "nowrap" }, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, justifyContent: "flex-end", children: [
1945
+ /* @__PURE__ */ jsx(
1946
+ Button,
1947
+ {
1948
+ variant: "tertiary",
1949
+ size: "S",
1950
+ onClick: () => setExpandedDiffBatchId(isExpanded ? null : rec.batchId),
1951
+ children: isExpanded ? "Hide Diffs" : "View Diffs"
1952
+ }
1953
+ ),
1954
+ !rec.isRolledBack && /* @__PURE__ */ jsx(
1955
+ Button,
1956
+ {
1957
+ variant: "danger",
1958
+ size: "S",
1959
+ loading: rollingBackBatchId === rec.batchId,
1960
+ onClick: () => handleRollback(rec.batchId),
1961
+ children: "Rollback"
1962
+ }
1963
+ )
1964
+ ] }) })
1965
+ ]
1966
+ }
1967
+ ),
1968
+ isExpanded && /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: 6, style: { background: "rgba(0, 0, 0, 0.06)", padding: 16 }, children: /* @__PURE__ */ jsxs(
1969
+ Box,
1970
+ {
1971
+ className: "bulk-updater-table-container",
1972
+ padding: 3,
1973
+ background: "neutral0",
1974
+ hasRadius: true,
1975
+ borderStyle: "solid",
1976
+ borderWidth: "1px",
1977
+ borderColor: "neutral200",
1978
+ style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" },
1979
+ children: [
1980
+ /* @__PURE__ */ jsxs(Typography, { variant: "delta", fontWeight: "bold", marginBottom: 2, children: [
1981
+ "Detailed Before/After Snapshots for Batch ",
1982
+ rec.batchId,
1983
+ ":"
1984
+ ] }),
1985
+ /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: rec.diffs.length + 1, children: [
1986
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1987
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Collection" }) }),
1988
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Document / Entry" }) }),
1989
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Previous Value" }) }),
1990
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Applied Value" }) })
1991
+ ] }) }),
1992
+ /* @__PURE__ */ jsx(Tbody, { children: rec.diffs.map((d, dIdx) => /* @__PURE__ */ jsxs(Tr, { children: [
1993
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: d.collectionName }) }),
1994
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: d.label }) }),
1995
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: formatDisplayValue(d.oldValue) }) }),
1996
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "success600", children: formatDisplayValue(d.newValue) }) })
1997
+ ] }, dIdx)) })
1998
+ ] })
1999
+ ]
2000
+ }
2001
+ ) }) })
2002
+ ] }, rec.batchId);
2003
+ }) })
2004
+ ] }) })
2005
+ ]
2006
+ }
2007
+ ),
2008
+ showConfirmModal && /* @__PURE__ */ jsx(
2009
+ "div",
2010
+ {
2011
+ style: {
2012
+ position: "fixed",
2013
+ inset: 0,
2014
+ backgroundColor: "rgba(20, 20, 30, 0.65)",
2015
+ backdropFilter: "blur(2px)",
2016
+ display: "flex",
2017
+ alignItems: "center",
2018
+ justifyContent: "center",
2019
+ zIndex: 9999,
2020
+ padding: 16,
2021
+ boxSizing: "border-box",
2022
+ overflowY: "auto"
2023
+ },
2024
+ onClick: () => setShowConfirmModal(false),
2025
+ children: /* @__PURE__ */ jsxs(
2026
+ Box,
2027
+ {
2028
+ background: "neutral0",
2029
+ hasRadius: true,
2030
+ shadow: "popupShadow",
2031
+ style: {
2032
+ maxWidth: 680,
2033
+ width: "100%",
2034
+ maxHeight: "calc(100vh - 48px)",
2035
+ display: "flex",
2036
+ flexDirection: "column",
2037
+ overflow: "hidden",
2038
+ position: "relative",
2039
+ boxShadow: "0 24px 48px rgba(0, 0, 0, 0.32), 0 0 1px rgba(0, 0, 0, 0.2)"
2040
+ },
2041
+ onClick: (e) => e.stopPropagation(),
2042
+ children: [
2043
+ /* @__PURE__ */ jsx(
2044
+ Box,
2045
+ {
2046
+ padding: 5,
2047
+ background: "neutral100",
2048
+ style: {
2049
+ borderBottom: "1px solid rgba(120, 120, 150, 0.2)",
2050
+ flexShrink: 0
2051
+ },
2052
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
2053
+ /* @__PURE__ */ jsxs(Box, { children: [
2054
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Confirm Bulk Update" }),
2055
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", display: "block", children: "Review target attributes, affected counts, and server execution policy before proceeding." })
2056
+ ] }),
2057
+ /* @__PURE__ */ jsx(
2058
+ Badge,
2059
+ {
2060
+ backgroundColor: license?.tier === "enterprise" ? "primary100" : "neutral200",
2061
+ textColor: license?.tier === "enterprise" ? "primary700" : "neutral700",
2062
+ children: license?.tier === "enterprise" ? "ENTERPRISE 4-EYES" : "STANDARD"
2063
+ }
2064
+ )
2065
+ ] })
2066
+ }
2067
+ ),
2068
+ /* @__PURE__ */ jsxs(
2069
+ Box,
2070
+ {
2071
+ padding: 5,
2072
+ style: {
2073
+ flex: "1 1 auto",
2074
+ minHeight: 0,
2075
+ overflowY: "auto",
2076
+ WebkitOverflowScrolling: "touch"
2077
+ },
2078
+ children: [
2079
+ /* @__PURE__ */ jsxs(Box, { padding: 4, background: "neutral100", hasRadius: true, marginBottom: 4, children: [
2080
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 2, display: "block", children: "UPDATE DETAILS" }),
2081
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
2082
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", children: [
2083
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: "Target Attribute:" }),
2084
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "primary100", textColor: "primary700", children: selectedAttribute })
2085
+ ] }),
2086
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
2087
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: "Target Value:" }),
2088
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", style: { wordBreak: "break-all", maxWidth: 360 }, children: relationsMeta[selectedAttribute] ? /* @__PURE__ */ jsx("span", { children: selectedRelationDocId === "__clear__" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger700", children: "Clear / Unlink Relation" }) : /* @__PURE__ */ jsxs(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: [
2089
+ relationsMeta[selectedAttribute].isMulti ? `[${relationMode.toUpperCase()}] ` : "",
2090
+ relationOptions.find((o) => o.documentId === selectedRelationDocId)?.label || selectedRelationDocId || "(none selected)"
2091
+ ] }) }) : useRegex ? /* @__PURE__ */ jsxs("span", { children: [
2092
+ "Replace ",
2093
+ /* @__PURE__ */ jsxs("code", { children: [
2094
+ "/",
2095
+ regexPattern,
2096
+ "/g"
2097
+ ] }),
2098
+ " with ",
2099
+ /* @__PURE__ */ jsxs("strong", { children: [
2100
+ '"',
2101
+ attributeValue,
2102
+ '"'
2103
+ ] })
2104
+ ] }) : /* @__PURE__ */ jsxs("strong", { children: [
2105
+ '"',
2106
+ attributeValue || "(empty string)",
2107
+ '"'
2108
+ ] }) })
2109
+ ] }),
2110
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", children: [
2111
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: "Total Selected Records:" }),
2112
+ /* @__PURE__ */ jsxs(Typography, { fontWeight: "bold", textColor: "primary600", children: [
2113
+ totalSelectedCount,
2114
+ " records"
2115
+ ] })
2116
+ ] }),
2117
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
2118
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: "Publishing Mode:" }),
2119
+ /* @__PURE__ */ jsx(
2120
+ Badge,
2121
+ {
2122
+ backgroundColor: publishOnApproval ? "success100" : "warning100",
2123
+ textColor: publishOnApproval ? "success700" : "warning700",
2124
+ children: publishOnApproval ? "Publish Immediately" : "Keep as Draft"
2125
+ }
2126
+ )
2127
+ ] })
2128
+ ] })
2129
+ ] }),
2130
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
2131
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 2, display: "block", children: "COLLECTION BREAKDOWN" }),
2132
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 1, children: Object.entries(getSelectedBreakdown()).map(([name, count]) => /* @__PURE__ */ jsxs(
2133
+ Flex,
2134
+ {
2135
+ justifyContent: "space-between",
2136
+ padding: 2,
2137
+ background: "neutral100",
2138
+ hasRadius: true,
2139
+ style: { border: "1px solid rgba(120, 120, 150, 0.15)" },
2140
+ children: [
2141
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", children: name }),
2142
+ /* @__PURE__ */ jsxs(Badge, { backgroundColor: "neutral200", textColor: "neutral800", children: [
2143
+ count,
2144
+ " ",
2145
+ count === 1 ? "record" : "records"
2146
+ ] })
2147
+ ]
2148
+ },
2149
+ name
2150
+ )) })
2151
+ ] }),
2152
+ /* @__PURE__ */ jsx(Box, { padding: 4, hasRadius: true, marginBottom: 4, background: executionWindow?.isBusinessHours ? "warning100" : "success100", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, children: [
2153
+ /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", textColor: executionWindow?.isBusinessHours ? "warning700" : "success700", children: executionWindow?.isBusinessHours ? "⏰ Peak Business Hours Restriction Active" : "✅ Server Off-Peak Window Active" }) }),
2154
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: executionWindow?.isBusinessHours ? "warning700" : "success700", children: executionWindow?.policyMessage || (executionWindow?.isBusinessHours ? "Server policy restricts bulk updates during peak business hours (09:00 - 18:00). Direct execution is paused to preserve API throughput." : "Currently outside peak business hours. Direct execution is permitted.") })
2155
+ ] }) }),
2156
+ /* @__PURE__ */ jsx(
2157
+ Box,
2158
+ {
2159
+ padding: 4,
2160
+ background: "neutral100",
2161
+ hasRadius: true,
2162
+ marginBottom: 4,
2163
+ style: { border: "1px solid rgba(120, 120, 150, 0.15)" },
2164
+ children: /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
2165
+ /* @__PURE__ */ jsx(
2166
+ Checkbox,
2167
+ {
2168
+ name: "publishOnApproval",
2169
+ checked: publishOnApproval,
2170
+ onCheckedChange: (checked) => setPublishOnApproval(Boolean(checked))
2171
+ }
2172
+ ),
2173
+ /* @__PURE__ */ jsxs(Box, { style: { cursor: "pointer" }, onClick: () => setPublishOnApproval(!publishOnApproval), children: [
2174
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", display: "block", children: "Publish immediately upon checker approval / execution" }),
2175
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "When checked (default), content types with Draft & Publish enabled will have their updated documents published immediately upon execution. When unchecked, changes remain in Draft for further editorial review." })
2176
+ ] })
2177
+ ] })
2178
+ }
2179
+ ),
2180
+ /* @__PURE__ */ jsx(Box, { marginBottom: 2, children: /* @__PURE__ */ jsx(
2181
+ TextInput,
2182
+ {
2183
+ label: "Change Request / Release Notes (Optional)",
2184
+ name: "makerNotes",
2185
+ placeholder: "e.g. REL-4021: Updating summer pricing & promo attributes",
2186
+ value: makerNotesInput,
2187
+ onChange: (e) => setMakerNotesInput(e.target.value)
2188
+ }
2189
+ ) })
2190
+ ]
2191
+ }
2192
+ ),
2193
+ /* @__PURE__ */ jsx(
2194
+ Box,
2195
+ {
2196
+ padding: 4,
2197
+ background: "neutral100",
2198
+ style: {
2199
+ borderTop: "1px solid rgba(120, 120, 150, 0.2)",
2200
+ flexShrink: 0,
2201
+ position: "sticky",
2202
+ bottom: 0,
2203
+ zIndex: 20,
2204
+ boxShadow: "0 -4px 16px rgba(0, 0, 0, 0.08)"
2205
+ },
2206
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 3, style: { width: "100%" }, children: [
2207
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "M", onClick: () => setShowConfirmModal(false), children: "Cancel" }),
2208
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", justifyContent: "flex-end", alignItems: "center", children: [
2209
+ (license?.tier === "enterprise" || license?.features?.includes("Maker-Checker Approval Workflow (4-Eyes Principle)")) && /* @__PURE__ */ jsx(
2210
+ Button,
2211
+ {
2212
+ variant: executionWindow?.isBusinessHours ? "default" : "secondary",
2213
+ size: "M",
2214
+ onClick: () => executeBatchWithMode("maker_checker"),
2215
+ loading: updating,
2216
+ children: "Submit for Checker Approval"
2217
+ }
2218
+ ),
2219
+ executionWindow?.isBusinessHours ? /* @__PURE__ */ jsxs(Fragment, { children: [
2220
+ /* @__PURE__ */ jsxs(
2221
+ Button,
2222
+ {
2223
+ variant: license?.tier === "enterprise" ? "secondary" : "default",
2224
+ size: "M",
2225
+ onClick: () => executeBatchWithMode("queue_offpeak"),
2226
+ loading: updating,
2227
+ children: [
2228
+ "Queue for Off-Peak (",
2229
+ executionWindow?.offPeakStartTime || "18:00",
2230
+ ")"
2231
+ ]
2232
+ }
2233
+ ),
2234
+ /* @__PURE__ */ jsx(
2235
+ Button,
2236
+ {
2237
+ variant: "danger-light",
2238
+ size: "S",
2239
+ onClick: () => executeBatchWithMode("immediate", true),
2240
+ loading: updating,
2241
+ children: "Force Run Now"
2242
+ }
2243
+ )
2244
+ ] }) : /* @__PURE__ */ jsx(
2245
+ Button,
2246
+ {
2247
+ variant: "default",
2248
+ size: "M",
2249
+ onClick: () => executeBatchWithMode("immediate"),
2250
+ loading: updating,
2251
+ children: "Confirm & Execute Now"
2252
+ }
2253
+ )
2254
+ ] })
2255
+ ] })
2256
+ }
2257
+ )
2258
+ ]
2259
+ }
2260
+ )
2261
+ }
2262
+ )
2263
+ ]
2264
+ }
2265
+ );
2266
+ };
2267
+ export {
2268
+ BulkUpdater as default,
2269
+ formatDisplayValue,
2270
+ getAvailableColumnsForCollection
2271
+ };
2272
+ //# sourceMappingURL=BulkUpdater-R78pX-om.mjs.map