strapi-plugin-bulk-updater 1.5.8 → 1.5.10

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.
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import React__default, { useState, useEffect, useMemo, useRef } from "react";
3
3
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
- import { Box, Flex, Typography, Button, Combobox, ComboboxOption, SingleSelect, SingleSelectOption, Badge, Checkbox, Alert, Table, Thead, Tr, Th, Tbody, Td, TextInput, Loader, Field } from "@strapi/design-system";
4
+ import { Box, Flex, Typography, Button, Combobox, ComboboxOption, TextInput, SingleSelect, SingleSelectOption, Badge, Checkbox, Alert, Table, Thead, Tr, Th, Tbody, Td, Loader, Field } from "@strapi/design-system";
5
5
  import { ArrowClockwise, Download, Upload, ChevronLeft, ChevronRight, Eye, Check, Cog, Play } from "@strapi/icons";
6
6
  import { i as isAbortError, V as ValidationRulesTab } from "./ValidationRulesTab-BtOCWK8E.mjs";
7
7
  function parseCsvRows(csvText) {
@@ -136,13 +136,104 @@ function extractDocId(row) {
136
136
  }
137
137
  return "";
138
138
  }
139
+ function getEntryLifecycleStatus(entry, col, pendingBatches = []) {
140
+ const isPendingApproval = pendingBatches.some((b) => {
141
+ if (b.status !== "pending_approval") return false;
142
+ const targets = b.payload?.selectedEntries || [];
143
+ return targets.some((t) => entry.documentId && t.documentId === entry.documentId || String(t.documentId) === String(entry.id));
144
+ });
145
+ if (isPendingApproval) {
146
+ return {
147
+ status: "pending_review",
148
+ label: "Pending Review",
149
+ badgeColor: "warning100",
150
+ textColor: "warning700"
151
+ };
152
+ }
153
+ if (col && col.draftAndPublish === false) {
154
+ return {
155
+ status: "published",
156
+ label: "Published",
157
+ badgeColor: "success100",
158
+ textColor: "success700"
159
+ };
160
+ }
161
+ if (entry.status === "modified") {
162
+ return {
163
+ status: "modified",
164
+ label: "Modified",
165
+ badgeColor: "primary100",
166
+ textColor: "primary700"
167
+ };
168
+ }
169
+ if (entry.status === "draft") {
170
+ return {
171
+ status: "draft",
172
+ label: "Draft",
173
+ badgeColor: "neutral150",
174
+ textColor: "neutral700"
175
+ };
176
+ }
177
+ if (entry.status === "published") {
178
+ return {
179
+ status: "published",
180
+ label: "Published",
181
+ badgeColor: "success100",
182
+ textColor: "success700"
183
+ };
184
+ }
185
+ if (entry.status === "unknown") {
186
+ return {
187
+ status: "unknown",
188
+ label: "Unknown",
189
+ badgeColor: "neutral200",
190
+ textColor: "neutral600"
191
+ };
192
+ }
193
+ if (!entry.documentId && !entry.id && !entry.slug) {
194
+ return {
195
+ status: "unknown",
196
+ label: "Unknown",
197
+ badgeColor: "neutral200",
198
+ textColor: "neutral600"
199
+ };
200
+ }
201
+ if (entry.publishedAt) {
202
+ if (entry.updatedAt && new Date(entry.updatedAt).getTime() > new Date(entry.publishedAt).getTime()) {
203
+ return {
204
+ status: "modified",
205
+ label: "Modified",
206
+ badgeColor: "primary100",
207
+ textColor: "primary700"
208
+ };
209
+ }
210
+ return {
211
+ status: "published",
212
+ label: "Published",
213
+ badgeColor: "success100",
214
+ textColor: "success700"
215
+ };
216
+ }
217
+ return {
218
+ status: "draft",
219
+ label: "Draft",
220
+ badgeColor: "neutral150",
221
+ textColor: "neutral700"
222
+ };
223
+ }
139
224
  function CsvManagerTab() {
140
225
  const { get, post } = useFetchClient();
141
226
  const { toggleNotification } = useNotification();
142
227
  const [collections, setCollections] = useState([]);
143
228
  const [loadingCollections, setLoadingCollections] = useState(true);
144
229
  const [selectedUid, setSelectedUid] = useState("");
230
+ const [collectionEntries, setCollectionEntries] = useState([]);
231
+ const [pendingBatches, setPendingBatches] = useState([]);
145
232
  const [exportLimit, setExportLimit] = useState(0);
233
+ const [exportLimitInput, setExportLimitInput] = useState("");
234
+ const [exportingCollection, setExportingCollection] = useState(false);
235
+ const [downloadingTemplate, setDownloadingTemplate] = useState(false);
236
+ const [exportingViolations, setExportingViolations] = useState(false);
146
237
  const [exportOnlyViolations, setExportOnlyViolations] = useState(false);
147
238
  const [rules, setRules] = useState([]);
148
239
  const [selectedRuleId, setSelectedRuleId] = useState("");
@@ -170,17 +261,24 @@ function CsvManagerTab() {
170
261
  const fetchInitialData = async () => {
171
262
  try {
172
263
  setLoadingCollections(true);
173
- const [modelsRes, rulesRes] = await Promise.all([
264
+ const [modelsRes, rulesRes, batchesRes] = await Promise.all([
174
265
  get("/bulk-updater/models"),
175
- get("/bulk-updater/validation-rules")
266
+ get("/bulk-updater/validation-rules"),
267
+ get("/bulk-updater/pending-batches").catch(() => ({ data: { batches: [] } }))
176
268
  ]);
177
269
  if (modelsRes.data?.executionWindow) {
178
270
  setExecutionWindow(modelsRes.data.executionWindow);
179
271
  }
272
+ if (batchesRes.data?.batches) {
273
+ setPendingBatches(batchesRes.data.batches);
274
+ }
275
+ let loadedCollections = [];
180
276
  if (modelsRes.data?.collections) {
181
- setCollections(modelsRes.data.collections);
182
- if (modelsRes.data.collections.length > 0) {
183
- setSelectedUid(modelsRes.data.collections[0].uid);
277
+ loadedCollections = modelsRes.data.collections;
278
+ setCollections(loadedCollections);
279
+ if (loadedCollections.length > 0) {
280
+ const firstUid = loadedCollections[0].uid;
281
+ setSelectedUid(firstUid);
184
282
  }
185
283
  }
186
284
  if (rulesRes.data?.rules) {
@@ -199,9 +297,66 @@ function CsvManagerTab() {
199
297
  setLoadingCollections(false);
200
298
  }
201
299
  };
300
+ const fetchAndEnrichEntries = async (colUid, cols = collections, batches = pendingBatches) => {
301
+ if (!colUid) return [];
302
+ try {
303
+ const res = await get(`/bulk-updater/collections/${colUid}/entries?page=1&pageSize=500`);
304
+ if (res.data && (res.data.success || res.data.entries !== void 0)) {
305
+ const fetchedEntries = res.data.entries || [];
306
+ const col = (cols || []).find((c) => c.uid === colUid) || null;
307
+ const enrichedEntries = fetchedEntries.map((entry) => {
308
+ if (!entry.status) {
309
+ const lifecycle = getEntryLifecycleStatus(entry, col, batches);
310
+ return {
311
+ ...entry,
312
+ status: lifecycle.status,
313
+ publishedAt: entry.publishedAt ?? (lifecycle.status === "published" ? entry.updatedAt || null : null)
314
+ };
315
+ }
316
+ return entry;
317
+ });
318
+ setCollectionEntries(enrichedEntries);
319
+ return enrichedEntries;
320
+ }
321
+ } catch (err) {
322
+ if (isAbortError(err)) return [];
323
+ }
324
+ return [];
325
+ };
326
+ const enrichCsvRowsWithLifecycle = (rows, knownEntries = collectionEntries) => {
327
+ if (!rows || rows.length === 0) return rows;
328
+ const entryByDocId = /* @__PURE__ */ new Map();
329
+ for (const e of knownEntries) {
330
+ if (e.documentId) entryByDocId.set(String(e.documentId), e);
331
+ if (e.id) entryByDocId.set(String(e.id), e);
332
+ }
333
+ return rows.map((row) => {
334
+ const docId = extractDocId(row);
335
+ const matched = docId ? entryByDocId.get(docId) : void 0;
336
+ const existingStatus = row.status || row.Status;
337
+ const existingPubAt = row.publishedAt || row.published_at;
338
+ let status = existingStatus;
339
+ let publishedAt = existingPubAt;
340
+ if (!status && matched?.status) {
341
+ status = matched.status;
342
+ }
343
+ if (!publishedAt && matched?.publishedAt) {
344
+ publishedAt = matched.publishedAt;
345
+ }
346
+ if (status || publishedAt) {
347
+ return {
348
+ ...row,
349
+ ...status ? { status } : {},
350
+ ...publishedAt ? { publishedAt } : {}
351
+ };
352
+ }
353
+ return row;
354
+ });
355
+ };
202
356
  const handleDownloadTemplate = async () => {
203
- if (!selectedUid) return;
357
+ if (!selectedUid || downloadingTemplate) return;
204
358
  try {
359
+ setDownloadingTemplate(true);
205
360
  const res = await get(`/bulk-updater/csv/template/${selectedUid}`);
206
361
  const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
207
362
  const filename = res.data?.filename || `${selectedUid.split(".").pop()}_template.csv`;
@@ -216,11 +371,14 @@ function CsvManagerTab() {
216
371
  type: "danger",
217
372
  message: err.message || "Failed downloading CSV template"
218
373
  });
374
+ } finally {
375
+ setDownloadingTemplate(false);
219
376
  }
220
377
  };
221
378
  const handleExportCollection = async () => {
222
- if (!selectedUid) return;
379
+ if (!selectedUid || exportingCollection) return;
223
380
  try {
381
+ setExportingCollection(true);
224
382
  const res = await get(`/bulk-updater/csv/export/${selectedUid}?limit=${exportLimit}`);
225
383
  const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
226
384
  const filename = res.data?.filename || `${selectedUid.split(".").pop()}_export.csv`;
@@ -235,11 +393,14 @@ function CsvManagerTab() {
235
393
  type: "danger",
236
394
  message: err.message || "Failed exporting collection to CSV"
237
395
  });
396
+ } finally {
397
+ setExportingCollection(false);
238
398
  }
239
399
  };
240
400
  const handleExportViolations = async () => {
241
- if (!selectedRuleId) return;
401
+ if (!selectedRuleId || exportingViolations) return;
242
402
  try {
403
+ setExportingViolations(true);
243
404
  const res = await get(`/bulk-updater/csv/export-violations/${selectedRuleId}`);
244
405
  const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
245
406
  const filename = res.data?.filename || `violations_${selectedRuleId}.csv`;
@@ -254,6 +415,8 @@ function CsvManagerTab() {
254
415
  type: "danger",
255
416
  message: err.message || "Failed exporting violations to CSV"
256
417
  });
418
+ } finally {
419
+ setExportingViolations(false);
257
420
  }
258
421
  };
259
422
  const downloadCsvFile = (content, fallbackName) => {
@@ -287,7 +450,8 @@ function CsvManagerTab() {
287
450
  setCsvRawText(text);
288
451
  try {
289
452
  const records = parseCsv(text);
290
- setAllParsedRecords(records);
453
+ const enriched = enrichCsvRowsWithLifecycle(records, collectionEntries);
454
+ setAllParsedRecords(enriched);
291
455
  setPreviewPage(1);
292
456
  } catch (err) {
293
457
  setAllParsedRecords([]);
@@ -297,7 +461,12 @@ function CsvManagerTab() {
297
461
  };
298
462
  const handleExecuteImport = async () => {
299
463
  if (!selectedUid || allParsedRecords.length === 0) return;
300
- const totalRows = allParsedRecords.length;
464
+ let currentEntries = collectionEntries;
465
+ if (currentEntries.length === 0) {
466
+ currentEntries = await fetchAndEnrichEntries(selectedUid);
467
+ }
468
+ const enrichedRecords = enrichCsvRowsWithLifecycle(allParsedRecords, currentEntries);
469
+ const totalRows = enrichedRecords.length;
301
470
  const CHUNK_SIZE = 100;
302
471
  const totalChunks = Math.ceil(totalRows / CHUNK_SIZE);
303
472
  const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
@@ -323,7 +492,7 @@ function CsvManagerTab() {
323
492
  for (let chunkIdx = 0; chunkIdx < totalChunks; chunkIdx++) {
324
493
  const startIdx = chunkIdx * CHUNK_SIZE;
325
494
  const endIdx = Math.min(startIdx + CHUNK_SIZE, totalRows);
326
- const chunkRows = allParsedRecords.slice(startIdx, endIdx);
495
+ const chunkRows = enrichedRecords.slice(startIdx, endIdx);
327
496
  const isFirst = chunkIdx === 0;
328
497
  const isLast = chunkIdx === totalChunks - 1;
329
498
  const startRowNumber = startIdx + 2;
@@ -391,9 +560,14 @@ function CsvManagerTab() {
391
560
  if (!selectedUid || allParsedRecords.length === 0) return;
392
561
  try {
393
562
  setPreflightLoading(true);
563
+ let currentEntries = collectionEntries;
564
+ if (currentEntries.length === 0) {
565
+ currentEntries = await fetchAndEnrichEntries(selectedUid);
566
+ }
567
+ const enrichedRecords = enrichCsvRowsWithLifecycle(allParsedRecords, currentEntries);
394
568
  const res = await post("/bulk-updater/csv/validate-preflight", {
395
569
  uid: selectedUid,
396
- rows: allParsedRecords,
570
+ rows: enrichedRecords,
397
571
  mode: importMode
398
572
  });
399
573
  if (res.data && res.data.success) {
@@ -434,9 +608,14 @@ function CsvManagerTab() {
434
608
  }
435
609
  try {
436
610
  setSubmittingBatch(true);
611
+ let currentEntries = collectionEntries;
612
+ if (currentEntries.length === 0) {
613
+ currentEntries = await fetchAndEnrichEntries(selectedUid);
614
+ }
615
+ const enrichedRecords = enrichCsvRowsWithLifecycle(allParsedRecords, currentEntries);
437
616
  const res = await post("/bulk-updater/csv/submit-batch", {
438
617
  uid: selectedUid,
439
- rows: allParsedRecords,
618
+ rows: enrichedRecords,
440
619
  executionMode,
441
620
  makerNotes: makerNotesInput,
442
621
  forceImmediate,
@@ -518,7 +697,8 @@ function CsvManagerTab() {
518
697
  variant: "tertiary",
519
698
  startIcon: /* @__PURE__ */ jsx(Download, {}),
520
699
  onClick: handleDownloadTemplate,
521
- disabled: !selectedUid,
700
+ disabled: !selectedUid || downloadingTemplate,
701
+ loading: downloadingTemplate,
522
702
  children: "Download Sample CSV Template"
523
703
  }
524
704
  )
@@ -526,35 +706,61 @@ function CsvManagerTab() {
526
706
  /* @__PURE__ */ jsxs(Box, { padding: 5, background: "neutral100", hasRadius: true, children: [
527
707
  /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h3", fontWeight: "bold", marginBottom: 2, children: "1. Export Data to CSV" }),
528
708
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", marginBottom: 4, children: "Extract entire collections without artificial limits (streams 10,000+ records in paged chunks) or pinpoint records violating validation quotas." }),
529
- /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", alignItems: "center", marginTop: 3, children: [
530
- /* @__PURE__ */ jsx(Box, { style: { width: 220 }, children: /* @__PURE__ */ jsxs(
531
- SingleSelect,
532
- {
533
- value: String(exportLimit),
534
- onChange: (val) => setExportLimit(Number(val)),
535
- placeholder: "Export Limit",
536
- children: [
537
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "0", children: "All Records (No Limit)" }),
538
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "10000", children: "Up to 10,000 Records" }),
539
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "5000", children: "Up to 5,000 Records" }),
540
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "1000", children: "Up to 1,000 Records" })
541
- ]
542
- }
543
- ) }),
544
- /* @__PURE__ */ jsxs(
709
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", alignItems: "flex-end", marginTop: 3, children: [
710
+ /* @__PURE__ */ jsxs(Box, { style: { width: 220 }, children: [
711
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", marginBottom: 1, display: "block", children: "Records to Download" }),
712
+ /* @__PURE__ */ jsx(
713
+ TextInput,
714
+ {
715
+ type: "number",
716
+ min: 0,
717
+ placeholder: "All records (or enter count)",
718
+ value: exportLimitInput,
719
+ onChange: (e) => {
720
+ const raw = e.target.value;
721
+ setExportLimitInput(raw);
722
+ const parsed = parseInt(raw, 10);
723
+ setExportLimit(!raw || isNaN(parsed) || parsed <= 0 ? 0 : parsed);
724
+ },
725
+ hint: exportLimit === 0 ? "Exporting all records (no limit)" : `Exporting first ${exportLimit.toLocaleString()} records`
726
+ }
727
+ )
728
+ ] }),
729
+ /* @__PURE__ */ jsxs(Box, { children: [
730
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", marginBottom: 1, display: "block", children: "Quick Presets:" }),
731
+ /* @__PURE__ */ jsx(Flex, { gap: 1, wrap: "wrap", children: [
732
+ { label: "All", value: 0 },
733
+ { label: "100", value: 100 },
734
+ { label: "500", value: 500 },
735
+ { label: "1,000", value: 1e3 },
736
+ { label: "5,000", value: 5e3 },
737
+ { label: "10,000", value: 1e4 }
738
+ ].map((preset) => {
739
+ const isSelected = preset.value === 0 ? exportLimit === 0 && (!exportLimitInput || exportLimitInput === "0") : exportLimit === preset.value && exportLimitInput === String(preset.value);
740
+ return /* @__PURE__ */ jsx(
741
+ Button,
742
+ {
743
+ variant: isSelected ? "secondary" : "tertiary",
744
+ size: "S",
745
+ onClick: () => {
746
+ setExportLimit(preset.value);
747
+ setExportLimitInput(preset.value === 0 ? "" : String(preset.value));
748
+ },
749
+ children: preset.label
750
+ },
751
+ preset.value
752
+ );
753
+ }) })
754
+ ] }),
755
+ /* @__PURE__ */ jsx(
545
756
  Button,
546
757
  {
547
758
  variant: "default",
548
759
  startIcon: /* @__PURE__ */ jsx(Download, {}),
549
760
  onClick: handleExportCollection,
550
- disabled: !selectedUid,
551
- children: [
552
- "Export ",
553
- exportLimit === 0 ? "All" : `${exportLimit}`,
554
- " ",
555
- selectedCol?.displayName || "Collection",
556
- " Records (.csv)"
557
- ]
761
+ disabled: !selectedUid || exportingCollection,
762
+ loading: exportingCollection,
763
+ children: exportingCollection ? "Preparing CSV download..." : `Export ${exportLimit === 0 ? "All" : exportLimit.toLocaleString()} ${selectedCol?.displayName || "Collection"} Records (.csv)`
558
764
  }
559
765
  ),
560
766
  rules.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { marginLeft: "auto" }, children: [
@@ -576,7 +782,8 @@ function CsvManagerTab() {
576
782
  variant: "danger-light",
577
783
  startIcon: /* @__PURE__ */ jsx(Download, {}),
578
784
  onClick: handleExportViolations,
579
- disabled: !selectedRuleId,
785
+ disabled: !selectedRuleId || exportingViolations,
786
+ loading: exportingViolations,
580
787
  children: "Export Violators CSV"
581
788
  }
582
789
  )
@@ -2853,91 +3060,6 @@ const CheckerReviewModal = ({
2853
3060
  }
2854
3061
  );
2855
3062
  };
2856
- function getEntryLifecycleStatus(entry, col, pendingBatches = []) {
2857
- const isPendingApproval = pendingBatches.some((b) => {
2858
- if (b.status !== "pending_approval") return false;
2859
- const targets = b.payload?.selectedEntries || [];
2860
- return targets.some((t) => entry.documentId && t.documentId === entry.documentId || String(t.documentId) === String(entry.id));
2861
- });
2862
- if (isPendingApproval) {
2863
- return {
2864
- status: "pending_review",
2865
- label: "Pending Review",
2866
- badgeColor: "warning100",
2867
- textColor: "warning700"
2868
- };
2869
- }
2870
- if (col && col.draftAndPublish === false) {
2871
- return {
2872
- status: "published",
2873
- label: "Published",
2874
- badgeColor: "success100",
2875
- textColor: "success700"
2876
- };
2877
- }
2878
- if (entry.status === "modified") {
2879
- return {
2880
- status: "modified",
2881
- label: "Modified",
2882
- badgeColor: "primary100",
2883
- textColor: "primary700"
2884
- };
2885
- }
2886
- if (entry.status === "draft") {
2887
- return {
2888
- status: "draft",
2889
- label: "Draft",
2890
- badgeColor: "neutral150",
2891
- textColor: "neutral700"
2892
- };
2893
- }
2894
- if (entry.status === "published") {
2895
- return {
2896
- status: "published",
2897
- label: "Published",
2898
- badgeColor: "success100",
2899
- textColor: "success700"
2900
- };
2901
- }
2902
- if (entry.status === "unknown") {
2903
- return {
2904
- status: "unknown",
2905
- label: "Unknown",
2906
- badgeColor: "neutral200",
2907
- textColor: "neutral600"
2908
- };
2909
- }
2910
- if (!entry.documentId && !entry.id && !entry.slug) {
2911
- return {
2912
- status: "unknown",
2913
- label: "Unknown",
2914
- badgeColor: "neutral200",
2915
- textColor: "neutral600"
2916
- };
2917
- }
2918
- if (entry.publishedAt) {
2919
- if (entry.updatedAt && new Date(entry.updatedAt).getTime() > new Date(entry.publishedAt).getTime()) {
2920
- return {
2921
- status: "modified",
2922
- label: "Modified",
2923
- badgeColor: "primary100",
2924
- textColor: "primary700"
2925
- };
2926
- }
2927
- return {
2928
- status: "published",
2929
- label: "Published",
2930
- badgeColor: "success100",
2931
- textColor: "success700"
2932
- };
2933
- }
2934
- return {
2935
- status: "draft",
2936
- label: "Draft",
2937
- badgeColor: "neutral150",
2938
- textColor: "neutral700"
2939
- };
2940
- }
2941
3063
  function parseDynamicZonePath(path) {
2942
3064
  if (!path) return { cleanPath: "", rootKey: "", subPath: "" };
2943
3065
  const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\]\.(.+)$/);
@@ -7533,4 +7655,4 @@ export {
7533
7655
  getSortOptionsForCollection,
7534
7656
  parseDynamicZonePath
7535
7657
  };
7536
- //# sourceMappingURL=BulkUpdater-B58N9HHk.mjs.map
7658
+ //# sourceMappingURL=BulkUpdater-7rkObCic.mjs.map