strapi-plugin-bulk-updater 1.0.0 → 1.1.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 +41 -24
  2. package/dist/_chunks/{BulkUpdater-CooMo-Hc.js → BulkUpdater-BRcMQGP3.js} +507 -67
  3. package/dist/_chunks/BulkUpdater-BRcMQGP3.js.map +1 -0
  4. package/dist/_chunks/{BulkUpdater-R78pX-om.mjs → BulkUpdater-BSZP0U18.mjs} +514 -74
  5. package/dist/_chunks/BulkUpdater-BSZP0U18.mjs.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 +30491 -269
  23. package/dist/server/index.js.map +1 -1
  24. package/dist/server/index.mjs +30481 -269
  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
@@ -5,8 +5,423 @@ const React = require("react");
5
5
  const admin = require("@strapi/strapi/admin");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
+ const ValidationRulesTab = require("./ValidationRulesTab-BoqhoTjo.js");
8
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
9
10
  const React__default = /* @__PURE__ */ _interopDefault(React);
11
+ function CsvManagerTab() {
12
+ const { get, post } = admin.useFetchClient();
13
+ const { toggleNotification } = admin.useNotification();
14
+ const [collections, setCollections] = React.useState([]);
15
+ const [loadingCollections, setLoadingCollections] = React.useState(true);
16
+ const [selectedUid, setSelectedUid] = React.useState("");
17
+ const [exportLimit, setExportLimit] = React.useState(1e3);
18
+ const [exportOnlyViolations, setExportOnlyViolations] = React.useState(false);
19
+ const [rules, setRules] = React.useState([]);
20
+ const [selectedRuleId, setSelectedRuleId] = React.useState("");
21
+ const [importMode, setImportMode] = React.useState("update");
22
+ const [publishOnImport, setPublishOnImport] = React.useState(true);
23
+ const [csvRawText, setCsvRawText] = React.useState("");
24
+ const [fileName, setFileName] = React.useState("");
25
+ const [parsedPreview, setParsedPreview] = React.useState([]);
26
+ const [importing, setImporting] = React.useState(false);
27
+ const [importResult, setImportResult] = React.useState(null);
28
+ React.useEffect(() => {
29
+ fetchInitialData();
30
+ }, []);
31
+ const fetchInitialData = async () => {
32
+ try {
33
+ setLoadingCollections(true);
34
+ const [modelsRes, rulesRes] = await Promise.all([
35
+ get("/bulk-updater/models"),
36
+ get("/bulk-updater/validation-rules")
37
+ ]);
38
+ if (modelsRes.data?.collections) {
39
+ setCollections(modelsRes.data.collections);
40
+ if (modelsRes.data.collections.length > 0) {
41
+ setSelectedUid(modelsRes.data.collections[0].uid);
42
+ }
43
+ }
44
+ if (rulesRes.data?.rules) {
45
+ setRules(rulesRes.data.rules);
46
+ if (rulesRes.data.rules.length > 0) {
47
+ setSelectedRuleId(rulesRes.data.rules[0].id);
48
+ }
49
+ }
50
+ } catch (err) {
51
+ toggleNotification({
52
+ type: "danger",
53
+ message: err.message || "Failed loading collections for CSV Manager"
54
+ });
55
+ } finally {
56
+ setLoadingCollections(false);
57
+ }
58
+ };
59
+ const handleDownloadTemplate = async () => {
60
+ if (!selectedUid) return;
61
+ try {
62
+ const res = await get(`/bulk-updater/csv/template/${selectedUid}`);
63
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
64
+ const filename = res.data?.filename || `${selectedUid.split(".").pop()}_template.csv`;
65
+ downloadCsvFile(csvStr, filename);
66
+ toggleNotification({
67
+ type: "success",
68
+ message: "CSV Template downloaded successfully"
69
+ });
70
+ } catch (err) {
71
+ toggleNotification({
72
+ type: "danger",
73
+ message: err.message || "Failed downloading CSV template"
74
+ });
75
+ }
76
+ };
77
+ const handleExportCollection = async () => {
78
+ if (!selectedUid) return;
79
+ try {
80
+ const res = await get(`/bulk-updater/csv/export/${selectedUid}?limit=${exportLimit}`);
81
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
82
+ const filename = res.data?.filename || `${selectedUid.split(".").pop()}_export.csv`;
83
+ downloadCsvFile(csvStr, filename);
84
+ toggleNotification({
85
+ type: "success",
86
+ message: "Collection exported to CSV successfully"
87
+ });
88
+ } catch (err) {
89
+ toggleNotification({
90
+ type: "danger",
91
+ message: err.message || "Failed exporting collection to CSV"
92
+ });
93
+ }
94
+ };
95
+ const handleExportViolations = async () => {
96
+ if (!selectedRuleId) return;
97
+ try {
98
+ const res = await get(`/bulk-updater/csv/export-violations/${selectedRuleId}`);
99
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
100
+ const filename = res.data?.filename || `violations_${selectedRuleId}.csv`;
101
+ downloadCsvFile(csvStr, filename);
102
+ toggleNotification({
103
+ type: "success",
104
+ message: "Rule violations exported to CSV successfully"
105
+ });
106
+ } catch (err) {
107
+ toggleNotification({
108
+ type: "danger",
109
+ message: err.message || "Failed exporting violations to CSV"
110
+ });
111
+ }
112
+ };
113
+ const downloadCsvFile = (content, fallbackName) => {
114
+ if (!content) {
115
+ toggleNotification({
116
+ type: "warning",
117
+ message: "No CSV content to download"
118
+ });
119
+ return;
120
+ }
121
+ const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
122
+ const url = URL.createObjectURL(blob);
123
+ const link = document.createElement("a");
124
+ link.href = url;
125
+ link.setAttribute("download", fallbackName);
126
+ document.body.appendChild(link);
127
+ link.click();
128
+ document.body.removeChild(link);
129
+ URL.revokeObjectURL(url);
130
+ };
131
+ const handleFileUpload = (e) => {
132
+ const file = e.target.files?.[0];
133
+ if (!file) return;
134
+ setFileName(file.name);
135
+ setImportResult(null);
136
+ const reader = new FileReader();
137
+ reader.onload = (event) => {
138
+ const text = event.target?.result;
139
+ setCsvRawText(text);
140
+ try {
141
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length > 0);
142
+ if (lines.length > 0) {
143
+ const headers = lines[0].split(",").map((h) => h.replace(/^"|"$/g, "").trim());
144
+ const previewRows = [];
145
+ for (let i = 1; i < Math.min(lines.length, 6); i++) {
146
+ const vals = lines[i].split(",").map((v) => v.replace(/^"|"$/g, "").trim());
147
+ const row = {};
148
+ headers.forEach((h, idx) => {
149
+ row[h] = vals[idx] || "";
150
+ });
151
+ previewRows.push(row);
152
+ }
153
+ setParsedPreview(previewRows);
154
+ }
155
+ } catch (err) {
156
+ setParsedPreview([]);
157
+ }
158
+ };
159
+ reader.readAsText(file);
160
+ };
161
+ const handleExecuteImport = async () => {
162
+ if (!selectedUid || !csvRawText) return;
163
+ try {
164
+ setImporting(true);
165
+ setImportResult(null);
166
+ const res = await post("/bulk-updater/csv/import", {
167
+ uid: selectedUid,
168
+ csvText: csvRawText,
169
+ mode: importMode,
170
+ publish: publishOnImport
171
+ });
172
+ if (res.data && res.data.success) {
173
+ setImportResult(res.data);
174
+ toggleNotification({
175
+ type: "success",
176
+ message: `CSV Import complete: ${res.data.stats.created} created, ${res.data.stats.updated} updated.`
177
+ });
178
+ } else {
179
+ throw new Error(res.data?.error || "Import failed");
180
+ }
181
+ } catch (err) {
182
+ toggleNotification({
183
+ type: "danger",
184
+ message: err.message || "Error processing CSV import"
185
+ });
186
+ } finally {
187
+ setImporting(false);
188
+ }
189
+ };
190
+ const selectedCol = collections.find((c) => c.uid === selectedUid);
191
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, background: "neutral0", hasRadius: true, shadow: "filterShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
192
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
193
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
194
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", fontWeight: "bold", children: "CSV Export & Import Center" }),
195
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.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." })
196
+ ] }),
197
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowClockwise, {}), onClick: fetchInitialData, children: "Refresh" })
198
+ ] }),
199
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, alignItems: "flex-end", children: [
200
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { flex: 1 }, children: [
201
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Target Collection Type" }),
202
+ /* @__PURE__ */ jsxRuntime.jsx(
203
+ designSystem.SingleSelect,
204
+ {
205
+ value: selectedUid,
206
+ onChange: (val) => {
207
+ setSelectedUid(val);
208
+ setImportResult(null);
209
+ },
210
+ disabled: loadingCollections,
211
+ placeholder: "Select a collection...",
212
+ children: collections.map((col) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.SingleSelectOption, { value: col.uid, children: [
213
+ col.displayName,
214
+ " (",
215
+ col.totalEntries ?? 0,
216
+ " records)"
217
+ ] }, col.uid))
218
+ }
219
+ )
220
+ ] }),
221
+ /* @__PURE__ */ jsxRuntime.jsx(
222
+ designSystem.Button,
223
+ {
224
+ variant: "tertiary",
225
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
226
+ onClick: handleDownloadTemplate,
227
+ disabled: !selectedUid,
228
+ children: "Download Sample CSV Template"
229
+ }
230
+ )
231
+ ] }) }),
232
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 5, background: "neutral100", hasRadius: true, children: [
233
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", tag: "h3", fontWeight: "bold", marginBottom: 2, children: "1. Export Data to CSV" }),
234
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", marginBottom: 4, children: "Extract entire collections or pinpoint records violating validation quotas." }),
235
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, wrap: "wrap", alignItems: "center", marginTop: 3, children: [
236
+ /* @__PURE__ */ jsxRuntime.jsxs(
237
+ designSystem.Button,
238
+ {
239
+ variant: "default",
240
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
241
+ onClick: handleExportCollection,
242
+ disabled: !selectedUid,
243
+ children: [
244
+ "Export All ",
245
+ selectedCol?.displayName || "Collection",
246
+ " Records (.csv)"
247
+ ]
248
+ }
249
+ ),
250
+ rules.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", style: { marginLeft: "auto" }, children: [
251
+ /* @__PURE__ */ jsxRuntime.jsx(
252
+ designSystem.SingleSelect,
253
+ {
254
+ value: selectedRuleId,
255
+ onChange: (val) => setSelectedRuleId(val),
256
+ placeholder: "Select Quota Rule...",
257
+ children: rules.map((r) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.SingleSelectOption, { value: r.id, children: [
258
+ "Rule: ",
259
+ r.name
260
+ ] }, r.id))
261
+ }
262
+ ),
263
+ /* @__PURE__ */ jsxRuntime.jsx(
264
+ designSystem.Button,
265
+ {
266
+ variant: "danger-light",
267
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
268
+ onClick: handleExportViolations,
269
+ disabled: !selectedRuleId,
270
+ children: "Export Violators CSV"
271
+ }
272
+ )
273
+ ] })
274
+ ] })
275
+ ] }),
276
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 5, background: "neutral100", hasRadius: true, children: [
277
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", tag: "h3", fontWeight: "bold", marginBottom: 2, children: "2. Import & Batch Override CSV" }),
278
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", marginBottom: 4, children: "Upload an updated CSV file to bulk update existing records or insert new records." }),
279
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 6, marginBottom: 4, wrap: "wrap", children: [
280
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
281
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Operation Mode" }),
282
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, children: [
283
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
284
+ /* @__PURE__ */ jsxRuntime.jsx(
285
+ "input",
286
+ {
287
+ type: "radio",
288
+ name: "importMode",
289
+ value: "update",
290
+ checked: importMode === "update",
291
+ onChange: () => setImportMode("update")
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: importMode === "update" ? "bold" : "normal", children: "Update Existing (Matches by documentId)" })
295
+ ] }),
296
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
297
+ /* @__PURE__ */ jsxRuntime.jsx(
298
+ "input",
299
+ {
300
+ type: "radio",
301
+ name: "importMode",
302
+ value: "create_or_override",
303
+ checked: importMode === "create_or_override",
304
+ onChange: () => setImportMode("create_or_override")
305
+ }
306
+ ),
307
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: importMode === "create_or_override" ? "bold" : "normal", children: "Add / Override (Inserts new or replaces existing)" })
308
+ ] })
309
+ ] })
310
+ ] }),
311
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { marginLeft: "auto" }, children: [
312
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 2, children: "Publish Options" }),
313
+ /* @__PURE__ */ jsxRuntime.jsx(
314
+ designSystem.Checkbox,
315
+ {
316
+ checked: publishOnImport,
317
+ onValueChange: (val) => setPublishOnImport(val),
318
+ children: "Auto-publish on import"
319
+ }
320
+ )
321
+ ] })
322
+ ] }),
323
+ /* @__PURE__ */ jsxRuntime.jsxs(
324
+ designSystem.Box,
325
+ {
326
+ padding: 6,
327
+ background: "neutral0",
328
+ hasRadius: true,
329
+ style: {
330
+ border: "2px dashed #dcdce4",
331
+ textAlign: "center",
332
+ cursor: "pointer"
333
+ },
334
+ children: [
335
+ /* @__PURE__ */ jsxRuntime.jsx(
336
+ "input",
337
+ {
338
+ type: "file",
339
+ accept: ".csv",
340
+ id: "csv-file-input",
341
+ style: { display: "none" },
342
+ onChange: handleFileUpload
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: "csv-file-input", style: { cursor: "pointer", display: "block" }, children: [
346
+ /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, { style: { width: 32, height: 32, margin: "0 auto 8px", color: "#4945ff" } }),
347
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", display: "block", children: fileName ? `Selected: ${fileName}` : "Click here or drag a .csv file to upload" }),
348
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "UTF-8 encoded CSV matching collection schema headers" })
349
+ ] })
350
+ ]
351
+ }
352
+ ),
353
+ parsedPreview.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginTop: 4, background: "neutral0", padding: 4, hasRadius: true, children: [
354
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, children: [
355
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "sigma", textColor: "neutral700", children: [
356
+ "FILE PREVIEW (FIRST ",
357
+ parsedPreview.length,
358
+ " ROWS)"
359
+ ] }),
360
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral800", children: "Ready to Import" })
361
+ ] }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: Object.keys(parsedPreview[0]).length, rowCount: parsedPreview.length + 1, children: [
363
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: Object.keys(parsedPreview[0]).map((h) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: h }) }, h)) }) }),
364
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: parsedPreview.map((row, rIdx) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: Object.keys(parsedPreview[0]).map((h) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", children: row[h] || "-" }) }, h)) }, rIdx)) })
365
+ ] }),
366
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
367
+ designSystem.Button,
368
+ {
369
+ variant: "default",
370
+ size: "L",
371
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Check, {}),
372
+ onClick: handleExecuteImport,
373
+ loading: importing,
374
+ children: importMode === "update" ? "Execute Bulk Update" : "Execute Add / Override"
375
+ }
376
+ ) })
377
+ ] }),
378
+ importResult && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(
379
+ designSystem.Alert,
380
+ {
381
+ variant: importResult.stats.errors.length > 0 ? "warning" : "success",
382
+ title: "CSV Import Results",
383
+ children: [
384
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "omega", display: "block", marginBottom: 1, children: [
385
+ "Successfully processed: ",
386
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
387
+ importResult.stats.created,
388
+ " created"
389
+ ] }),
390
+ ",",
391
+ " ",
392
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
393
+ importResult.stats.updated,
394
+ " updated"
395
+ ] }),
396
+ ",",
397
+ " ",
398
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
399
+ importResult.stats.skipped,
400
+ " skipped"
401
+ ] }),
402
+ " out of ",
403
+ importResult.stats.total,
404
+ " total rows."
405
+ ] }),
406
+ importResult.stats.errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginTop: 2, children: [
407
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: [
408
+ "Errors encountered (",
409
+ importResult.stats.errors.length,
410
+ "):"
411
+ ] }),
412
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { margin: "4px 0 0 16px", padding: 0 }, children: importResult.stats.errors.slice(0, 5).map((err, idx) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
413
+ "Row ",
414
+ err.row,
415
+ ": ",
416
+ err.error
417
+ ] }) }, idx)) })
418
+ ] })
419
+ ]
420
+ }
421
+ ) })
422
+ ] })
423
+ ] }) });
424
+ }
10
425
  function getAvailableColumnsForCollection(col, selectedAttribute) {
11
426
  const cols = [
12
427
  {
@@ -163,7 +578,7 @@ const BulkUpdater = () => {
163
578
  try {
164
579
  const params = new URLSearchParams(window.location.search);
165
580
  const t = params.get("tab");
166
- if (t === "audit" || t === "editor") return t;
581
+ if (t === "audit" || t === "editor" || t === "rules" || t === "csv") return t;
167
582
  } catch {
168
583
  }
169
584
  return "editor";
@@ -182,7 +597,6 @@ const BulkUpdater = () => {
182
597
  const [updating, setUpdating] = React.useState(false);
183
598
  const [attributes, setAttributes] = React.useState([]);
184
599
  const [collections, setCollections] = React.useState([]);
185
- const [license, setLicense] = React.useState(null);
186
600
  const [auditEnabled, setAuditEnabled] = React.useState(false);
187
601
  const [executionWindow, setExecutionWindow] = React.useState(null);
188
602
  const [showConfirmModal, setShowConfirmModal] = React.useState(false);
@@ -270,7 +684,6 @@ const BulkUpdater = () => {
270
684
  setAttributes(res.data.attributes || []);
271
685
  const cols = res.data.collections || [];
272
686
  setCollections(cols);
273
- setLicense(res.data.license || null);
274
687
  setAuditEnabled(Boolean(res.data.auditEnabled));
275
688
  if (res.data.executionWindow) {
276
689
  setExecutionWindow(res.data.executionWindow);
@@ -618,13 +1031,6 @@ const BulkUpdater = () => {
618
1031
  });
619
1032
  return;
620
1033
  }
621
- if (useRegex && !license?.isPro) {
622
- toggleNotification({
623
- type: "warning",
624
- message: "Regex replacement is a Pro feature. Please activate your license in Settings."
625
- });
626
- return;
627
- }
628
1034
  const totalSelected = Object.values(selectedEntries).filter(Boolean).length;
629
1035
  if (totalSelected === 0) {
630
1036
  toggleNotification({
@@ -881,14 +1287,6 @@ const BulkUpdater = () => {
881
1287
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
882
1288
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", children: [
883
1289
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", fontWeight: "bold", children: "Bulk Attribute Updater" }),
884
- /* @__PURE__ */ jsxRuntime.jsx(
885
- designSystem.Badge,
886
- {
887
- backgroundColor: license?.isPro ? "success100" : "neutral200",
888
- textColor: license?.isPro ? "success700" : "neutral700",
889
- children: license?.isPro ? `${license.tier.toUpperCase()} EDITION` : "COMMUNITY EDITION"
890
- }
891
- ),
892
1290
  auditEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "AUDIT & ROLLBACK ACTIVE" })
893
1291
  ] }),
894
1292
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: "Cross-collection batch attribute management with declarative audit trail and one-click rollback." })
@@ -929,6 +1327,24 @@ const BulkUpdater = () => {
929
1327
  ] }) : auditHistory.length > 0 ? ` (${auditHistory.length})` : ""
930
1328
  ]
931
1329
  }
1330
+ ),
1331
+ /* @__PURE__ */ jsxRuntime.jsx(
1332
+ designSystem.Button,
1333
+ {
1334
+ variant: activeTab === "rules" ? "default" : "tertiary",
1335
+ onClick: () => setActiveTab("rules"),
1336
+ size: "M",
1337
+ children: "Validation & Quota Rules"
1338
+ }
1339
+ ),
1340
+ /* @__PURE__ */ jsxRuntime.jsx(
1341
+ designSystem.Button,
1342
+ {
1343
+ variant: activeTab === "csv" ? "default" : "tertiary",
1344
+ onClick: () => setActiveTab("csv"),
1345
+ size: "M",
1346
+ children: "CSV Import / Export & Override"
1347
+ }
932
1348
  )
933
1349
  ] }),
934
1350
  executionWindow && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -1108,38 +1524,24 @@ const BulkUpdater = () => {
1108
1524
  }
1109
1525
  ) })
1110
1526
  ] }),
1111
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginTop: 4, padding: 4, background: "neutral100", hasRadius: true, children: [
1112
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1113
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
1114
- /* @__PURE__ */ jsxRuntime.jsx(
1115
- designSystem.Checkbox,
1116
- {
1117
- checked: useRegex,
1118
- onCheckedChange: (checked) => setUseRegex(Boolean(checked)),
1119
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "bold", children: "Enable Regex Find & Replace" })
1120
- }
1121
- ),
1122
- /* @__PURE__ */ jsxRuntime.jsx(
1123
- designSystem.Badge,
1124
- {
1125
- backgroundColor: license?.isPro ? "success100" : "warning100",
1126
- textColor: license?.isPro ? "success700" : "warning700",
1127
- children: license?.isPro ? "PRO UNLOCKED" : "PRO FEATURE"
1128
- }
1129
- )
1130
- ] }),
1131
- useRegex && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1132
- designSystem.TextInput,
1133
- {
1134
- placeholder: "Regex pattern (e.g. \\b(OldName)\\b)...",
1135
- value: regexPattern,
1136
- onChange: (e) => setRegexPattern(e.target.value),
1137
- disabled: !license?.isPro
1138
- }
1139
- ) })
1140
- ] }),
1141
- !license?.isPro && useRegex && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "warning700", children: "Regex replacement is locked to Pro license holders. Visit Settings > Bulk Updater to activate your key." }) })
1142
- ] }),
1527
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginTop: 4, padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1528
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsx(
1529
+ designSystem.Checkbox,
1530
+ {
1531
+ checked: useRegex,
1532
+ onCheckedChange: (checked) => setUseRegex(Boolean(checked)),
1533
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "bold", children: "Enable Regex Find & Replace" })
1534
+ }
1535
+ ) }),
1536
+ useRegex && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1537
+ designSystem.TextInput,
1538
+ {
1539
+ placeholder: "Regex pattern (e.g. \\b(OldName)\\b)...",
1540
+ value: regexPattern,
1541
+ onChange: (e) => setRegexPattern(e.target.value)
1542
+ }
1543
+ ) })
1544
+ ] }) }),
1143
1545
  selectedAttribute && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginTop: 4, padding: 3, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
1144
1546
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", children: "Attribute Info:" }),
1145
1547
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", children: [
@@ -1483,6 +1885,52 @@ const BulkUpdater = () => {
1483
1885
  ]
1484
1886
  }
1485
1887
  ),
1888
+ /* @__PURE__ */ jsxRuntime.jsx(
1889
+ designSystem.Button,
1890
+ {
1891
+ variant: "tertiary",
1892
+ size: "S",
1893
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
1894
+ onClick: async () => {
1895
+ try {
1896
+ const params = new URLSearchParams();
1897
+ if (selectedAttribute) params.set("attribute", selectedAttribute);
1898
+ if (filterOperator && filterOperator !== "none") {
1899
+ params.set("filterOperator", filterOperator);
1900
+ if (filterQuery) params.set("filterQuery", filterQuery);
1901
+ }
1902
+ const res = await get(`/bulk-updater/csv/export/${col.uid}?${params.toString()}`);
1903
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
1904
+ if (!csvStr) {
1905
+ toggleNotification({
1906
+ type: "warning",
1907
+ message: "No CSV content returned for this collection"
1908
+ });
1909
+ return;
1910
+ }
1911
+ const blob = new Blob([csvStr], { type: "text/csv;charset=utf-8;" });
1912
+ const url = URL.createObjectURL(blob);
1913
+ const link = document.createElement("a");
1914
+ link.href = url;
1915
+ link.setAttribute("download", `${col.singularName || "export"}_entries.csv`);
1916
+ document.body.appendChild(link);
1917
+ link.click();
1918
+ document.body.removeChild(link);
1919
+ URL.revokeObjectURL(url);
1920
+ toggleNotification({
1921
+ type: "success",
1922
+ message: `Exported ${col.displayName} entries to CSV`
1923
+ });
1924
+ } catch (e) {
1925
+ toggleNotification({
1926
+ type: "danger",
1927
+ message: e.message || "Failed exporting CSV"
1928
+ });
1929
+ }
1930
+ },
1931
+ children: "Export CSV"
1932
+ }
1933
+ ),
1486
1934
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: col.isSingleType ? "1 single entry" : filterOperator !== "none" ? `${matchingColCount} matching of ${totalEntries} entries` : `${totalEntries} entries` })
1487
1935
  ] })
1488
1936
  ] }),
@@ -2009,6 +2457,8 @@ const BulkUpdater = () => {
2009
2457
  ]
2010
2458
  }
2011
2459
  ),
2460
+ activeTab === "rules" && /* @__PURE__ */ jsxRuntime.jsx(ValidationRulesTab.ValidationRulesTab, {}),
2461
+ activeTab === "csv" && /* @__PURE__ */ jsxRuntime.jsx(CsvManagerTab, {}),
2012
2462
  showConfirmModal && /* @__PURE__ */ jsxRuntime.jsx(
2013
2463
  "div",
2014
2464
  {
@@ -2053,20 +2503,10 @@ const BulkUpdater = () => {
2053
2503
  borderBottom: "1px solid rgba(120, 120, 150, 0.2)",
2054
2504
  flexShrink: 0
2055
2505
  },
2056
- children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
2057
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
2058
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Confirm Bulk Update" }),
2059
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", display: "block", children: "Review target attributes, affected counts, and server execution policy before proceeding." })
2060
- ] }),
2061
- /* @__PURE__ */ jsxRuntime.jsx(
2062
- designSystem.Badge,
2063
- {
2064
- backgroundColor: license?.tier === "enterprise" ? "primary100" : "neutral200",
2065
- textColor: license?.tier === "enterprise" ? "primary700" : "neutral700",
2066
- children: license?.tier === "enterprise" ? "ENTERPRISE 4-EYES" : "STANDARD"
2067
- }
2068
- )
2069
- ] })
2506
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
2507
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Confirm Bulk Update" }),
2508
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", display: "block", children: "Review target attributes, affected counts, and server execution policy before proceeding." })
2509
+ ] }) })
2070
2510
  }
2071
2511
  ),
2072
2512
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2210,7 +2650,7 @@ const BulkUpdater = () => {
2210
2650
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", gap: 3, style: { width: "100%" }, children: [
2211
2651
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", size: "M", onClick: () => setShowConfirmModal(false), children: "Cancel" }),
2212
2652
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, wrap: "wrap", justifyContent: "flex-end", alignItems: "center", children: [
2213
- (license?.tier === "enterprise" || license?.features?.includes("Maker-Checker Approval Workflow (4-Eyes Principle)")) && /* @__PURE__ */ jsxRuntime.jsx(
2653
+ /* @__PURE__ */ jsxRuntime.jsx(
2214
2654
  designSystem.Button,
2215
2655
  {
2216
2656
  variant: executionWindow?.isBusinessHours ? "default" : "secondary",
@@ -2224,7 +2664,7 @@ const BulkUpdater = () => {
2224
2664
  /* @__PURE__ */ jsxRuntime.jsxs(
2225
2665
  designSystem.Button,
2226
2666
  {
2227
- variant: license?.tier === "enterprise" ? "secondary" : "default",
2667
+ variant: "secondary",
2228
2668
  size: "M",
2229
2669
  onClick: () => executeBatchWithMode("queue_offpeak"),
2230
2670
  loading: updating,
@@ -2271,4 +2711,4 @@ const BulkUpdater = () => {
2271
2711
  exports.default = BulkUpdater;
2272
2712
  exports.formatDisplayValue = formatDisplayValue;
2273
2713
  exports.getAvailableColumnsForCollection = getAvailableColumnsForCollection;
2274
- //# sourceMappingURL=BulkUpdater-CooMo-Hc.js.map
2714
+ //# sourceMappingURL=BulkUpdater-BRcMQGP3.js.map