strapi-plugin-bulk-updater 1.0.1 → 1.2.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.
Files changed (32) hide show
  1. package/README.md +47 -24
  2. package/dist/_chunks/{BulkUpdater-R78pX-om.mjs → BulkUpdater-BDszahRM.mjs} +657 -159
  3. package/dist/_chunks/BulkUpdater-BDszahRM.mjs.map +1 -0
  4. package/dist/_chunks/{BulkUpdater-CooMo-Hc.js → BulkUpdater-DCrCPKSe.js} +649 -151
  5. package/dist/_chunks/BulkUpdater-DCrCPKSe.js.map +1 -0
  6. package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs +183 -0
  7. package/dist/_chunks/ScopedQuotaSelectInput-ChWrQIBV.mjs.map +1 -0
  8. package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js +201 -0
  9. package/dist/_chunks/ScopedQuotaSelectInput-s4oUMLgK.js.map +1 -0
  10. package/dist/_chunks/SettingsPage-BRGTrW9H.js +43 -0
  11. package/dist/_chunks/SettingsPage-BRGTrW9H.js.map +1 -0
  12. package/dist/_chunks/SettingsPage-ieu-Out8.mjs +43 -0
  13. package/dist/_chunks/SettingsPage-ieu-Out8.mjs.map +1 -0
  14. package/dist/_chunks/ValidationRulesTab-BGcqM9cN.mjs +598 -0
  15. package/dist/_chunks/ValidationRulesTab-BGcqM9cN.mjs.map +1 -0
  16. package/dist/_chunks/ValidationRulesTab-BoqhoTjo.js +597 -0
  17. package/dist/_chunks/ValidationRulesTab-BoqhoTjo.js.map +1 -0
  18. package/dist/admin/index.js +19 -3
  19. package/dist/admin/index.js.map +1 -1
  20. package/dist/admin/index.mjs +19 -3
  21. package/dist/admin/index.mjs.map +1 -1
  22. package/dist/server/index.js +30795 -318
  23. package/dist/server/index.js.map +1 -1
  24. package/dist/server/index.mjs +30785 -318
  25. package/dist/server/index.mjs.map +1 -1
  26. package/package.json +10 -3
  27. package/dist/_chunks/BulkUpdater-CooMo-Hc.js.map +0 -1
  28. package/dist/_chunks/BulkUpdater-R78pX-om.mjs.map +0 -1
  29. package/dist/_chunks/SettingsPage-C6QyloJe.js +0 -176
  30. package/dist/_chunks/SettingsPage-C6QyloJe.js.map +0 -1
  31. package/dist/_chunks/SettingsPage-Drb7s719.mjs +0 -176
  32. package/dist/_chunks/SettingsPage-Drb7s719.mjs.map +0 -1
@@ -1,8 +1,428 @@
1
- import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
- import React, { useState, useEffect } from "react";
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import React__default, { useState, useEffect, useRef } from "react";
3
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";
4
+ import { Box, Flex, Typography, Button, SingleSelect, SingleSelectOption, Checkbox, Badge, Table, Thead, Tr, Th, Tbody, Td, Alert, TextInput } from "@strapi/design-system";
5
+ import { ArrowClockwise, Download, Upload, Check, Play } from "@strapi/icons";
6
+ import { V as ValidationRulesTab } from "./ValidationRulesTab-BGcqM9cN.mjs";
7
+ function CsvManagerTab() {
8
+ const { get, post } = useFetchClient();
9
+ const { toggleNotification } = useNotification();
10
+ const [collections, setCollections] = useState([]);
11
+ const [loadingCollections, setLoadingCollections] = useState(true);
12
+ const [selectedUid, setSelectedUid] = useState("");
13
+ const [exportLimit, setExportLimit] = useState(1e3);
14
+ const [exportOnlyViolations, setExportOnlyViolations] = useState(false);
15
+ const [rules, setRules] = useState([]);
16
+ const [selectedRuleId, setSelectedRuleId] = useState("");
17
+ const [importMode, setImportMode] = useState("update");
18
+ const [publishOnImport, setPublishOnImport] = useState(true);
19
+ const [csvRawText, setCsvRawText] = useState("");
20
+ const [fileName, setFileName] = useState("");
21
+ const [parsedPreview, setParsedPreview] = useState([]);
22
+ const [importing, setImporting] = useState(false);
23
+ const [importResult, setImportResult] = useState(null);
24
+ useEffect(() => {
25
+ fetchInitialData();
26
+ }, []);
27
+ const fetchInitialData = async () => {
28
+ try {
29
+ setLoadingCollections(true);
30
+ const [modelsRes, rulesRes] = await Promise.all([
31
+ get("/bulk-updater/models"),
32
+ get("/bulk-updater/validation-rules")
33
+ ]);
34
+ if (modelsRes.data?.collections) {
35
+ setCollections(modelsRes.data.collections);
36
+ if (modelsRes.data.collections.length > 0) {
37
+ setSelectedUid(modelsRes.data.collections[0].uid);
38
+ }
39
+ }
40
+ if (rulesRes.data?.rules) {
41
+ setRules(rulesRes.data.rules);
42
+ if (rulesRes.data.rules.length > 0) {
43
+ setSelectedRuleId(rulesRes.data.rules[0].id);
44
+ }
45
+ }
46
+ } catch (err) {
47
+ toggleNotification({
48
+ type: "danger",
49
+ message: err.message || "Failed loading collections for CSV Manager"
50
+ });
51
+ } finally {
52
+ setLoadingCollections(false);
53
+ }
54
+ };
55
+ const handleDownloadTemplate = async () => {
56
+ if (!selectedUid) return;
57
+ try {
58
+ const res = await get(`/bulk-updater/csv/template/${selectedUid}`);
59
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
60
+ const filename = res.data?.filename || `${selectedUid.split(".").pop()}_template.csv`;
61
+ downloadCsvFile(csvStr, filename);
62
+ toggleNotification({
63
+ type: "success",
64
+ message: "CSV Template downloaded successfully"
65
+ });
66
+ } catch (err) {
67
+ toggleNotification({
68
+ type: "danger",
69
+ message: err.message || "Failed downloading CSV template"
70
+ });
71
+ }
72
+ };
73
+ const handleExportCollection = async () => {
74
+ if (!selectedUid) return;
75
+ try {
76
+ const res = await get(`/bulk-updater/csv/export/${selectedUid}?limit=${exportLimit}`);
77
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
78
+ const filename = res.data?.filename || `${selectedUid.split(".").pop()}_export.csv`;
79
+ downloadCsvFile(csvStr, filename);
80
+ toggleNotification({
81
+ type: "success",
82
+ message: "Collection exported to CSV successfully"
83
+ });
84
+ } catch (err) {
85
+ toggleNotification({
86
+ type: "danger",
87
+ message: err.message || "Failed exporting collection to CSV"
88
+ });
89
+ }
90
+ };
91
+ const handleExportViolations = async () => {
92
+ if (!selectedRuleId) return;
93
+ try {
94
+ const res = await get(`/bulk-updater/csv/export-violations/${selectedRuleId}`);
95
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
96
+ const filename = res.data?.filename || `violations_${selectedRuleId}.csv`;
97
+ downloadCsvFile(csvStr, filename);
98
+ toggleNotification({
99
+ type: "success",
100
+ message: "Rule violations exported to CSV successfully"
101
+ });
102
+ } catch (err) {
103
+ toggleNotification({
104
+ type: "danger",
105
+ message: err.message || "Failed exporting violations to CSV"
106
+ });
107
+ }
108
+ };
109
+ const downloadCsvFile = (content, fallbackName) => {
110
+ if (!content) {
111
+ toggleNotification({
112
+ type: "warning",
113
+ message: "No CSV content to download"
114
+ });
115
+ return;
116
+ }
117
+ const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
118
+ const url = URL.createObjectURL(blob);
119
+ const link = document.createElement("a");
120
+ link.href = url;
121
+ link.setAttribute("download", fallbackName);
122
+ document.body.appendChild(link);
123
+ link.click();
124
+ document.body.removeChild(link);
125
+ URL.revokeObjectURL(url);
126
+ };
127
+ const handleFileUpload = (e) => {
128
+ const file = e.target.files?.[0];
129
+ if (!file) return;
130
+ setFileName(file.name);
131
+ setImportResult(null);
132
+ const reader = new FileReader();
133
+ reader.onload = (event) => {
134
+ const text = event.target?.result;
135
+ setCsvRawText(text);
136
+ try {
137
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
138
+ if (lines.length > 0) {
139
+ const headers = lines[0].split(",").map((h) => h.replace(/^"|"$/g, "").trim());
140
+ const previewRows = [];
141
+ for (let i = 1; i < Math.min(lines.length, 6); i++) {
142
+ const vals = lines[i].split(",").map((v) => v.replace(/^"|"$/g, "").trim());
143
+ const row = {};
144
+ headers.forEach((h, idx) => {
145
+ row[h] = vals[idx] || "";
146
+ });
147
+ previewRows.push(row);
148
+ }
149
+ setParsedPreview(previewRows);
150
+ }
151
+ } catch (err) {
152
+ setParsedPreview([]);
153
+ }
154
+ };
155
+ reader.readAsText(file);
156
+ };
157
+ const handleExecuteImport = async () => {
158
+ if (!selectedUid || !csvRawText) return;
159
+ try {
160
+ setImporting(true);
161
+ setImportResult(null);
162
+ const res = await post("/bulk-updater/csv/import", {
163
+ uid: selectedUid,
164
+ csvText: csvRawText,
165
+ mode: importMode,
166
+ publish: publishOnImport
167
+ });
168
+ if (res.data && res.data.success) {
169
+ setImportResult(res.data);
170
+ toggleNotification({
171
+ type: "success",
172
+ message: `CSV Import complete: ${res.data.stats.created} created, ${res.data.stats.updated} updated.`
173
+ });
174
+ } else {
175
+ throw new Error(res.data?.error || "Import failed");
176
+ }
177
+ } catch (err) {
178
+ toggleNotification({
179
+ type: "danger",
180
+ message: err.message || "Error processing CSV import"
181
+ });
182
+ } finally {
183
+ setImporting(false);
184
+ }
185
+ };
186
+ const selectedCol = collections.find((c) => c.uid === selectedUid);
187
+ return /* @__PURE__ */ jsx(Box, { padding: 6, background: "neutral0", hasRadius: true, shadow: "filterShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
188
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
189
+ /* @__PURE__ */ jsxs(Box, { children: [
190
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", fontWeight: "bold", children: "CSV Export & Import Center" }),
191
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Export all records, filtered subsets, or rule violations into CSV. Correct or add records in spreadsheets and re-upload with full schema & quota enforcement." })
192
+ ] }),
193
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}), onClick: fetchInitialData, children: "Refresh" })
194
+ ] }),
195
+ /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-end", children: [
196
+ /* @__PURE__ */ jsxs(Box, { style: { flex: 1 }, children: [
197
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Target Collection Type" }),
198
+ /* @__PURE__ */ jsx(
199
+ SingleSelect,
200
+ {
201
+ value: selectedUid,
202
+ onChange: (val) => {
203
+ setSelectedUid(val);
204
+ setImportResult(null);
205
+ },
206
+ disabled: loadingCollections,
207
+ placeholder: "Select a collection...",
208
+ children: collections.map((col) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: col.uid, children: [
209
+ col.displayName,
210
+ " (",
211
+ col.totalEntries ?? 0,
212
+ " records)"
213
+ ] }, col.uid))
214
+ }
215
+ )
216
+ ] }),
217
+ /* @__PURE__ */ jsx(
218
+ Button,
219
+ {
220
+ variant: "tertiary",
221
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
222
+ onClick: handleDownloadTemplate,
223
+ disabled: !selectedUid,
224
+ children: "Download Sample CSV Template"
225
+ }
226
+ )
227
+ ] }) }),
228
+ /* @__PURE__ */ jsxs(Box, { padding: 5, background: "neutral100", hasRadius: true, children: [
229
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h3", fontWeight: "bold", marginBottom: 2, children: "1. Export Data to CSV" }),
230
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", marginBottom: 4, children: "Extract entire collections or pinpoint records violating validation quotas." }),
231
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", alignItems: "center", marginTop: 3, children: [
232
+ /* @__PURE__ */ jsxs(
233
+ Button,
234
+ {
235
+ variant: "default",
236
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
237
+ onClick: handleExportCollection,
238
+ disabled: !selectedUid,
239
+ children: [
240
+ "Export All ",
241
+ selectedCol?.displayName || "Collection",
242
+ " Records (.csv)"
243
+ ]
244
+ }
245
+ ),
246
+ rules.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { marginLeft: "auto" }, children: [
247
+ /* @__PURE__ */ jsx(
248
+ SingleSelect,
249
+ {
250
+ value: selectedRuleId,
251
+ onChange: (val) => setSelectedRuleId(val),
252
+ placeholder: "Select Quota Rule...",
253
+ children: rules.map((r) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: r.id, children: [
254
+ "Rule: ",
255
+ r.name
256
+ ] }, r.id))
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsx(
260
+ Button,
261
+ {
262
+ variant: "danger-light",
263
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
264
+ onClick: handleExportViolations,
265
+ disabled: !selectedRuleId,
266
+ children: "Export Violators CSV"
267
+ }
268
+ )
269
+ ] })
270
+ ] })
271
+ ] }),
272
+ /* @__PURE__ */ jsxs(Box, { padding: 5, background: "neutral100", hasRadius: true, children: [
273
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h3", fontWeight: "bold", marginBottom: 2, children: "2. Import & Batch Override CSV" }),
274
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", marginBottom: 4, children: "Upload an updated CSV file to bulk update existing records or insert new records." }),
275
+ /* @__PURE__ */ jsxs(Flex, { gap: 6, marginBottom: 4, wrap: "wrap", children: [
276
+ /* @__PURE__ */ jsxs(Box, { children: [
277
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Operation Mode" }),
278
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
279
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
280
+ /* @__PURE__ */ jsx(
281
+ "input",
282
+ {
283
+ type: "radio",
284
+ name: "importMode",
285
+ value: "update",
286
+ checked: importMode === "update",
287
+ onChange: () => setImportMode("update")
288
+ }
289
+ ),
290
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: importMode === "update" ? "bold" : "normal", children: "Update Existing (Matches by documentId)" })
291
+ ] }),
292
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
293
+ /* @__PURE__ */ jsx(
294
+ "input",
295
+ {
296
+ type: "radio",
297
+ name: "importMode",
298
+ value: "create_or_override",
299
+ checked: importMode === "create_or_override",
300
+ onChange: () => setImportMode("create_or_override")
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: importMode === "create_or_override" ? "bold" : "normal", children: "Add / Override (Inserts new or replaces existing)" })
304
+ ] })
305
+ ] })
306
+ ] }),
307
+ /* @__PURE__ */ jsxs(Box, { style: { marginLeft: "auto" }, children: [
308
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Publish Options" }),
309
+ /* @__PURE__ */ jsx(
310
+ Checkbox,
311
+ {
312
+ checked: publishOnImport,
313
+ onValueChange: (val) => setPublishOnImport(val),
314
+ children: "Auto-publish on import"
315
+ }
316
+ )
317
+ ] })
318
+ ] }),
319
+ /* @__PURE__ */ jsxs(
320
+ Box,
321
+ {
322
+ padding: 6,
323
+ background: "neutral0",
324
+ hasRadius: true,
325
+ style: {
326
+ border: "2px dashed #dcdce4",
327
+ textAlign: "center",
328
+ cursor: "pointer"
329
+ },
330
+ children: [
331
+ /* @__PURE__ */ jsx(
332
+ "input",
333
+ {
334
+ type: "file",
335
+ accept: ".csv",
336
+ id: "csv-file-input",
337
+ style: { display: "none" },
338
+ onChange: handleFileUpload
339
+ }
340
+ ),
341
+ /* @__PURE__ */ jsxs("label", { htmlFor: "csv-file-input", style: { cursor: "pointer", display: "block" }, children: [
342
+ /* @__PURE__ */ jsx(Upload, { style: { width: 32, height: 32, margin: "0 auto 8px", color: "#4945ff" } }),
343
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", display: "block", children: fileName ? `Selected: ${fileName}` : "Click here or drag a .csv file to upload" }),
344
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "UTF-8 encoded CSV matching collection schema headers" })
345
+ ] })
346
+ ]
347
+ }
348
+ ),
349
+ parsedPreview.length > 0 && /* @__PURE__ */ jsxs(Box, { marginTop: 4, background: "neutral0", padding: 4, hasRadius: true, children: [
350
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, children: [
351
+ /* @__PURE__ */ jsxs(Typography, { variant: "sigma", textColor: "neutral700", children: [
352
+ "FILE PREVIEW (FIRST ",
353
+ parsedPreview.length,
354
+ " ROWS)"
355
+ ] }),
356
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral800", children: "Ready to Import" })
357
+ ] }),
358
+ /* @__PURE__ */ jsxs(Table, { colCount: Object.keys(parsedPreview[0]).length, rowCount: parsedPreview.length + 1, children: [
359
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsx(Tr, { children: Object.keys(parsedPreview[0]).map((h) => /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: h }) }, h)) }) }),
360
+ /* @__PURE__ */ jsx(Tbody, { children: parsedPreview.map((row, rIdx) => /* @__PURE__ */ jsx(Tr, { children: Object.keys(parsedPreview[0]).map((h) => /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "omega", children: row[h] || "-" }) }, h)) }, rIdx)) })
361
+ ] }),
362
+ /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", marginTop: 4, children: /* @__PURE__ */ jsx(
363
+ Button,
364
+ {
365
+ variant: "default",
366
+ size: "L",
367
+ startIcon: /* @__PURE__ */ jsx(Check, {}),
368
+ onClick: handleExecuteImport,
369
+ loading: importing,
370
+ children: importMode === "update" ? "Execute Bulk Update" : "Execute Add / Override"
371
+ }
372
+ ) })
373
+ ] }),
374
+ importResult && /* @__PURE__ */ jsx(Box, { marginTop: 4, children: /* @__PURE__ */ jsxs(
375
+ Alert,
376
+ {
377
+ variant: importResult.stats.errors.length > 0 ? "warning" : "success",
378
+ title: "CSV Import Results",
379
+ children: [
380
+ /* @__PURE__ */ jsxs(Typography, { variant: "omega", display: "block", marginBottom: 1, children: [
381
+ "Successfully processed: ",
382
+ /* @__PURE__ */ jsxs("strong", { children: [
383
+ importResult.stats.created,
384
+ " created"
385
+ ] }),
386
+ ",",
387
+ " ",
388
+ /* @__PURE__ */ jsxs("strong", { children: [
389
+ importResult.stats.updated,
390
+ " updated"
391
+ ] }),
392
+ ",",
393
+ " ",
394
+ /* @__PURE__ */ jsxs("strong", { children: [
395
+ importResult.stats.skipped,
396
+ " skipped"
397
+ ] }),
398
+ " out of ",
399
+ importResult.stats.total,
400
+ " total rows."
401
+ ] }),
402
+ importResult.stats.errors.length > 0 && /* @__PURE__ */ jsxs(Box, { marginTop: 2, children: [
403
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: [
404
+ "Errors encountered (",
405
+ importResult.stats.errors.length,
406
+ "):"
407
+ ] }),
408
+ /* @__PURE__ */ jsx("ul", { style: { margin: "4px 0 0 16px", padding: 0 }, children: importResult.stats.errors.slice(0, 5).map((err, idx) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
409
+ "Row ",
410
+ err.row,
411
+ ": ",
412
+ err.error
413
+ ] }) }, idx)) })
414
+ ] })
415
+ ]
416
+ }
417
+ ) })
418
+ ] })
419
+ ] }) });
420
+ }
421
+ function getNestedValue(obj, path) {
422
+ if (!obj || !path) return void 0;
423
+ if (!path.includes(".")) return obj[path];
424
+ return path.split(".").reduce((acc, part) => acc && typeof acc === "object" ? acc[part] : void 0, obj);
425
+ }
6
426
  function getAvailableColumnsForCollection(col, selectedAttribute) {
7
427
  const cols = [
8
428
  {
@@ -23,7 +443,7 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
23
443
  if (attr === "name" || attr === "title" || attr === selectedAttribute) {
24
444
  return;
25
445
  }
26
- const formattedLabel = attr.charAt(0).toUpperCase() + attr.slice(1);
446
+ const formattedLabel = attr.includes(".") ? attr.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" > ") : attr.charAt(0).toUpperCase() + attr.slice(1);
27
447
  cols.push({
28
448
  key: `attr_${attr}`,
29
449
  label: formattedLabel,
@@ -109,7 +529,7 @@ function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAt
109
529
  ] });
110
530
  }
111
531
  case "target_attr": {
112
- const val = entry[selectedAttribute];
532
+ const val = getNestedValue(entry, selectedAttribute);
113
533
  const displayStr = formatDisplayValue(val);
114
534
  const isUnset = displayStr === "(empty / not set)" || displayStr === "(empty / unlinked)";
115
535
  return /* @__PURE__ */ jsx(
@@ -139,7 +559,7 @@ function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAt
139
559
  }
140
560
  default: {
141
561
  const attrName = colDef.key.replace("attr_", "");
142
- const rawVal = entry[attrName];
562
+ const rawVal = getNestedValue(entry, attrName);
143
563
  const displayStr = formatDisplayValue(rawVal);
144
564
  return /* @__PURE__ */ jsx(
145
565
  Typography,
@@ -159,7 +579,7 @@ const BulkUpdater = () => {
159
579
  try {
160
580
  const params = new URLSearchParams(window.location.search);
161
581
  const t = params.get("tab");
162
- if (t === "audit" || t === "editor") return t;
582
+ if (t === "audit" || t === "editor" || t === "rules" || t === "csv") return t;
163
583
  } catch {
164
584
  }
165
585
  return "editor";
@@ -178,7 +598,6 @@ const BulkUpdater = () => {
178
598
  const [updating, setUpdating] = useState(false);
179
599
  const [attributes, setAttributes] = useState([]);
180
600
  const [collections, setCollections] = useState([]);
181
- const [license, setLicense] = useState(null);
182
601
  const [auditEnabled, setAuditEnabled] = useState(false);
183
602
  const [executionWindow, setExecutionWindow] = useState(null);
184
603
  const [showConfirmModal, setShowConfirmModal] = useState(false);
@@ -214,7 +633,8 @@ const BulkUpdater = () => {
214
633
  const [regexPattern, setRegexPattern] = useState("");
215
634
  const [filterOperator, setFilterOperator] = useState("none");
216
635
  const [filterQuery, setFilterQuery] = useState("");
217
- const [hideNonMatching, setHideNonMatching] = useState(false);
636
+ const [entrySearch, setEntrySearch] = useState("");
637
+ const isFilterActive = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
218
638
  const [lastResults, setLastResults] = useState(null);
219
639
  const [updateSummary, setUpdateSummary] = useState(null);
220
640
  const [collectionPages, setCollectionPages] = useState({});
@@ -266,7 +686,6 @@ const BulkUpdater = () => {
266
686
  setAttributes(res.data.attributes || []);
267
687
  const cols = res.data.collections || [];
268
688
  setCollections(cols);
269
- setLicense(res.data.license || null);
270
689
  setAuditEnabled(Boolean(res.data.auditEnabled));
271
690
  if (res.data.executionWindow) {
272
691
  setExecutionWindow(res.data.executionWindow);
@@ -311,12 +730,15 @@ const BulkUpdater = () => {
311
730
  setLoading(false);
312
731
  }
313
732
  };
314
- const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder) => {
733
+ const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder, overrideFilterOp, overrideFilterQuery, overrideEntrySearch, autoSelectReturned = true) => {
315
734
  const currentPageState = collectionPages[colUid];
316
735
  const size = customPageSize || currentPageState?.pageSize || defaultPageSize;
317
736
  const currentSort = collectionSorts[colUid] || { field: "createdAt", order: "desc" };
318
737
  const sortField = customSortField !== void 0 ? customSortField : currentSort.field;
319
738
  const sortOrder = customSortOrder !== void 0 ? customSortOrder : currentSort.order;
739
+ const op = overrideFilterOp !== void 0 ? overrideFilterOp : filterOperator;
740
+ const q = overrideFilterQuery !== void 0 ? overrideFilterQuery : filterQuery;
741
+ const s = overrideEntrySearch !== void 0 ? overrideEntrySearch : entrySearch;
320
742
  setCollectionPages((prev) => ({
321
743
  ...prev,
322
744
  [colUid]: {
@@ -330,16 +752,20 @@ const BulkUpdater = () => {
330
752
  pageSize: String(size)
331
753
  });
332
754
  if (selectedAttribute) params.set("attribute", selectedAttribute);
333
- if (filterOperator && filterOperator !== "none") {
334
- params.set("filterOperator", filterOperator);
335
- if (filterQuery) params.set("filterQuery", filterQuery);
755
+ if (op && op !== "none") {
756
+ params.set("filterOperator", op);
757
+ if (q) params.set("filterQuery", q);
758
+ }
759
+ if (s && s.trim()) {
760
+ params.set("search", s.trim());
336
761
  }
337
762
  if (sortField) {
338
763
  params.set("sortField", sortField);
339
764
  params.set("sortOrder", sortOrder);
340
765
  }
341
766
  const res = await get(`/bulk-updater/collections/${colUid}/entries?${params.toString()}`);
342
- if (res.data && res.data.success) {
767
+ if (res.data && (res.data.success || res.data.entries !== void 0)) {
768
+ const fetchedEntries = res.data.entries || [];
343
769
  setCollectionPages((prev) => ({
344
770
  ...prev,
345
771
  [colUid]: {
@@ -347,10 +773,20 @@ const BulkUpdater = () => {
347
773
  pageSize: res.data.pagination.pageSize,
348
774
  total: res.data.pagination.total,
349
775
  pageCount: res.data.pagination.pageCount,
350
- entries: res.data.entries || [],
776
+ entries: fetchedEntries,
351
777
  loading: false
352
778
  }
353
779
  }));
780
+ const isFilterActiveNow = op !== "none" && (["empty", "not_empty"].includes(op) || Boolean(q.trim())) || Boolean(s.trim());
781
+ if (isFilterActiveNow && autoSelectReturned) {
782
+ setSelectedEntries((prev) => {
783
+ const next = { ...prev };
784
+ fetchedEntries.forEach((e) => {
785
+ next[`${colUid}${KEY_SEP}${e.documentId}`] = true;
786
+ });
787
+ return next;
788
+ });
789
+ }
354
790
  }
355
791
  } catch (err) {
356
792
  toggleNotification({
@@ -414,6 +850,35 @@ const BulkUpdater = () => {
414
850
  fetchPendingBatches();
415
851
  }
416
852
  }, [activeTab]);
853
+ const isInitialFilterMount = useRef(true);
854
+ useEffect(() => {
855
+ if (isInitialFilterMount.current) {
856
+ isInitialFilterMount.current = false;
857
+ return;
858
+ }
859
+ const timer = setTimeout(() => {
860
+ if (collections.length > 0) {
861
+ const isFilterActiveNow = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
862
+ if (isFilterActiveNow) {
863
+ setSelectedEntries({});
864
+ }
865
+ collections.forEach((col) => {
866
+ loadCollectionPage(
867
+ col.uid,
868
+ 1,
869
+ void 0,
870
+ void 0,
871
+ void 0,
872
+ filterOperator,
873
+ filterQuery,
874
+ entrySearch,
875
+ true
876
+ );
877
+ });
878
+ }
879
+ }, 350);
880
+ return () => clearTimeout(timer);
881
+ }, [filterOperator, filterQuery, entrySearch]);
417
882
  const KEY_SEP = "@@@";
418
883
  const handleAttributeChange = (newAttr) => {
419
884
  if (newAttr !== selectedAttribute) {
@@ -421,7 +886,7 @@ const BulkUpdater = () => {
421
886
  setSelectedEntries({});
422
887
  setFilterOperator("none");
423
888
  setFilterQuery("");
424
- setHideNonMatching(false);
889
+ setEntrySearch("");
425
890
  setSelectedRelationDocId("");
426
891
  setAttributeValue("");
427
892
  if (relationsMeta[newAttr]) {
@@ -438,7 +903,7 @@ const BulkUpdater = () => {
438
903
  (c) => visibleTypeUids[c.uid] !== false && (!collectionSearch.trim() || c.displayName.toLowerCase().includes(collectionSearch.toLowerCase().trim()) || c.uid.toLowerCase().includes(collectionSearch.toLowerCase().trim()))
439
904
  );
440
905
  const handleGlobalPageSizeChange = (newSize) => {
441
- const capped = Math.min(100, Math.max(1, newSize));
906
+ const capped = Math.min(500, Math.max(1, newSize));
442
907
  setDefaultPageSize(capped);
443
908
  collections.forEach((col) => {
444
909
  if (!col.isSingleType) {
@@ -447,7 +912,7 @@ const BulkUpdater = () => {
447
912
  });
448
913
  };
449
914
  const handleCollectionPageSizeChange = (colUid, newSize) => {
450
- const capped = Math.min(100, Math.max(1, newSize));
915
+ const capped = Math.min(500, Math.max(1, newSize));
451
916
  loadCollectionPage(colUid, 1, capped);
452
917
  };
453
918
  const handleCollectionSortChange = (colUid, field, order) => {
@@ -471,7 +936,7 @@ const BulkUpdater = () => {
471
936
  };
472
937
  const entryMatchesFilter = (entry, attr) => {
473
938
  if (filterOperator === "none") return true;
474
- const val = entry[attr];
939
+ const val = getNestedValue(entry, attr);
475
940
  const displayStr = formatDisplayValue(val);
476
941
  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
942
  switch (filterOperator) {
@@ -491,64 +956,48 @@ const BulkUpdater = () => {
491
956
  };
492
957
  const getVisibleEntries = (col) => {
493
958
  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));
959
+ return pageState ? pageState.entries : col.entries || [];
499
960
  };
500
- const matchingEntriesCount = React.useMemo(() => {
501
- if (filterOperator === "none") return 0;
961
+ const matchingEntriesCount = React__default.useMemo(() => {
962
+ if (!isFilterActive) return 0;
502
963
  let count = 0;
503
964
  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
- });
965
+ const pageState = collectionPages[col.uid];
966
+ if (pageState) {
967
+ count += pageState.total;
512
968
  }
513
969
  });
514
970
  return count;
515
- }, [filteredCollections, collectionPages, selectedAttribute, filterOperator, filterQuery]);
971
+ }, [filteredCollections, collectionPages, isFilterActive]);
516
972
  const selectAllMatching = () => {
517
973
  const updated = { ...selectedEntries };
518
974
  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
- }
975
+ const pageState = collectionPages[col.uid];
976
+ const entries = pageState ? pageState.entries : col.entries || [];
977
+ entries.forEach((e) => {
978
+ updated[`${col.uid}${KEY_SEP}${e.documentId}`] = true;
979
+ });
528
980
  });
529
981
  setSelectedEntries(updated);
530
982
  };
531
983
  const deselectAllMatching = () => {
532
984
  const updated = { ...selectedEntries };
533
985
  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
- }
986
+ const pageState = collectionPages[col.uid];
987
+ const entries = pageState ? pageState.entries : col.entries || [];
988
+ entries.forEach((e) => {
989
+ delete updated[`${col.uid}${KEY_SEP}${e.documentId}`];
990
+ });
543
991
  });
544
992
  setSelectedEntries(updated);
545
993
  };
546
994
  const clearFilter = () => {
547
995
  setFilterOperator("none");
548
996
  setFilterQuery("");
549
- setHideNonMatching(false);
997
+ setEntrySearch("");
998
+ setSelectedEntries({});
550
999
  collections.forEach((col) => {
551
- loadCollectionPage(col.uid, 1);
1000
+ loadCollectionPage(col.uid, 1, void 0, void 0, void 0, "none", "", "", false);
552
1001
  });
553
1002
  };
554
1003
  const isCollectionFullySelected = (col) => {
@@ -614,13 +1063,6 @@ const BulkUpdater = () => {
614
1063
  });
615
1064
  return;
616
1065
  }
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
1066
  const totalSelected = Object.values(selectedEntries).filter(Boolean).length;
625
1067
  if (totalSelected === 0) {
626
1068
  toggleNotification({
@@ -677,6 +1119,7 @@ const BulkUpdater = () => {
677
1119
  message: `Bulk update complete! Batch ID: ${res.data.batchId}. Updated: ${res.data.updatedCount}, Skipped: ${res.data.skippedCount}`
678
1120
  });
679
1121
  await fetchModels();
1122
+ await fetchAuditHistory();
680
1123
  }
681
1124
  } else {
682
1125
  toggleNotification({
@@ -804,7 +1247,7 @@ const BulkUpdater = () => {
804
1247
  }
805
1248
  };
806
1249
  const totalSelectedCount = Object.values(selectedEntries).filter(Boolean).length;
807
- const rootRef = React.useRef(null);
1250
+ const rootRef = React__default.useRef(null);
808
1251
  useEffect(() => {
809
1252
  if (rootRef.current && rootRef.current.parentElement) {
810
1253
  rootRef.current.parentElement.style.minWidth = "0";
@@ -877,14 +1320,6 @@ const BulkUpdater = () => {
877
1320
  /* @__PURE__ */ jsxs(Box, { children: [
878
1321
  /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
879
1322
  /* @__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
1323
  auditEnabled && /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "AUDIT & ROLLBACK ACTIVE" })
889
1324
  ] }),
890
1325
  /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: "Cross-collection batch attribute management with declarative audit trail and one-click rollback." })
@@ -925,6 +1360,24 @@ const BulkUpdater = () => {
925
1360
  ] }) : auditHistory.length > 0 ? ` (${auditHistory.length})` : ""
926
1361
  ]
927
1362
  }
1363
+ ),
1364
+ /* @__PURE__ */ jsx(
1365
+ Button,
1366
+ {
1367
+ variant: activeTab === "rules" ? "default" : "tertiary",
1368
+ onClick: () => setActiveTab("rules"),
1369
+ size: "M",
1370
+ children: "Validation & Quota Rules"
1371
+ }
1372
+ ),
1373
+ /* @__PURE__ */ jsx(
1374
+ Button,
1375
+ {
1376
+ variant: activeTab === "csv" ? "default" : "tertiary",
1377
+ onClick: () => setActiveTab("csv"),
1378
+ size: "M",
1379
+ children: "CSV Import / Export & Override"
1380
+ }
928
1381
  )
929
1382
  ] }),
930
1383
  executionWindow && /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsx(
@@ -953,7 +1406,7 @@ const BulkUpdater = () => {
953
1406
  /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral700", children: [
954
1407
  /* @__PURE__ */ jsx("code", { children: latestActiveBatch.batchId }),
955
1408
  " (",
956
- latestActiveBatch.recordCount,
1409
+ latestActiveBatch.updatedCount,
957
1410
  " entries updated by ",
958
1411
  latestActiveBatch.userEmail,
959
1412
  ")"
@@ -1104,38 +1557,24 @@ const BulkUpdater = () => {
1104
1557
  }
1105
1558
  ) })
1106
1559
  ] }),
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
- ] }),
1560
+ /* @__PURE__ */ jsx(Box, { marginTop: 4, padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1561
+ /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsx(
1562
+ Checkbox,
1563
+ {
1564
+ checked: useRegex,
1565
+ onCheckedChange: (checked) => setUseRegex(Boolean(checked)),
1566
+ children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: "Enable Regex Find & Replace" })
1567
+ }
1568
+ ) }),
1569
+ useRegex && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsx(
1570
+ TextInput,
1571
+ {
1572
+ placeholder: "Regex pattern (e.g. \\b(OldName)\\b)...",
1573
+ value: regexPattern,
1574
+ onChange: (e) => setRegexPattern(e.target.value)
1575
+ }
1576
+ ) })
1577
+ ] }) }),
1139
1578
  selectedAttribute && /* @__PURE__ */ jsx(Box, { marginTop: 4, padding: 3, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
1140
1579
  /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "Attribute Info:" }),
1141
1580
  /* @__PURE__ */ jsxs(Typography, { variant: "pi", children: [
@@ -1194,24 +1633,25 @@ const BulkUpdater = () => {
1194
1633
  children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
1195
1634
  /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, children: [
1196
1635
  /* @__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: [
1636
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: "Search & Filter Targets:" }),
1637
+ isFilterActive && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
1199
1638
  matchingEntriesCount,
1200
1639
  " matching ",
1201
- matchingEntriesCount === 1 ? "entry" : "entries"
1640
+ matchingEntriesCount === 1 ? "entry" : "entries",
1641
+ " found in database"
1202
1642
  ] })
1203
1643
  ] }),
1204
- filterOperator !== "none" && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Filter" })
1644
+ isFilterActive && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Search & Filter" })
1205
1645
  ] }),
1206
1646
  /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-end", wrap: "wrap", children: [
1207
- /* @__PURE__ */ jsx(Box, { style: { width: 240, minWidth: 200 }, children: /* @__PURE__ */ jsxs(
1647
+ /* @__PURE__ */ jsx(Box, { style: { width: 220, minWidth: 180 }, children: /* @__PURE__ */ jsxs(
1208
1648
  SingleSelect,
1209
1649
  {
1210
1650
  label: `Filter condition for "${selectedAttribute}"`,
1211
1651
  value: filterOperator,
1212
1652
  onChange: (val) => setFilterOperator(String(val)),
1213
1653
  children: [
1214
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "none", children: "Show All (No Filter)" }),
1654
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "none", children: "Show All (No Attribute Filter)" }),
1215
1655
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "empty", children: "Is Empty or Null" }),
1216
1656
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "not_empty", children: "Is Not Empty" }),
1217
1657
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "equals", children: "Equals Exact Text" }),
@@ -1220,28 +1660,33 @@ const BulkUpdater = () => {
1220
1660
  ]
1221
1661
  }
1222
1662
  ) }),
1223
- (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 220 }, children: /* @__PURE__ */ jsx(
1663
+ (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsx(
1224
1664
  TextInput,
1225
1665
  {
1226
- label: "Value to Search",
1666
+ label: `Value for "${selectedAttribute}"`,
1227
1667
  placeholder: "Type value to match...",
1228
1668
  value: filterQuery,
1229
1669
  onChange: (e) => setFilterQuery(e.target.value)
1230
1670
  }
1231
1671
  ) }),
1232
- filterOperator !== "none" && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1233
- /* @__PURE__ */ jsxs(
1672
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsx(
1673
+ TextInput,
1674
+ {
1675
+ label: "Full-text Entry Search",
1676
+ placeholder: "Search title, name, or text across records...",
1677
+ value: entrySearch,
1678
+ onChange: (e) => setEntrySearch(e.target.value)
1679
+ }
1680
+ ) }),
1681
+ isFilterActive && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1682
+ /* @__PURE__ */ jsx(
1234
1683
  Button,
1235
1684
  {
1236
1685
  variant: "default",
1237
1686
  size: "S",
1238
1687
  onClick: selectAllMatching,
1239
1688
  disabled: matchingEntriesCount === 0,
1240
- children: [
1241
- "Select Matching (",
1242
- matchingEntriesCount,
1243
- ")"
1244
- ]
1689
+ children: "Select All Loaded"
1245
1690
  }
1246
1691
  ),
1247
1692
  /* @__PURE__ */ jsx(
@@ -1251,19 +1696,12 @@ const BulkUpdater = () => {
1251
1696
  size: "S",
1252
1697
  onClick: deselectAllMatching,
1253
1698
  disabled: matchingEntriesCount === 0,
1254
- children: "Deselect Matching"
1699
+ children: "Deselect All Loaded"
1255
1700
  }
1256
1701
  )
1257
1702
  ] })
1258
1703
  ] }),
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
- ) })
1704
+ isFilterActive && /* @__PURE__ */ jsx(Flex, { gap: 2, alignItems: "center", marginTop: 1, children: /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", size: "S", children: "✓ Filter active: Showing only matching entries. All returned matching records are pre-selected." }) })
1267
1705
  ] })
1268
1706
  }
1269
1707
  ),
@@ -1357,8 +1795,8 @@ const BulkUpdater = () => {
1357
1795
  }
1358
1796
  ) }),
1359
1797
  /* @__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(
1798
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Default page size (max 500):" }),
1799
+ /* @__PURE__ */ jsx(Box, { style: { width: 130 }, children: /* @__PURE__ */ jsxs(
1362
1800
  SingleSelect,
1363
1801
  {
1364
1802
  size: "S",
@@ -1369,7 +1807,9 @@ const BulkUpdater = () => {
1369
1807
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "10", children: "10 / page" }),
1370
1808
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "25", children: "25 / page" }),
1371
1809
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "50", children: "50 / page" }),
1372
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100 / page" })
1810
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100 / page" }),
1811
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "250", children: "250 / page" }),
1812
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "500", children: "500 / page" })
1373
1813
  ]
1374
1814
  }
1375
1815
  ) })
@@ -1384,7 +1824,7 @@ const BulkUpdater = () => {
1384
1824
  const pageSize = pageState ? pageState.pageSize : defaultPageSize;
1385
1825
  const hasAttr = col.attributes.includes(selectedAttribute);
1386
1826
  const visibleEntries = getVisibleEntries(col);
1387
- const matchingColCount = entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length;
1827
+ entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length;
1388
1828
  const isFullySelected = isCollectionFullySelected(col);
1389
1829
  const isPartiallySelected = isCollectionPartiallySelected(col);
1390
1830
  const currentSort = collectionSorts[col.uid] || { field: "createdAt", order: "desc" };
@@ -1479,7 +1919,56 @@ const BulkUpdater = () => {
1479
1919
  ]
1480
1920
  }
1481
1921
  ),
1482
- /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: col.isSingleType ? "1 single entry" : filterOperator !== "none" ? `${matchingColCount} matching of ${totalEntries} entries` : `${totalEntries} entries` })
1922
+ /* @__PURE__ */ jsx(
1923
+ Button,
1924
+ {
1925
+ variant: "tertiary",
1926
+ size: "S",
1927
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
1928
+ onClick: async () => {
1929
+ try {
1930
+ const params = new URLSearchParams();
1931
+ if (selectedAttribute) params.set("attribute", selectedAttribute);
1932
+ if (filterOperator && filterOperator !== "none") {
1933
+ params.set("filterOperator", filterOperator);
1934
+ if (filterQuery) params.set("filterQuery", filterQuery);
1935
+ }
1936
+ if (entrySearch && entrySearch.trim()) {
1937
+ params.set("search", entrySearch.trim());
1938
+ }
1939
+ const res = await get(`/bulk-updater/csv/export/${col.uid}?${params.toString()}`);
1940
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
1941
+ if (!csvStr) {
1942
+ toggleNotification({
1943
+ type: "warning",
1944
+ message: "No CSV content returned for this collection"
1945
+ });
1946
+ return;
1947
+ }
1948
+ const blob = new Blob([csvStr], { type: "text/csv;charset=utf-8;" });
1949
+ const url = URL.createObjectURL(blob);
1950
+ const link = document.createElement("a");
1951
+ link.href = url;
1952
+ link.setAttribute("download", `${col.singularName || "export"}_entries.csv`);
1953
+ document.body.appendChild(link);
1954
+ link.click();
1955
+ document.body.removeChild(link);
1956
+ URL.revokeObjectURL(url);
1957
+ toggleNotification({
1958
+ type: "success",
1959
+ message: `Exported ${col.displayName} entries to CSV`
1960
+ });
1961
+ } catch (e) {
1962
+ toggleNotification({
1963
+ type: "danger",
1964
+ message: e.message || "Failed exporting CSV"
1965
+ });
1966
+ }
1967
+ },
1968
+ children: "Export CSV"
1969
+ }
1970
+ ),
1971
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: col.isSingleType ? "1 single entry" : isFilterActive ? `${totalEntries} matching entries in DB` : `${totalEntries} entries` })
1483
1972
  ] })
1484
1973
  ] }),
1485
1974
  openColumnPickerUid === col.uid && /* @__PURE__ */ jsx(
@@ -1549,7 +2038,7 @@ const BulkUpdater = () => {
1549
2038
  /* @__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
2039
  const key = `${col.uid}${KEY_SEP}${entry.documentId}`;
1551
2040
  const isSelected = Boolean(selectedEntries[key]);
1552
- const isMatch = filterOperator !== "none" && entryMatchesFilter(entry, selectedAttribute);
2041
+ const isMatch = isFilterActive;
1553
2042
  return /* @__PURE__ */ jsx(
1554
2043
  Tr,
1555
2044
  {
@@ -1581,11 +2070,12 @@ const BulkUpdater = () => {
1581
2070
  Math.min(page * pageSize, totalEntries),
1582
2071
  " of ",
1583
2072
  totalEntries,
1584
- " entries"
2073
+ " ",
2074
+ isFilterActive ? "matching entries" : "entries"
1585
2075
  ] }),
1586
2076
  /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
1587
2077
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Show:" }),
1588
- /* @__PURE__ */ jsx(Box, { style: { width: 80 }, children: /* @__PURE__ */ jsxs(
2078
+ /* @__PURE__ */ jsx(Box, { style: { width: 85 }, children: /* @__PURE__ */ jsxs(
1589
2079
  SingleSelect,
1590
2080
  {
1591
2081
  size: "S",
@@ -1596,12 +2086,27 @@ const BulkUpdater = () => {
1596
2086
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "10", children: "10" }),
1597
2087
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "25", children: "25" }),
1598
2088
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "50", children: "50" }),
1599
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100" })
2089
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "100", children: "100" }),
2090
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "250", children: "250" }),
2091
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "500", children: "500" })
1600
2092
  ]
1601
2093
  }
1602
2094
  ) }),
1603
2095
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "per page" })
1604
- ] })
2096
+ ] }),
2097
+ totalEntries > visibleEntries.length && /* @__PURE__ */ jsxs(
2098
+ Button,
2099
+ {
2100
+ variant: "tertiary",
2101
+ size: "S",
2102
+ onClick: () => handleCollectionPageSizeChange(col.uid, Math.min(totalEntries, 500)),
2103
+ children: [
2104
+ "Load all ",
2105
+ totalEntries,
2106
+ " entries"
2107
+ ]
2108
+ }
2109
+ )
1605
2110
  ] }),
1606
2111
  pageCount > 1 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1607
2112
  /* @__PURE__ */ jsx(
@@ -1884,7 +2389,7 @@ const BulkUpdater = () => {
1884
2389
  /* @__PURE__ */ jsx(Tbody, { children: auditHistory.map((rec) => {
1885
2390
  const isExpanded = expandedDiffBatchId === rec.batchId;
1886
2391
  const dateStr = new Date(rec.executedAt).toLocaleString();
1887
- return /* @__PURE__ */ jsxs(React.Fragment, { children: [
2392
+ return /* @__PURE__ */ jsxs(React__default.Fragment, { children: [
1888
2393
  /* @__PURE__ */ jsxs(
1889
2394
  Tr,
1890
2395
  {
@@ -2005,6 +2510,8 @@ const BulkUpdater = () => {
2005
2510
  ]
2006
2511
  }
2007
2512
  ),
2513
+ activeTab === "rules" && /* @__PURE__ */ jsx(ValidationRulesTab, {}),
2514
+ activeTab === "csv" && /* @__PURE__ */ jsx(CsvManagerTab, {}),
2008
2515
  showConfirmModal && /* @__PURE__ */ jsx(
2009
2516
  "div",
2010
2517
  {
@@ -2049,20 +2556,10 @@ const BulkUpdater = () => {
2049
2556
  borderBottom: "1px solid rgba(120, 120, 150, 0.2)",
2050
2557
  flexShrink: 0
2051
2558
  },
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
- ] })
2559
+ children: /* @__PURE__ */ jsx(Flex, { justifyContent: "space-between", alignItems: "center", children: /* @__PURE__ */ jsxs(Box, { children: [
2560
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Confirm Bulk Update" }),
2561
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", display: "block", children: "Review target attributes, affected counts, and server execution policy before proceeding." })
2562
+ ] }) })
2066
2563
  }
2067
2564
  ),
2068
2565
  /* @__PURE__ */ jsxs(
@@ -2206,7 +2703,7 @@ const BulkUpdater = () => {
2206
2703
  children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 3, style: { width: "100%" }, children: [
2207
2704
  /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "M", onClick: () => setShowConfirmModal(false), children: "Cancel" }),
2208
2705
  /* @__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(
2706
+ /* @__PURE__ */ jsx(
2210
2707
  Button,
2211
2708
  {
2212
2709
  variant: executionWindow?.isBusinessHours ? "default" : "secondary",
@@ -2220,7 +2717,7 @@ const BulkUpdater = () => {
2220
2717
  /* @__PURE__ */ jsxs(
2221
2718
  Button,
2222
2719
  {
2223
- variant: license?.tier === "enterprise" ? "secondary" : "default",
2720
+ variant: "secondary",
2224
2721
  size: "M",
2225
2722
  onClick: () => executeBatchWithMode("queue_offpeak"),
2226
2723
  loading: updating,
@@ -2267,6 +2764,7 @@ const BulkUpdater = () => {
2267
2764
  export {
2268
2765
  BulkUpdater as default,
2269
2766
  formatDisplayValue,
2270
- getAvailableColumnsForCollection
2767
+ getAvailableColumnsForCollection,
2768
+ getNestedValue
2271
2769
  };
2272
- //# sourceMappingURL=BulkUpdater-R78pX-om.mjs.map
2770
+ //# sourceMappingURL=BulkUpdater-BDszahRM.mjs.map