strapi-plugin-bulk-updater 1.2.12 → 1.3.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.
@@ -3,7 +3,7 @@ import React__default, { useState, useEffect, useMemo, useRef } from "react";
3
3
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
4
4
  import { Box, Flex, Typography, Button, SingleSelect, SingleSelectOption, Checkbox, Badge, Alert, Table, Thead, Tr, Th, Tbody, Td, Loader, TextInput, ComboboxOption, Field, Combobox } from "@strapi/design-system";
5
5
  import { ArrowClockwise, Download, Upload, Check, Eye, Play } from "@strapi/icons";
6
- import { V as ValidationRulesTab } from "./ValidationRulesTab-BGcqM9cN.mjs";
6
+ import { V as ValidationRulesTab } from "./ValidationRulesTab-Dt53h-Ke.mjs";
7
7
  function CsvManagerTab() {
8
8
  const { get, post } = useFetchClient();
9
9
  const { toggleNotification } = useNotification();
@@ -114,7 +114,8 @@ function CsvManagerTab() {
114
114
  });
115
115
  return;
116
116
  }
117
- const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
117
+ const safeContent = content.startsWith("\uFEFF") ? content : "\uFEFF" + content;
118
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
118
119
  const url = URL.createObjectURL(blob);
119
120
  const link = document.createElement("a");
120
121
  link.href = url;
@@ -503,7 +504,9 @@ function formatVal(v) {
503
504
  return String(v);
504
505
  }
505
506
  function downloadFile(content, filename, mimeType = "text/csv;charset=utf-8;") {
506
- const blob = new Blob([content], { type: mimeType });
507
+ const isCsv = mimeType.includes("csv") || filename.endsWith(".csv");
508
+ const safeContent = isCsv && !content.startsWith("\uFEFF") ? "\uFEFF" + content : content;
509
+ const blob = new Blob([safeContent], { type: mimeType });
507
510
  const url = URL.createObjectURL(blob);
508
511
  const a = document.createElement("a");
509
512
  a.href = url;
@@ -625,7 +628,7 @@ function ProcessingDashboardModal({
625
628
  ].join(",")
626
629
  )
627
630
  ];
628
- downloadFile(csvRows.join("\n"), `bulk_update_report_${targetId}.csv`);
631
+ downloadFile(csvRows.join("\r\n") + "\r\n", `bulk_update_report_${targetId}.csv`);
629
632
  toggleNotification({
630
633
  type: "success",
631
634
  message: "Execution report downloaded successfully"
@@ -1451,7 +1454,7 @@ function getNestedValue(obj, path) {
1451
1454
  if (!path.includes(".")) return obj[path];
1452
1455
  return path.split(".").reduce((acc, part) => acc && typeof acc === "object" ? acc[part] : void 0, obj);
1453
1456
  }
1454
- function getAvailableColumnsForCollection(col, selectedAttribute) {
1457
+ function getAvailableColumnsForCollection(col, selectedAttribute, searchAttribute) {
1455
1458
  const cols = [
1456
1459
  {
1457
1460
  key: "select",
@@ -1468,7 +1471,7 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1468
1471
  ];
1469
1472
  const candidateAttrs = col.attributes || [];
1470
1473
  candidateAttrs.forEach((attr) => {
1471
- if (attr === "name" || attr === "title" || attr === selectedAttribute) {
1474
+ if (attr === "name" || attr === "title" || attr === selectedAttribute || searchAttribute && attr === searchAttribute) {
1472
1475
  return;
1473
1476
  }
1474
1477
  const formattedLabel = attr.includes(".") ? attr.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" > ") : attr.charAt(0).toUpperCase() + attr.slice(1);
@@ -1479,9 +1482,19 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1479
1482
  defaultVisible: ["department", "email", "location"].includes(attr)
1480
1483
  });
1481
1484
  });
1485
+ if (searchAttribute && searchAttribute !== selectedAttribute) {
1486
+ const formattedSearchLabel = searchAttribute.includes(".") ? searchAttribute.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" > ") : searchAttribute;
1487
+ cols.push({
1488
+ key: "search_attr",
1489
+ label: `Search: "${formattedSearchLabel}"`,
1490
+ sortable: !col.isSingleType && col.attributes.includes(searchAttribute),
1491
+ defaultVisible: true
1492
+ });
1493
+ }
1494
+ const targetLabel = searchAttribute && searchAttribute !== selectedAttribute ? `Update Target: "${selectedAttribute}"` : `Current "${selectedAttribute}"`;
1482
1495
  cols.push({
1483
1496
  key: "target_attr",
1484
- label: `Current "${selectedAttribute}"`,
1497
+ label: targetLabel,
1485
1498
  sortable: !col.isSingleType && col.attributes.includes(selectedAttribute),
1486
1499
  defaultVisible: true
1487
1500
  });
@@ -1493,17 +1506,19 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1493
1506
  });
1494
1507
  return cols;
1495
1508
  }
1496
- function renderHeaderCell(colDef, col, currentSort, selectedAttribute, onSort) {
1509
+ function renderHeaderCell(colDef, col, currentSort, selectedAttribute, searchAttribute, onSort) {
1497
1510
  if (colDef.key === "select") {
1498
1511
  return /* @__PURE__ */ jsx(Th, { style: { width: 48 }, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Select" }) }, "select");
1499
1512
  }
1500
- const isCurrentSorted = colDef.key === "identifier" && (currentSort.field === "name" || currentSort.field === "title") || colDef.key === "target_attr" && currentSort.field === selectedAttribute || colDef.key.startsWith("attr_") && currentSort.field === colDef.key.replace("attr_", "");
1513
+ const isCurrentSorted = colDef.key === "identifier" && (currentSort.field === "name" || currentSort.field === "title") || colDef.key === "search_attr" && currentSort.field === searchAttribute || colDef.key === "target_attr" && currentSort.field === selectedAttribute || colDef.key.startsWith("attr_") && currentSort.field === colDef.key.replace("attr_", "");
1501
1514
  const sortArrow = isCurrentSorted ? currentSort.order === "asc" ? "▲" : "▼" : "⇅";
1502
1515
  const handleClick = () => {
1503
1516
  if (!colDef.sortable) return;
1504
1517
  if (colDef.key === "identifier") {
1505
1518
  const field = col.attributes.includes("name") ? "name" : col.attributes.includes("title") ? "title" : "createdAt";
1506
1519
  onSort(col.uid, field);
1520
+ } else if (colDef.key === "search_attr") {
1521
+ onSort(col.uid, searchAttribute);
1507
1522
  } else if (colDef.key === "target_attr") {
1508
1523
  onSort(col.uid, selectedAttribute);
1509
1524
  } else if (colDef.key.startsWith("attr_")) {
@@ -1539,7 +1554,7 @@ function formatDisplayValue(val) {
1539
1554
  }
1540
1555
  return String(val);
1541
1556
  }
1542
- function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAttr, isSelected, onToggleSelect) {
1557
+ function renderCellContent(colDef, entry, col, selectedAttribute, searchAttribute, isMatch, hasAttr, isSelected, onToggleSelect) {
1543
1558
  switch (colDef.key) {
1544
1559
  case "select":
1545
1560
  return /* @__PURE__ */ jsx(
@@ -1556,6 +1571,27 @@ function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAt
1556
1571
  entry.email && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", display: "block", children: entry.email })
1557
1572
  ] });
1558
1573
  }
1574
+ case "search_attr": {
1575
+ const val = getNestedValue(entry, searchAttribute);
1576
+ const displayStr = formatDisplayValue(val);
1577
+ const isUnset = displayStr === "(empty / not set)" || displayStr === "(empty / unlinked)";
1578
+ return /* @__PURE__ */ jsx(
1579
+ Typography,
1580
+ {
1581
+ textColor: isUnset ? "neutral500" : "primary700",
1582
+ fontWeight: "semiBold",
1583
+ style: {
1584
+ fontStyle: isUnset ? "italic" : "normal",
1585
+ display: "inline-block",
1586
+ maxWidth: 260,
1587
+ overflow: "hidden",
1588
+ textOverflow: "ellipsis",
1589
+ whiteSpace: "nowrap"
1590
+ },
1591
+ children: displayStr
1592
+ }
1593
+ );
1594
+ }
1559
1595
  case "target_attr": {
1560
1596
  const val = getNestedValue(entry, selectedAttribute);
1561
1597
  const displayStr = formatDisplayValue(val);
@@ -1646,6 +1682,7 @@ const BulkUpdater = () => {
1646
1682
  const [showPublishConfirmModal, setShowPublishConfirmModal] = useState(false);
1647
1683
  const [publishing, setPublishing] = useState(false);
1648
1684
  const [publishMakerNotesInput, setPublishMakerNotesInput] = useState("");
1685
+ const [bulkExporting, setBulkExporting] = useState(false);
1649
1686
  const MODELS_CACHE_KEY = "strapi_bulk_updater_models_cache";
1650
1687
  const LAST_ATTR_KEY = "strapi_bulk_updater_last_attr";
1651
1688
  const getInitialAttribute = () => {
@@ -1679,10 +1716,34 @@ const BulkUpdater = () => {
1679
1716
  };
1680
1717
  const [useRegex, setUseRegex] = useState(false);
1681
1718
  const [regexPattern, setRegexPattern] = useState("");
1719
+ const DEPTH_STORAGE_KEY = "strapi_bulk_updater_component_depth";
1720
+ const [componentDepth, setComponentDepth] = useState(() => {
1721
+ try {
1722
+ const stored = localStorage.getItem(DEPTH_STORAGE_KEY);
1723
+ if (stored) {
1724
+ const val = Number(stored);
1725
+ if (val >= 1 && val <= 5) return val;
1726
+ }
1727
+ } catch {
1728
+ }
1729
+ return 3;
1730
+ });
1731
+ const handleComponentDepthChange = (depth) => {
1732
+ const capped = Math.min(5, Math.max(1, depth));
1733
+ setComponentDepth(capped);
1734
+ try {
1735
+ localStorage.setItem(DEPTH_STORAGE_KEY, String(capped));
1736
+ } catch {
1737
+ }
1738
+ };
1739
+ const [searchAttribute, setSearchAttribute] = useState("");
1740
+ const [searchAttributeInput, setSearchAttributeInput] = useState("");
1741
+ const [attributesMeta, setAttributesMeta] = useState({});
1742
+ const effectiveSearchAttribute = searchAttribute || selectedAttribute;
1682
1743
  const [filterOperator, setFilterOperator] = useState("none");
1683
1744
  const [filterQuery, setFilterQuery] = useState("");
1684
1745
  const [entrySearch, setEntrySearch] = useState("");
1685
- const isFilterActive = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1746
+ const isFilterActive = Boolean(searchAttribute && searchAttribute !== selectedAttribute) || filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1686
1747
  const [lastResults, setLastResults] = useState(null);
1687
1748
  const [updateSummary, setUpdateSummary] = useState(null);
1688
1749
  const [collectionPages, setCollectionPages] = useState({});
@@ -1762,6 +1823,9 @@ const BulkUpdater = () => {
1762
1823
  if (data.relationsMeta) {
1763
1824
  setRelationsMeta(data.relationsMeta);
1764
1825
  }
1826
+ if (data.attributesMeta) {
1827
+ setAttributesMeta(data.attributesMeta);
1828
+ }
1765
1829
  let storedAttr = null;
1766
1830
  try {
1767
1831
  storedAttr = localStorage.getItem(LAST_ATTR_KEY);
@@ -1809,7 +1873,7 @@ const BulkUpdater = () => {
1809
1873
  setLoading(false);
1810
1874
  }
1811
1875
  };
1812
- const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder, overrideFilterOp, overrideFilterQuery, overrideEntrySearch, autoSelectReturned = true) => {
1876
+ const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder, overrideFilterOp, overrideFilterQuery, overrideEntrySearch, autoSelectReturned = true, overrideSearchAttr) => {
1813
1877
  const currentPageState = collectionPages[colUid];
1814
1878
  const size = customPageSize || currentPageState?.pageSize || defaultPageSize;
1815
1879
  const currentSort = collectionSorts[colUid] || { field: "createdAt", order: "desc" };
@@ -1818,6 +1882,7 @@ const BulkUpdater = () => {
1818
1882
  const op = overrideFilterOp !== void 0 ? overrideFilterOp : filterOperator;
1819
1883
  const q = overrideFilterQuery !== void 0 ? overrideFilterQuery : filterQuery;
1820
1884
  const s = overrideEntrySearch !== void 0 ? overrideEntrySearch : entrySearch;
1885
+ const searchAttr = overrideSearchAttr !== void 0 ? overrideSearchAttr : searchAttribute;
1821
1886
  setCollectionPages((prev) => ({
1822
1887
  ...prev,
1823
1888
  [colUid]: {
@@ -1831,6 +1896,9 @@ const BulkUpdater = () => {
1831
1896
  pageSize: String(size)
1832
1897
  });
1833
1898
  if (selectedAttribute) params.set("attribute", selectedAttribute);
1899
+ if (searchAttr && searchAttr !== selectedAttribute) {
1900
+ params.set("searchAttribute", searchAttr);
1901
+ }
1834
1902
  if (op && op !== "none") {
1835
1903
  params.set("filterOperator", op);
1836
1904
  if (q) params.set("filterQuery", q);
@@ -1860,7 +1928,7 @@ const BulkUpdater = () => {
1860
1928
  loading: false
1861
1929
  }
1862
1930
  }));
1863
- const isFilterActiveNow = op !== "none" && (["empty", "not_empty"].includes(op) || Boolean(q.trim())) || Boolean(s.trim());
1931
+ const isFilterActiveNow = Boolean(searchAttr && searchAttr !== selectedAttribute) || op !== "none" && (["empty", "not_empty"].includes(op) || Boolean(q.trim())) || Boolean(s.trim());
1864
1932
  if (isFilterActiveNow && autoSelectReturned) {
1865
1933
  setSelectedEntries((prev) => {
1866
1934
  const next = { ...prev };
@@ -1947,13 +2015,14 @@ const BulkUpdater = () => {
1947
2015
  }
1948
2016
  const timer = setTimeout(() => {
1949
2017
  if (collections.length > 0) {
1950
- const isFilterActiveNow = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
2018
+ const isFilterActiveNow = Boolean(searchAttribute && searchAttribute !== selectedAttribute) || filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1951
2019
  if (isFilterActiveNow) {
1952
2020
  setSelectedEntries({});
1953
2021
  }
2022
+ const targetSearchAttr = searchAttribute || selectedAttribute;
1954
2023
  const targetCols = collections.filter((col) => {
1955
2024
  if (visibleTypeUids[col.uid] === false) return false;
1956
- if (filterOperator !== "none" && !col.attributes.includes(selectedAttribute)) return false;
2025
+ if (filterOperator !== "none" && targetSearchAttr && !col.attributes.includes(targetSearchAttr)) return false;
1957
2026
  return true;
1958
2027
  });
1959
2028
  targetCols.forEach((col) => {
@@ -1966,13 +2035,14 @@ const BulkUpdater = () => {
1966
2035
  filterOperator,
1967
2036
  filterQuery,
1968
2037
  entrySearch,
1969
- true
2038
+ true,
2039
+ searchAttribute
1970
2040
  );
1971
2041
  });
1972
2042
  }
1973
2043
  }, 350);
1974
2044
  return () => clearTimeout(timer);
1975
- }, [filterOperator, filterQuery, entrySearch]);
2045
+ }, [searchAttribute, filterOperator, filterQuery, entrySearch]);
1976
2046
  const KEY_SEP = "@@@";
1977
2047
  const handleAttributeChange = (newAttr) => {
1978
2048
  if (newAttr !== selectedAttribute) {
@@ -2041,39 +2111,119 @@ const BulkUpdater = () => {
2041
2111
  setVisibleTypeUids(newVis);
2042
2112
  };
2043
2113
  const [attributeSearch, setAttributeSearch] = useState("");
2114
+ const [searchAttributeSearch, setSearchAttributeSearch] = useState("");
2115
+ const getAttrDepth = (attr) => attributesMeta[attr]?.depth ?? (attr.includes(".") ? attr.split(".").length - 1 : 0);
2116
+ const depthFilteredAttributes = useMemo(() => {
2117
+ return attributes.filter((attr) => getAttrDepth(attr) <= componentDepth);
2118
+ }, [attributes, componentDepth, attributesMeta]);
2044
2119
  const filteredAttributes = useMemo(() => {
2045
2120
  if (!attributeSearch.trim()) {
2046
- return attributes;
2121
+ return depthFilteredAttributes;
2047
2122
  }
2048
2123
  const q = attributeSearch.toLowerCase().trim();
2049
2124
  const cleanQ = q.replace(/[^a-z0-9]/g, "");
2050
- return attributes.filter((attr) => {
2125
+ return depthFilteredAttributes.filter((attr) => {
2051
2126
  const lower = attr.toLowerCase();
2052
2127
  const cleanLower = lower.replace(/[^a-z0-9]/g, "");
2053
2128
  const rel = relationsMeta[attr];
2054
2129
  const relTarget = (rel?.targetDisplayName || "").toLowerCase();
2055
2130
  return lower.includes(q) || cleanLower.includes(cleanQ) || relTarget.includes(q);
2056
2131
  });
2057
- }, [attributes, attributeSearch, relationsMeta]);
2132
+ }, [depthFilteredAttributes, attributeSearch, relationsMeta]);
2133
+ const filteredSearchAttributes = useMemo(() => {
2134
+ if (!searchAttributeSearch.trim()) {
2135
+ return depthFilteredAttributes;
2136
+ }
2137
+ const q = searchAttributeSearch.toLowerCase().trim();
2138
+ const cleanQ = q.replace(/[^a-z0-9]/g, "");
2139
+ return depthFilteredAttributes.filter((attr) => {
2140
+ const lower = attr.toLowerCase();
2141
+ const cleanLower = lower.replace(/[^a-z0-9]/g, "");
2142
+ const rel = relationsMeta[attr];
2143
+ const relTarget = (rel?.targetDisplayName || "").toLowerCase();
2144
+ return lower.includes(q) || cleanLower.includes(cleanQ) || relTarget.includes(q);
2145
+ });
2146
+ }, [depthFilteredAttributes, searchAttributeSearch, relationsMeta]);
2058
2147
  const memoizedComboboxOptions = useMemo(() => {
2059
2148
  if (filteredAttributes.length === 0) {
2060
2149
  return /* @__PURE__ */ jsx(ComboboxOption, { value: "__none__", disabled: true, textValue: "No matching attributes found", children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
2061
2150
  'No matching attributes found for "',
2062
2151
  attributeSearch,
2063
- '"'
2152
+ '" (Depth L',
2153
+ componentDepth,
2154
+ ")"
2064
2155
  ] }) });
2065
2156
  }
2066
2157
  return filteredAttributes.map((attr) => {
2067
2158
  const rel = relationsMeta[attr];
2159
+ const depth = getAttrDepth(attr);
2068
2160
  return /* @__PURE__ */ jsx(ComboboxOption, { value: attr, textValue: attr, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2069
2161
  /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: attr === selectedAttribute ? "bold" : "regular", children: attr }),
2070
2162
  rel ? /* @__PURE__ */ jsxs(Badge, { backgroundColor: "secondary100", textColor: "secondary700", size: "S", children: [
2071
2163
  "relation ➔ ",
2072
2164
  rel.targetDisplayName
2073
- ] }) : attr.includes(".") ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral700", size: "S", children: "component" }) : null
2165
+ ] }) : depth > 0 ? /* @__PURE__ */ jsxs(Badge, { backgroundColor: depth === 1 ? "neutral150" : depth === 2 ? "primary100" : "secondary100", textColor: depth === 1 ? "neutral700" : depth === 2 ? "primary700" : "secondary700", size: "S", children: [
2166
+ "L",
2167
+ depth,
2168
+ " component"
2169
+ ] }) : null
2074
2170
  ] }) }, attr);
2075
2171
  });
2076
- }, [filteredAttributes, relationsMeta, selectedAttribute, attributeSearch]);
2172
+ }, [filteredAttributes, relationsMeta, selectedAttribute, attributeSearch, componentDepth, attributesMeta]);
2173
+ const memoizedSearchComboboxOptions = useMemo(() => {
2174
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2175
+ /* @__PURE__ */ jsx(
2176
+ ComboboxOption,
2177
+ {
2178
+ value: "__default__",
2179
+ textValue: `Same as Update Attribute (${selectedAttribute || "None"})`,
2180
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2181
+ /* @__PURE__ */ jsxs(Typography, { variant: "omega", fontStyle: "italic", textColor: "neutral600", children: [
2182
+ "-- Same as Update Attribute (",
2183
+ selectedAttribute || "None",
2184
+ ") --"
2185
+ ] }),
2186
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", size: "S", children: "Default" })
2187
+ ] })
2188
+ }
2189
+ ),
2190
+ filteredSearchAttributes.map((attr) => {
2191
+ const rel = relationsMeta[attr];
2192
+ const depth = getAttrDepth(attr);
2193
+ return /* @__PURE__ */ jsx(ComboboxOption, { value: attr, textValue: attr, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2194
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: attr === searchAttribute ? "bold" : "regular", children: attr }),
2195
+ rel ? /* @__PURE__ */ jsxs(Badge, { backgroundColor: "secondary100", textColor: "secondary700", size: "S", children: [
2196
+ "relation ➔ ",
2197
+ rel.targetDisplayName
2198
+ ] }) : depth > 0 ? /* @__PURE__ */ jsxs(Badge, { backgroundColor: depth === 1 ? "neutral150" : depth === 2 ? "primary100" : "secondary100", textColor: depth === 1 ? "neutral700" : depth === 2 ? "primary700" : "secondary700", size: "S", children: [
2199
+ "L",
2200
+ depth,
2201
+ " component"
2202
+ ] }) : null
2203
+ ] }) }, `search_${attr}`);
2204
+ })
2205
+ ] });
2206
+ }, [filteredSearchAttributes, relationsMeta, searchAttribute, selectedAttribute, componentDepth, attributesMeta]);
2207
+ const handleSearchAttributeChange = (newAttr) => {
2208
+ const finalAttr = newAttr === "__default__" ? "" : newAttr;
2209
+ setSearchAttribute(finalAttr);
2210
+ if (collections.length > 0) {
2211
+ collections.forEach((col) => {
2212
+ loadCollectionPage(
2213
+ col.uid,
2214
+ 1,
2215
+ void 0,
2216
+ void 0,
2217
+ void 0,
2218
+ filterOperator,
2219
+ filterQuery,
2220
+ entrySearch,
2221
+ true,
2222
+ finalAttr
2223
+ );
2224
+ });
2225
+ }
2226
+ };
2077
2227
  const handleGlobalPageSizeChange = (newSize) => {
2078
2228
  const capped = Math.min(500, Math.max(1, newSize));
2079
2229
  setDefaultPageSize(capped);
@@ -2164,12 +2314,14 @@ const BulkUpdater = () => {
2164
2314
  setSelectedEntries(updated);
2165
2315
  };
2166
2316
  const clearFilter = () => {
2317
+ setSearchAttribute("");
2318
+ setSearchAttributeSearch("");
2167
2319
  setFilterOperator("none");
2168
2320
  setFilterQuery("");
2169
2321
  setEntrySearch("");
2170
2322
  setSelectedEntries({});
2171
2323
  collections.forEach((col) => {
2172
- loadCollectionPage(col.uid, 1, void 0, void 0, void 0, "none", "", "", false);
2324
+ loadCollectionPage(col.uid, 1, void 0, void 0, void 0, "none", "", "", false, "");
2173
2325
  });
2174
2326
  };
2175
2327
  const isCollectionFullySelected = (col) => {
@@ -2431,6 +2583,57 @@ const BulkUpdater = () => {
2431
2583
  setMakerNotesInput("");
2432
2584
  }
2433
2585
  };
2586
+ const handleBulkExportSelected = async () => {
2587
+ const targets = [];
2588
+ Object.keys(selectedEntries).forEach((key) => {
2589
+ if (selectedEntries[key]) {
2590
+ const parts = key.split(KEY_SEP);
2591
+ if (parts.length === 2) {
2592
+ targets.push({ uid: parts[0], documentId: parts[1] });
2593
+ }
2594
+ }
2595
+ });
2596
+ if (targets.length === 0) {
2597
+ toggleNotification({
2598
+ type: "warning",
2599
+ message: "Please select at least one entry to export"
2600
+ });
2601
+ return;
2602
+ }
2603
+ try {
2604
+ setBulkExporting(true);
2605
+ const res = await post("/bulk-updater/csv/export-bulk", { selectedEntries: targets });
2606
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
2607
+ if (!csvStr) {
2608
+ toggleNotification({
2609
+ type: "warning",
2610
+ message: "No CSV content returned for selected entries"
2611
+ });
2612
+ return;
2613
+ }
2614
+ const safeContent = csvStr.startsWith("\uFEFF") ? csvStr : "\uFEFF" + csvStr;
2615
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
2616
+ const url = URL.createObjectURL(blob);
2617
+ const link = document.createElement("a");
2618
+ link.href = url;
2619
+ link.setAttribute("download", res.data?.filename || `bulk_export_selected_${targets.length}.csv`);
2620
+ document.body.appendChild(link);
2621
+ link.click();
2622
+ document.body.removeChild(link);
2623
+ URL.revokeObjectURL(url);
2624
+ toggleNotification({
2625
+ type: "success",
2626
+ message: `Successfully exported ${res.data?.count || targets.length} selected records to Excel CSV`
2627
+ });
2628
+ } catch (e) {
2629
+ toggleNotification({
2630
+ type: "danger",
2631
+ message: e.message || "Failed exporting selected records to CSV"
2632
+ });
2633
+ } finally {
2634
+ setBulkExporting(false);
2635
+ }
2636
+ };
2434
2637
  const handleOpenPublishModal = () => {
2435
2638
  const totalSelected = Object.values(selectedEntries).filter(Boolean).length;
2436
2639
  if (totalSelected === 0) {
@@ -2914,7 +3117,29 @@ const BulkUpdater = () => {
2914
3117
  marginBottom: 6,
2915
3118
  style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
2916
3119
  children: [
2917
- /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", marginBottom: 4, fontWeight: "semiBold", children: "1. Configure Attribute Update" }),
3120
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, wrap: "wrap", gap: 3, children: [
3121
+ /* @__PURE__ */ jsxs(Box, { children: [
3122
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", fontWeight: "semiBold", children: "1. Configure Attribute Update" }),
3123
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Select the target attribute to update across collections." })
3124
+ ] }),
3125
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3126
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral700", fontWeight: "bold", children: "Component Nesting Depth:" }),
3127
+ [1, 2, 3, 4, 5].map((lvl) => /* @__PURE__ */ jsxs(
3128
+ Button,
3129
+ {
3130
+ variant: componentDepth === lvl ? "default" : "tertiary",
3131
+ size: "S",
3132
+ onClick: () => handleComponentDepthChange(lvl),
3133
+ title: `Discover nested component attributes up to Level ${lvl}`,
3134
+ children: [
3135
+ "Level ",
3136
+ lvl
3137
+ ]
3138
+ },
3139
+ `depth_cfg_${lvl}`
3140
+ ))
3141
+ ] })
3142
+ ] }),
2918
3143
  /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-end", wrap: "wrap", children: [
2919
3144
  /* @__PURE__ */ jsx(Box, { style: { minWidth: 280, flex: 1, maxWidth: 440 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
2920
3145
  /* @__PURE__ */ jsx(Field.Label, { children: "Attribute to Update" }),
@@ -3074,6 +3299,23 @@ const BulkUpdater = () => {
3074
3299
  ")"
3075
3300
  ]
3076
3301
  }
3302
+ ),
3303
+ /* @__PURE__ */ jsxs(
3304
+ Button,
3305
+ {
3306
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
3307
+ variant: "tertiary",
3308
+ size: "L",
3309
+ onClick: handleBulkExportSelected,
3310
+ loading: bulkExporting,
3311
+ disabled: loading || totalSelectedCount === 0,
3312
+ title: "Export selected entries to Excel-compatible CSV with full nested component support",
3313
+ children: [
3314
+ "Export Selected (",
3315
+ totalSelectedCount,
3316
+ ")"
3317
+ ]
3318
+ }
3077
3319
  )
3078
3320
  ] })
3079
3321
  ] }),
@@ -3228,26 +3470,83 @@ const BulkUpdater = () => {
3228
3470
  style: { width: "100%", maxWidth: "100%", boxSizing: "border-box" },
3229
3471
  children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
3230
3472
  /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, children: [
3231
- /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
3473
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3232
3474
  /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: "Search & Filter Targets:" }),
3233
3475
  isFilterActive && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
3234
3476
  matchingEntriesCount,
3235
3477
  " matching ",
3236
3478
  matchingEntriesCount === 1 ? "entry" : "entries",
3237
3479
  " found in database"
3480
+ ] }),
3481
+ searchAttribute && searchAttribute !== selectedAttribute && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: [
3482
+ "Searching by: ",
3483
+ searchAttribute,
3484
+ " → Target to update: ",
3485
+ selectedAttribute
3238
3486
  ] })
3239
3487
  ] }),
3240
- isFilterActive && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Search & Filter" })
3488
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3489
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
3490
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Depth:" }),
3491
+ [1, 2, 3, 4, 5].map((lvl) => /* @__PURE__ */ jsxs(
3492
+ Button,
3493
+ {
3494
+ variant: componentDepth === lvl ? "default" : "tertiary",
3495
+ size: "S",
3496
+ onClick: () => handleComponentDepthChange(lvl),
3497
+ title: `Show component attributes up to Level ${lvl}`,
3498
+ style: { minWidth: 26, padding: "2px 6px" },
3499
+ children: [
3500
+ "L",
3501
+ lvl
3502
+ ]
3503
+ },
3504
+ `s2_depth_${lvl}`
3505
+ ))
3506
+ ] }),
3507
+ isFilterActive && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Search & Filter" })
3508
+ ] })
3241
3509
  ] }),
3242
3510
  /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-end", wrap: "wrap", children: [
3243
- /* @__PURE__ */ jsx(Box, { style: { width: 220, minWidth: 180 }, children: /* @__PURE__ */ jsxs(
3511
+ /* @__PURE__ */ jsx(Box, { style: { width: 280, minWidth: 230 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
3512
+ /* @__PURE__ */ jsx(Field.Label, { children: "Search Attribute" }),
3513
+ /* @__PURE__ */ jsx(
3514
+ Combobox,
3515
+ {
3516
+ autocomplete: "none",
3517
+ placeholder: searchAttribute || `Same as Update (${selectedAttribute || "None"})`,
3518
+ value: searchAttribute || "__default__",
3519
+ clearLabel: "Reset search attribute",
3520
+ onClear: () => {
3521
+ handleSearchAttributeChange("");
3522
+ setSearchAttributeSearch("");
3523
+ },
3524
+ onInputChange: (e) => {
3525
+ setSearchAttributeSearch(e.target.value);
3526
+ },
3527
+ onOpenChange: (isOpen) => {
3528
+ if (!isOpen) {
3529
+ setSearchAttributeSearch("");
3530
+ }
3531
+ },
3532
+ onChange: (val) => {
3533
+ if (val && typeof val === "string" && val !== "__none__") {
3534
+ handleSearchAttributeChange(val);
3535
+ setSearchAttributeSearch("");
3536
+ }
3537
+ },
3538
+ children: memoizedSearchComboboxOptions
3539
+ }
3540
+ )
3541
+ ] }) }),
3542
+ /* @__PURE__ */ jsx(Box, { style: { width: 200, minWidth: 170 }, children: /* @__PURE__ */ jsxs(
3244
3543
  SingleSelect,
3245
3544
  {
3246
- label: selectedAttribute ? `Filter condition for "${selectedAttribute}"` : "Filter condition",
3545
+ label: effectiveSearchAttribute ? `Condition for "${effectiveSearchAttribute}"` : "Filter condition",
3247
3546
  value: filterOperator,
3248
3547
  onChange: (val) => setFilterOperator(String(val)),
3249
3548
  children: [
3250
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "none", children: "Show All (No Attribute Filter)" }),
3549
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "none", children: "Show All (No Value Filter)" }),
3251
3550
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "empty", children: "Is Empty or Null" }),
3252
3551
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "not_empty", children: "Is Not Empty" }),
3253
3552
  /* @__PURE__ */ jsx(SingleSelectOption, { value: "equals", children: "Equals Exact Text" }),
@@ -3256,16 +3555,16 @@ const BulkUpdater = () => {
3256
3555
  ]
3257
3556
  }
3258
3557
  ) }),
3259
- (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsx(
3558
+ (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsx(
3260
3559
  TextInput,
3261
3560
  {
3262
- label: selectedAttribute ? `Value for "${selectedAttribute}"` : "Value to match",
3561
+ label: effectiveSearchAttribute ? `Value for "${effectiveSearchAttribute}"` : "Value to match",
3263
3562
  placeholder: "Type value to match...",
3264
3563
  value: filterQuery,
3265
3564
  onChange: (e) => setFilterQuery(e.target.value)
3266
3565
  }
3267
3566
  ) }),
3268
- /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsx(
3567
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsx(
3269
3568
  TextInput,
3270
3569
  {
3271
3570
  label: "Full-text Entry Search",
@@ -3463,11 +3762,11 @@ const BulkUpdater = () => {
3463
3762
  const pageSize = pageState ? pageState.pageSize : defaultPageSize;
3464
3763
  const hasAttr = selectedAttribute ? col.attributes.includes(selectedAttribute) : false;
3465
3764
  const visibleEntries = getVisibleEntries(col);
3466
- selectedAttribute ? entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length : 0;
3765
+ selectedAttribute ? entries.filter((e) => entryMatchesFilter(e, effectiveSearchAttribute)).length : 0;
3467
3766
  const isFullySelected = isCollectionFullySelected(col);
3468
3767
  const isPartiallySelected = isCollectionPartiallySelected(col);
3469
3768
  const currentSort = collectionSorts[col.uid] || { field: "createdAt", order: "desc" };
3470
- const availableColumns = getAvailableColumnsForCollection(col, selectedAttribute);
3769
+ const availableColumns = getAvailableColumnsForCollection(col, selectedAttribute, searchAttribute);
3471
3770
  const savedColKeys = collectionColumnVisibility[col.uid];
3472
3771
  const activeColumns = savedColKeys ? availableColumns.filter((c) => c.isPermanent || savedColKeys.includes(c.key)) : availableColumns.filter((c) => c.defaultVisible);
3473
3772
  return /* @__PURE__ */ jsxs(
@@ -3575,6 +3874,10 @@ const BulkUpdater = () => {
3575
3874
  if (entrySearch && entrySearch.trim()) {
3576
3875
  params.set("search", entrySearch.trim());
3577
3876
  }
3877
+ const colSelectedDocIds = col.entries.filter((e) => selectedEntries[`${col.uid}${KEY_SEP}${e.documentId}`]).map((e) => e.documentId);
3878
+ if (colSelectedDocIds.length > 0) {
3879
+ params.set("documentIds", colSelectedDocIds.join(","));
3880
+ }
3578
3881
  const res = await get(`/bulk-updater/csv/export/${col.uid}?${params.toString()}`);
3579
3882
  const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
3580
3883
  if (!csvStr) {
@@ -3584,7 +3887,8 @@ const BulkUpdater = () => {
3584
3887
  });
3585
3888
  return;
3586
3889
  }
3587
- const blob = new Blob([csvStr], { type: "text/csv;charset=utf-8;" });
3890
+ const safeContent = csvStr.startsWith("\uFEFF") ? csvStr : "\uFEFF" + csvStr;
3891
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
3588
3892
  const url = URL.createObjectURL(blob);
3589
3893
  const link = document.createElement("a");
3590
3894
  link.href = url;
@@ -3595,7 +3899,7 @@ const BulkUpdater = () => {
3595
3899
  URL.revokeObjectURL(url);
3596
3900
  toggleNotification({
3597
3901
  type: "success",
3598
- message: `Exported ${col.displayName} entries to CSV`
3902
+ message: `Exported ${res.data?.count ?? (colSelectedDocIds.length || col.entries.length)} ${col.displayName} entries to Excel CSV`
3599
3903
  });
3600
3904
  } catch (e) {
3601
3905
  toggleNotification({
@@ -3672,7 +3976,7 @@ const BulkUpdater = () => {
3672
3976
  ),
3673
3977
  /* @__PURE__ */ jsx(Box, { className: "bulk-updater-table-container", style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" }, children: /* @__PURE__ */ jsxs(Table, { colCount: activeColumns.length, rowCount: visibleEntries.length + 1, style: { width: "100%" }, children: [
3674
3978
  /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsx(Tr, { children: activeColumns.map(
3675
- (colDef) => renderHeaderCell(colDef, col, currentSort, selectedAttribute, handleHeaderSortClick)
3979
+ (colDef) => renderHeaderCell(colDef, col, currentSort, selectedAttribute, searchAttribute, handleHeaderSortClick)
3676
3980
  ) }) }),
3677
3981
  /* @__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) => {
3678
3982
  const key = `${col.uid}${KEY_SEP}${entry.documentId}`;
@@ -3690,6 +3994,7 @@ const BulkUpdater = () => {
3690
3994
  entry,
3691
3995
  col,
3692
3996
  selectedAttribute,
3997
+ searchAttribute,
3693
3998
  isMatch,
3694
3999
  hasAttr,
3695
4000
  isSelected,
@@ -4665,4 +4970,4 @@ export {
4665
4970
  getAvailableColumnsForCollection,
4666
4971
  getNestedValue
4667
4972
  };
4668
- //# sourceMappingURL=BulkUpdater-DvCSr1vU.mjs.map
4973
+ //# sourceMappingURL=BulkUpdater-DowSmA56.mjs.map