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.
@@ -5,7 +5,7 @@ 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
+ const ValidationRulesTab = require("./ValidationRulesTab-BLZWPGrZ.js");
9
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
10
10
  const React__default = /* @__PURE__ */ _interopDefault(React);
11
11
  function CsvManagerTab() {
@@ -118,7 +118,8 @@ function CsvManagerTab() {
118
118
  });
119
119
  return;
120
120
  }
121
- const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
121
+ const safeContent = content.startsWith("\uFEFF") ? content : "\uFEFF" + content;
122
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
122
123
  const url = URL.createObjectURL(blob);
123
124
  const link = document.createElement("a");
124
125
  link.href = url;
@@ -507,7 +508,9 @@ function formatVal(v) {
507
508
  return String(v);
508
509
  }
509
510
  function downloadFile(content, filename, mimeType = "text/csv;charset=utf-8;") {
510
- const blob = new Blob([content], { type: mimeType });
511
+ const isCsv = mimeType.includes("csv") || filename.endsWith(".csv");
512
+ const safeContent = isCsv && !content.startsWith("\uFEFF") ? "\uFEFF" + content : content;
513
+ const blob = new Blob([safeContent], { type: mimeType });
511
514
  const url = URL.createObjectURL(blob);
512
515
  const a = document.createElement("a");
513
516
  a.href = url;
@@ -629,7 +632,7 @@ function ProcessingDashboardModal({
629
632
  ].join(",")
630
633
  )
631
634
  ];
632
- downloadFile(csvRows.join("\n"), `bulk_update_report_${targetId}.csv`);
635
+ downloadFile(csvRows.join("\r\n") + "\r\n", `bulk_update_report_${targetId}.csv`);
633
636
  toggleNotification({
634
637
  type: "success",
635
638
  message: "Execution report downloaded successfully"
@@ -1455,7 +1458,7 @@ function getNestedValue(obj, path) {
1455
1458
  if (!path.includes(".")) return obj[path];
1456
1459
  return path.split(".").reduce((acc, part) => acc && typeof acc === "object" ? acc[part] : void 0, obj);
1457
1460
  }
1458
- function getAvailableColumnsForCollection(col, selectedAttribute) {
1461
+ function getAvailableColumnsForCollection(col, selectedAttribute, searchAttribute) {
1459
1462
  const cols = [
1460
1463
  {
1461
1464
  key: "select",
@@ -1472,7 +1475,7 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1472
1475
  ];
1473
1476
  const candidateAttrs = col.attributes || [];
1474
1477
  candidateAttrs.forEach((attr) => {
1475
- if (attr === "name" || attr === "title" || attr === selectedAttribute) {
1478
+ if (attr === "name" || attr === "title" || attr === selectedAttribute || searchAttribute && attr === searchAttribute) {
1476
1479
  return;
1477
1480
  }
1478
1481
  const formattedLabel = attr.includes(".") ? attr.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" > ") : attr.charAt(0).toUpperCase() + attr.slice(1);
@@ -1483,9 +1486,19 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1483
1486
  defaultVisible: ["department", "email", "location"].includes(attr)
1484
1487
  });
1485
1488
  });
1489
+ if (searchAttribute && searchAttribute !== selectedAttribute) {
1490
+ const formattedSearchLabel = searchAttribute.includes(".") ? searchAttribute.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" > ") : searchAttribute;
1491
+ cols.push({
1492
+ key: "search_attr",
1493
+ label: `Search: "${formattedSearchLabel}"`,
1494
+ sortable: !col.isSingleType && col.attributes.includes(searchAttribute),
1495
+ defaultVisible: true
1496
+ });
1497
+ }
1498
+ const targetLabel = searchAttribute && searchAttribute !== selectedAttribute ? `Update Target: "${selectedAttribute}"` : `Current "${selectedAttribute}"`;
1486
1499
  cols.push({
1487
1500
  key: "target_attr",
1488
- label: `Current "${selectedAttribute}"`,
1501
+ label: targetLabel,
1489
1502
  sortable: !col.isSingleType && col.attributes.includes(selectedAttribute),
1490
1503
  defaultVisible: true
1491
1504
  });
@@ -1497,17 +1510,19 @@ function getAvailableColumnsForCollection(col, selectedAttribute) {
1497
1510
  });
1498
1511
  return cols;
1499
1512
  }
1500
- function renderHeaderCell(colDef, col, currentSort, selectedAttribute, onSort) {
1513
+ function renderHeaderCell(colDef, col, currentSort, selectedAttribute, searchAttribute, onSort) {
1501
1514
  if (colDef.key === "select") {
1502
1515
  return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { width: 48 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: "Select" }) }, "select");
1503
1516
  }
1504
- 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_", "");
1517
+ 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_", "");
1505
1518
  const sortArrow = isCurrentSorted ? currentSort.order === "asc" ? "▲" : "▼" : "⇅";
1506
1519
  const handleClick = () => {
1507
1520
  if (!colDef.sortable) return;
1508
1521
  if (colDef.key === "identifier") {
1509
1522
  const field = col.attributes.includes("name") ? "name" : col.attributes.includes("title") ? "title" : "createdAt";
1510
1523
  onSort(col.uid, field);
1524
+ } else if (colDef.key === "search_attr") {
1525
+ onSort(col.uid, searchAttribute);
1511
1526
  } else if (colDef.key === "target_attr") {
1512
1527
  onSort(col.uid, selectedAttribute);
1513
1528
  } else if (colDef.key.startsWith("attr_")) {
@@ -1543,7 +1558,7 @@ function formatDisplayValue(val) {
1543
1558
  }
1544
1559
  return String(val);
1545
1560
  }
1546
- function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAttr, isSelected, onToggleSelect) {
1561
+ function renderCellContent(colDef, entry, col, selectedAttribute, searchAttribute, isMatch, hasAttr, isSelected, onToggleSelect) {
1547
1562
  switch (colDef.key) {
1548
1563
  case "select":
1549
1564
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1560,6 +1575,27 @@ function renderCellContent(colDef, entry, col, selectedAttribute, isMatch, hasAt
1560
1575
  entry.email && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", display: "block", children: entry.email })
1561
1576
  ] });
1562
1577
  }
1578
+ case "search_attr": {
1579
+ const val = getNestedValue(entry, searchAttribute);
1580
+ const displayStr = formatDisplayValue(val);
1581
+ const isUnset = displayStr === "(empty / not set)" || displayStr === "(empty / unlinked)";
1582
+ return /* @__PURE__ */ jsxRuntime.jsx(
1583
+ designSystem.Typography,
1584
+ {
1585
+ textColor: isUnset ? "neutral500" : "primary700",
1586
+ fontWeight: "semiBold",
1587
+ style: {
1588
+ fontStyle: isUnset ? "italic" : "normal",
1589
+ display: "inline-block",
1590
+ maxWidth: 260,
1591
+ overflow: "hidden",
1592
+ textOverflow: "ellipsis",
1593
+ whiteSpace: "nowrap"
1594
+ },
1595
+ children: displayStr
1596
+ }
1597
+ );
1598
+ }
1563
1599
  case "target_attr": {
1564
1600
  const val = getNestedValue(entry, selectedAttribute);
1565
1601
  const displayStr = formatDisplayValue(val);
@@ -1650,6 +1686,7 @@ const BulkUpdater = () => {
1650
1686
  const [showPublishConfirmModal, setShowPublishConfirmModal] = React.useState(false);
1651
1687
  const [publishing, setPublishing] = React.useState(false);
1652
1688
  const [publishMakerNotesInput, setPublishMakerNotesInput] = React.useState("");
1689
+ const [bulkExporting, setBulkExporting] = React.useState(false);
1653
1690
  const MODELS_CACHE_KEY = "strapi_bulk_updater_models_cache";
1654
1691
  const LAST_ATTR_KEY = "strapi_bulk_updater_last_attr";
1655
1692
  const getInitialAttribute = () => {
@@ -1683,10 +1720,34 @@ const BulkUpdater = () => {
1683
1720
  };
1684
1721
  const [useRegex, setUseRegex] = React.useState(false);
1685
1722
  const [regexPattern, setRegexPattern] = React.useState("");
1723
+ const DEPTH_STORAGE_KEY = "strapi_bulk_updater_component_depth";
1724
+ const [componentDepth, setComponentDepth] = React.useState(() => {
1725
+ try {
1726
+ const stored = localStorage.getItem(DEPTH_STORAGE_KEY);
1727
+ if (stored) {
1728
+ const val = Number(stored);
1729
+ if (val >= 1 && val <= 5) return val;
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ return 3;
1734
+ });
1735
+ const handleComponentDepthChange = (depth) => {
1736
+ const capped = Math.min(5, Math.max(1, depth));
1737
+ setComponentDepth(capped);
1738
+ try {
1739
+ localStorage.setItem(DEPTH_STORAGE_KEY, String(capped));
1740
+ } catch {
1741
+ }
1742
+ };
1743
+ const [searchAttribute, setSearchAttribute] = React.useState("");
1744
+ const [searchAttributeInput, setSearchAttributeInput] = React.useState("");
1745
+ const [attributesMeta, setAttributesMeta] = React.useState({});
1746
+ const effectiveSearchAttribute = searchAttribute || selectedAttribute;
1686
1747
  const [filterOperator, setFilterOperator] = React.useState("none");
1687
1748
  const [filterQuery, setFilterQuery] = React.useState("");
1688
1749
  const [entrySearch, setEntrySearch] = React.useState("");
1689
- const isFilterActive = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1750
+ const isFilterActive = Boolean(searchAttribute && searchAttribute !== selectedAttribute) || filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1690
1751
  const [lastResults, setLastResults] = React.useState(null);
1691
1752
  const [updateSummary, setUpdateSummary] = React.useState(null);
1692
1753
  const [collectionPages, setCollectionPages] = React.useState({});
@@ -1766,6 +1827,9 @@ const BulkUpdater = () => {
1766
1827
  if (data.relationsMeta) {
1767
1828
  setRelationsMeta(data.relationsMeta);
1768
1829
  }
1830
+ if (data.attributesMeta) {
1831
+ setAttributesMeta(data.attributesMeta);
1832
+ }
1769
1833
  let storedAttr = null;
1770
1834
  try {
1771
1835
  storedAttr = localStorage.getItem(LAST_ATTR_KEY);
@@ -1813,7 +1877,7 @@ const BulkUpdater = () => {
1813
1877
  setLoading(false);
1814
1878
  }
1815
1879
  };
1816
- const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder, overrideFilterOp, overrideFilterQuery, overrideEntrySearch, autoSelectReturned = true) => {
1880
+ const loadCollectionPage = async (colUid, targetPage, customPageSize, customSortField, customSortOrder, overrideFilterOp, overrideFilterQuery, overrideEntrySearch, autoSelectReturned = true, overrideSearchAttr) => {
1817
1881
  const currentPageState = collectionPages[colUid];
1818
1882
  const size = customPageSize || currentPageState?.pageSize || defaultPageSize;
1819
1883
  const currentSort = collectionSorts[colUid] || { field: "createdAt", order: "desc" };
@@ -1822,6 +1886,7 @@ const BulkUpdater = () => {
1822
1886
  const op = overrideFilterOp !== void 0 ? overrideFilterOp : filterOperator;
1823
1887
  const q = overrideFilterQuery !== void 0 ? overrideFilterQuery : filterQuery;
1824
1888
  const s = overrideEntrySearch !== void 0 ? overrideEntrySearch : entrySearch;
1889
+ const searchAttr = overrideSearchAttr !== void 0 ? overrideSearchAttr : searchAttribute;
1825
1890
  setCollectionPages((prev) => ({
1826
1891
  ...prev,
1827
1892
  [colUid]: {
@@ -1835,6 +1900,9 @@ const BulkUpdater = () => {
1835
1900
  pageSize: String(size)
1836
1901
  });
1837
1902
  if (selectedAttribute) params.set("attribute", selectedAttribute);
1903
+ if (searchAttr && searchAttr !== selectedAttribute) {
1904
+ params.set("searchAttribute", searchAttr);
1905
+ }
1838
1906
  if (op && op !== "none") {
1839
1907
  params.set("filterOperator", op);
1840
1908
  if (q) params.set("filterQuery", q);
@@ -1864,7 +1932,7 @@ const BulkUpdater = () => {
1864
1932
  loading: false
1865
1933
  }
1866
1934
  }));
1867
- const isFilterActiveNow = op !== "none" && (["empty", "not_empty"].includes(op) || Boolean(q.trim())) || Boolean(s.trim());
1935
+ const isFilterActiveNow = Boolean(searchAttr && searchAttr !== selectedAttribute) || op !== "none" && (["empty", "not_empty"].includes(op) || Boolean(q.trim())) || Boolean(s.trim());
1868
1936
  if (isFilterActiveNow && autoSelectReturned) {
1869
1937
  setSelectedEntries((prev) => {
1870
1938
  const next = { ...prev };
@@ -1951,13 +2019,14 @@ const BulkUpdater = () => {
1951
2019
  }
1952
2020
  const timer = setTimeout(() => {
1953
2021
  if (collections.length > 0) {
1954
- const isFilterActiveNow = filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
2022
+ const isFilterActiveNow = Boolean(searchAttribute && searchAttribute !== selectedAttribute) || filterOperator !== "none" && (["empty", "not_empty"].includes(filterOperator) || Boolean(filterQuery.trim())) || Boolean(entrySearch.trim());
1955
2023
  if (isFilterActiveNow) {
1956
2024
  setSelectedEntries({});
1957
2025
  }
2026
+ const targetSearchAttr = searchAttribute || selectedAttribute;
1958
2027
  const targetCols = collections.filter((col) => {
1959
2028
  if (visibleTypeUids[col.uid] === false) return false;
1960
- if (filterOperator !== "none" && !col.attributes.includes(selectedAttribute)) return false;
2029
+ if (filterOperator !== "none" && targetSearchAttr && !col.attributes.includes(targetSearchAttr)) return false;
1961
2030
  return true;
1962
2031
  });
1963
2032
  targetCols.forEach((col) => {
@@ -1970,13 +2039,14 @@ const BulkUpdater = () => {
1970
2039
  filterOperator,
1971
2040
  filterQuery,
1972
2041
  entrySearch,
1973
- true
2042
+ true,
2043
+ searchAttribute
1974
2044
  );
1975
2045
  });
1976
2046
  }
1977
2047
  }, 350);
1978
2048
  return () => clearTimeout(timer);
1979
- }, [filterOperator, filterQuery, entrySearch]);
2049
+ }, [searchAttribute, filterOperator, filterQuery, entrySearch]);
1980
2050
  const KEY_SEP = "@@@";
1981
2051
  const handleAttributeChange = (newAttr) => {
1982
2052
  if (newAttr !== selectedAttribute) {
@@ -2045,39 +2115,119 @@ const BulkUpdater = () => {
2045
2115
  setVisibleTypeUids(newVis);
2046
2116
  };
2047
2117
  const [attributeSearch, setAttributeSearch] = React.useState("");
2118
+ const [searchAttributeSearch, setSearchAttributeSearch] = React.useState("");
2119
+ const getAttrDepth = (attr) => attributesMeta[attr]?.depth ?? (attr.includes(".") ? attr.split(".").length - 1 : 0);
2120
+ const depthFilteredAttributes = React.useMemo(() => {
2121
+ return attributes.filter((attr) => getAttrDepth(attr) <= componentDepth);
2122
+ }, [attributes, componentDepth, attributesMeta]);
2048
2123
  const filteredAttributes = React.useMemo(() => {
2049
2124
  if (!attributeSearch.trim()) {
2050
- return attributes;
2125
+ return depthFilteredAttributes;
2051
2126
  }
2052
2127
  const q = attributeSearch.toLowerCase().trim();
2053
2128
  const cleanQ = q.replace(/[^a-z0-9]/g, "");
2054
- return attributes.filter((attr) => {
2129
+ return depthFilteredAttributes.filter((attr) => {
2055
2130
  const lower = attr.toLowerCase();
2056
2131
  const cleanLower = lower.replace(/[^a-z0-9]/g, "");
2057
2132
  const rel = relationsMeta[attr];
2058
2133
  const relTarget = (rel?.targetDisplayName || "").toLowerCase();
2059
2134
  return lower.includes(q) || cleanLower.includes(cleanQ) || relTarget.includes(q);
2060
2135
  });
2061
- }, [attributes, attributeSearch, relationsMeta]);
2136
+ }, [depthFilteredAttributes, attributeSearch, relationsMeta]);
2137
+ const filteredSearchAttributes = React.useMemo(() => {
2138
+ if (!searchAttributeSearch.trim()) {
2139
+ return depthFilteredAttributes;
2140
+ }
2141
+ const q = searchAttributeSearch.toLowerCase().trim();
2142
+ const cleanQ = q.replace(/[^a-z0-9]/g, "");
2143
+ return depthFilteredAttributes.filter((attr) => {
2144
+ const lower = attr.toLowerCase();
2145
+ const cleanLower = lower.replace(/[^a-z0-9]/g, "");
2146
+ const rel = relationsMeta[attr];
2147
+ const relTarget = (rel?.targetDisplayName || "").toLowerCase();
2148
+ return lower.includes(q) || cleanLower.includes(cleanQ) || relTarget.includes(q);
2149
+ });
2150
+ }, [depthFilteredAttributes, searchAttributeSearch, relationsMeta]);
2062
2151
  const memoizedComboboxOptions = React.useMemo(() => {
2063
2152
  if (filteredAttributes.length === 0) {
2064
2153
  return /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: "__none__", disabled: true, textValue: "No matching attributes found", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
2065
2154
  'No matching attributes found for "',
2066
2155
  attributeSearch,
2067
- '"'
2156
+ '" (Depth L',
2157
+ componentDepth,
2158
+ ")"
2068
2159
  ] }) });
2069
2160
  }
2070
2161
  return filteredAttributes.map((attr) => {
2071
2162
  const rel = relationsMeta[attr];
2163
+ const depth = getAttrDepth(attr);
2072
2164
  return /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: attr, textValue: attr, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2073
2165
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: attr === selectedAttribute ? "bold" : "regular", children: attr }),
2074
2166
  rel ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", size: "S", children: [
2075
2167
  "relation ➔ ",
2076
2168
  rel.targetDisplayName
2077
- ] }) : attr.includes(".") ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral700", size: "S", children: "component" }) : null
2169
+ ] }) : depth > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: depth === 1 ? "neutral150" : depth === 2 ? "primary100" : "secondary100", textColor: depth === 1 ? "neutral700" : depth === 2 ? "primary700" : "secondary700", size: "S", children: [
2170
+ "L",
2171
+ depth,
2172
+ " component"
2173
+ ] }) : null
2078
2174
  ] }) }, attr);
2079
2175
  });
2080
- }, [filteredAttributes, relationsMeta, selectedAttribute, attributeSearch]);
2176
+ }, [filteredAttributes, relationsMeta, selectedAttribute, attributeSearch, componentDepth, attributesMeta]);
2177
+ const memoizedSearchComboboxOptions = React.useMemo(() => {
2178
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2179
+ /* @__PURE__ */ jsxRuntime.jsx(
2180
+ designSystem.ComboboxOption,
2181
+ {
2182
+ value: "__default__",
2183
+ textValue: `Same as Update Attribute (${selectedAttribute || "None"})`,
2184
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2185
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "omega", fontStyle: "italic", textColor: "neutral600", children: [
2186
+ "-- Same as Update Attribute (",
2187
+ selectedAttribute || "None",
2188
+ ") --"
2189
+ ] }),
2190
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral600", size: "S", children: "Default" })
2191
+ ] })
2192
+ }
2193
+ ),
2194
+ filteredSearchAttributes.map((attr) => {
2195
+ const rel = relationsMeta[attr];
2196
+ const depth = getAttrDepth(attr);
2197
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: attr, textValue: attr, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", width: "100%", gap: 2, children: [
2198
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: attr === searchAttribute ? "bold" : "regular", children: attr }),
2199
+ rel ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", size: "S", children: [
2200
+ "relation ➔ ",
2201
+ rel.targetDisplayName
2202
+ ] }) : depth > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: depth === 1 ? "neutral150" : depth === 2 ? "primary100" : "secondary100", textColor: depth === 1 ? "neutral700" : depth === 2 ? "primary700" : "secondary700", size: "S", children: [
2203
+ "L",
2204
+ depth,
2205
+ " component"
2206
+ ] }) : null
2207
+ ] }) }, `search_${attr}`);
2208
+ })
2209
+ ] });
2210
+ }, [filteredSearchAttributes, relationsMeta, searchAttribute, selectedAttribute, componentDepth, attributesMeta]);
2211
+ const handleSearchAttributeChange = (newAttr) => {
2212
+ const finalAttr = newAttr === "__default__" ? "" : newAttr;
2213
+ setSearchAttribute(finalAttr);
2214
+ if (collections.length > 0) {
2215
+ collections.forEach((col) => {
2216
+ loadCollectionPage(
2217
+ col.uid,
2218
+ 1,
2219
+ void 0,
2220
+ void 0,
2221
+ void 0,
2222
+ filterOperator,
2223
+ filterQuery,
2224
+ entrySearch,
2225
+ true,
2226
+ finalAttr
2227
+ );
2228
+ });
2229
+ }
2230
+ };
2081
2231
  const handleGlobalPageSizeChange = (newSize) => {
2082
2232
  const capped = Math.min(500, Math.max(1, newSize));
2083
2233
  setDefaultPageSize(capped);
@@ -2168,12 +2318,14 @@ const BulkUpdater = () => {
2168
2318
  setSelectedEntries(updated);
2169
2319
  };
2170
2320
  const clearFilter = () => {
2321
+ setSearchAttribute("");
2322
+ setSearchAttributeSearch("");
2171
2323
  setFilterOperator("none");
2172
2324
  setFilterQuery("");
2173
2325
  setEntrySearch("");
2174
2326
  setSelectedEntries({});
2175
2327
  collections.forEach((col) => {
2176
- loadCollectionPage(col.uid, 1, void 0, void 0, void 0, "none", "", "", false);
2328
+ loadCollectionPage(col.uid, 1, void 0, void 0, void 0, "none", "", "", false, "");
2177
2329
  });
2178
2330
  };
2179
2331
  const isCollectionFullySelected = (col) => {
@@ -2435,6 +2587,57 @@ const BulkUpdater = () => {
2435
2587
  setMakerNotesInput("");
2436
2588
  }
2437
2589
  };
2590
+ const handleBulkExportSelected = async () => {
2591
+ const targets = [];
2592
+ Object.keys(selectedEntries).forEach((key) => {
2593
+ if (selectedEntries[key]) {
2594
+ const parts = key.split(KEY_SEP);
2595
+ if (parts.length === 2) {
2596
+ targets.push({ uid: parts[0], documentId: parts[1] });
2597
+ }
2598
+ }
2599
+ });
2600
+ if (targets.length === 0) {
2601
+ toggleNotification({
2602
+ type: "warning",
2603
+ message: "Please select at least one entry to export"
2604
+ });
2605
+ return;
2606
+ }
2607
+ try {
2608
+ setBulkExporting(true);
2609
+ const res = await post("/bulk-updater/csv/export-bulk", { selectedEntries: targets });
2610
+ const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
2611
+ if (!csvStr) {
2612
+ toggleNotification({
2613
+ type: "warning",
2614
+ message: "No CSV content returned for selected entries"
2615
+ });
2616
+ return;
2617
+ }
2618
+ const safeContent = csvStr.startsWith("\uFEFF") ? csvStr : "\uFEFF" + csvStr;
2619
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
2620
+ const url = URL.createObjectURL(blob);
2621
+ const link = document.createElement("a");
2622
+ link.href = url;
2623
+ link.setAttribute("download", res.data?.filename || `bulk_export_selected_${targets.length}.csv`);
2624
+ document.body.appendChild(link);
2625
+ link.click();
2626
+ document.body.removeChild(link);
2627
+ URL.revokeObjectURL(url);
2628
+ toggleNotification({
2629
+ type: "success",
2630
+ message: `Successfully exported ${res.data?.count || targets.length} selected records to Excel CSV`
2631
+ });
2632
+ } catch (e) {
2633
+ toggleNotification({
2634
+ type: "danger",
2635
+ message: e.message || "Failed exporting selected records to CSV"
2636
+ });
2637
+ } finally {
2638
+ setBulkExporting(false);
2639
+ }
2640
+ };
2438
2641
  const handleOpenPublishModal = () => {
2439
2642
  const totalSelected = Object.values(selectedEntries).filter(Boolean).length;
2440
2643
  if (totalSelected === 0) {
@@ -2918,7 +3121,29 @@ const BulkUpdater = () => {
2918
3121
  marginBottom: 6,
2919
3122
  style: { width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
2920
3123
  children: [
2921
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", tag: "h2", marginBottom: 4, fontWeight: "semiBold", children: "1. Configure Attribute Update" }),
3124
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, wrap: "wrap", gap: 3, children: [
3125
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
3126
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", tag: "h2", fontWeight: "semiBold", children: "1. Configure Attribute Update" }),
3127
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Select the target attribute to update across collections." })
3128
+ ] }),
3129
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3130
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral700", fontWeight: "bold", children: "Component Nesting Depth:" }),
3131
+ [1, 2, 3, 4, 5].map((lvl) => /* @__PURE__ */ jsxRuntime.jsxs(
3132
+ designSystem.Button,
3133
+ {
3134
+ variant: componentDepth === lvl ? "default" : "tertiary",
3135
+ size: "S",
3136
+ onClick: () => handleComponentDepthChange(lvl),
3137
+ title: `Discover nested component attributes up to Level ${lvl}`,
3138
+ children: [
3139
+ "Level ",
3140
+ lvl
3141
+ ]
3142
+ },
3143
+ `depth_cfg_${lvl}`
3144
+ ))
3145
+ ] })
3146
+ ] }),
2922
3147
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, alignItems: "flex-end", wrap: "wrap", children: [
2923
3148
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { minWidth: 280, flex: 1, maxWidth: 440 }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
2924
3149
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Attribute to Update" }),
@@ -3078,6 +3303,23 @@ const BulkUpdater = () => {
3078
3303
  ")"
3079
3304
  ]
3080
3305
  }
3306
+ ),
3307
+ /* @__PURE__ */ jsxRuntime.jsxs(
3308
+ designSystem.Button,
3309
+ {
3310
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
3311
+ variant: "tertiary",
3312
+ size: "L",
3313
+ onClick: handleBulkExportSelected,
3314
+ loading: bulkExporting,
3315
+ disabled: loading || totalSelectedCount === 0,
3316
+ title: "Export selected entries to Excel-compatible CSV with full nested component support",
3317
+ children: [
3318
+ "Export Selected (",
3319
+ totalSelectedCount,
3320
+ ")"
3321
+ ]
3322
+ }
3081
3323
  )
3082
3324
  ] })
3083
3325
  ] }),
@@ -3232,26 +3474,83 @@ const BulkUpdater = () => {
3232
3474
  style: { width: "100%", maxWidth: "100%", boxSizing: "border-box" },
3233
3475
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 3, children: [
3234
3476
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 2, children: [
3235
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
3477
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3236
3478
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", children: "Search & Filter Targets:" }),
3237
3479
  isFilterActive && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
3238
3480
  matchingEntriesCount,
3239
3481
  " matching ",
3240
3482
  matchingEntriesCount === 1 ? "entry" : "entries",
3241
3483
  " found in database"
3484
+ ] }),
3485
+ searchAttribute && searchAttribute !== selectedAttribute && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: [
3486
+ "Searching by: ",
3487
+ searchAttribute,
3488
+ " → Target to update: ",
3489
+ selectedAttribute
3242
3490
  ] })
3243
3491
  ] }),
3244
- isFilterActive && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Search & Filter" })
3492
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
3493
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
3494
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Depth:" }),
3495
+ [1, 2, 3, 4, 5].map((lvl) => /* @__PURE__ */ jsxRuntime.jsxs(
3496
+ designSystem.Button,
3497
+ {
3498
+ variant: componentDepth === lvl ? "default" : "tertiary",
3499
+ size: "S",
3500
+ onClick: () => handleComponentDepthChange(lvl),
3501
+ title: `Show component attributes up to Level ${lvl}`,
3502
+ style: { minWidth: 26, padding: "2px 6px" },
3503
+ children: [
3504
+ "L",
3505
+ lvl
3506
+ ]
3507
+ },
3508
+ `s2_depth_${lvl}`
3509
+ ))
3510
+ ] }),
3511
+ isFilterActive && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: clearFilter, children: "Clear Search & Filter" })
3512
+ ] })
3245
3513
  ] }),
3246
3514
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "flex-end", wrap: "wrap", children: [
3247
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 220, minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
3515
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 280, minWidth: 230 }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
3516
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Search Attribute" }),
3517
+ /* @__PURE__ */ jsxRuntime.jsx(
3518
+ designSystem.Combobox,
3519
+ {
3520
+ autocomplete: "none",
3521
+ placeholder: searchAttribute || `Same as Update (${selectedAttribute || "None"})`,
3522
+ value: searchAttribute || "__default__",
3523
+ clearLabel: "Reset search attribute",
3524
+ onClear: () => {
3525
+ handleSearchAttributeChange("");
3526
+ setSearchAttributeSearch("");
3527
+ },
3528
+ onInputChange: (e) => {
3529
+ setSearchAttributeSearch(e.target.value);
3530
+ },
3531
+ onOpenChange: (isOpen) => {
3532
+ if (!isOpen) {
3533
+ setSearchAttributeSearch("");
3534
+ }
3535
+ },
3536
+ onChange: (val) => {
3537
+ if (val && typeof val === "string" && val !== "__none__") {
3538
+ handleSearchAttributeChange(val);
3539
+ setSearchAttributeSearch("");
3540
+ }
3541
+ },
3542
+ children: memoizedSearchComboboxOptions
3543
+ }
3544
+ )
3545
+ ] }) }),
3546
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 200, minWidth: 170 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
3248
3547
  designSystem.SingleSelect,
3249
3548
  {
3250
- label: selectedAttribute ? `Filter condition for "${selectedAttribute}"` : "Filter condition",
3549
+ label: effectiveSearchAttribute ? `Condition for "${effectiveSearchAttribute}"` : "Filter condition",
3251
3550
  value: filterOperator,
3252
3551
  onChange: (val) => setFilterOperator(String(val)),
3253
3552
  children: [
3254
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "none", children: "Show All (No Attribute Filter)" }),
3553
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "none", children: "Show All (No Value Filter)" }),
3255
3554
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "empty", children: "Is Empty or Null" }),
3256
3555
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "not_empty", children: "Is Not Empty" }),
3257
3556
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "equals", children: "Equals Exact Text" }),
@@ -3260,16 +3559,16 @@ const BulkUpdater = () => {
3260
3559
  ]
3261
3560
  }
3262
3561
  ) }),
3263
- (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(
3562
+ (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsx(
3264
3563
  designSystem.TextInput,
3265
3564
  {
3266
- label: selectedAttribute ? `Value for "${selectedAttribute}"` : "Value to match",
3565
+ label: effectiveSearchAttribute ? `Value for "${effectiveSearchAttribute}"` : "Value to match",
3267
3566
  placeholder: "Type value to match...",
3268
3567
  value: filterQuery,
3269
3568
  onChange: (e) => setFilterQuery(e.target.value)
3270
3569
  }
3271
3570
  ) }),
3272
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(
3571
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsx(
3273
3572
  designSystem.TextInput,
3274
3573
  {
3275
3574
  label: "Full-text Entry Search",
@@ -3467,11 +3766,11 @@ const BulkUpdater = () => {
3467
3766
  const pageSize = pageState ? pageState.pageSize : defaultPageSize;
3468
3767
  const hasAttr = selectedAttribute ? col.attributes.includes(selectedAttribute) : false;
3469
3768
  const visibleEntries = getVisibleEntries(col);
3470
- selectedAttribute ? entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length : 0;
3769
+ selectedAttribute ? entries.filter((e) => entryMatchesFilter(e, effectiveSearchAttribute)).length : 0;
3471
3770
  const isFullySelected = isCollectionFullySelected(col);
3472
3771
  const isPartiallySelected = isCollectionPartiallySelected(col);
3473
3772
  const currentSort = collectionSorts[col.uid] || { field: "createdAt", order: "desc" };
3474
- const availableColumns = getAvailableColumnsForCollection(col, selectedAttribute);
3773
+ const availableColumns = getAvailableColumnsForCollection(col, selectedAttribute, searchAttribute);
3475
3774
  const savedColKeys = collectionColumnVisibility[col.uid];
3476
3775
  const activeColumns = savedColKeys ? availableColumns.filter((c) => c.isPermanent || savedColKeys.includes(c.key)) : availableColumns.filter((c) => c.defaultVisible);
3477
3776
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3579,6 +3878,10 @@ const BulkUpdater = () => {
3579
3878
  if (entrySearch && entrySearch.trim()) {
3580
3879
  params.set("search", entrySearch.trim());
3581
3880
  }
3881
+ const colSelectedDocIds = col.entries.filter((e) => selectedEntries[`${col.uid}${KEY_SEP}${e.documentId}`]).map((e) => e.documentId);
3882
+ if (colSelectedDocIds.length > 0) {
3883
+ params.set("documentIds", colSelectedDocIds.join(","));
3884
+ }
3582
3885
  const res = await get(`/bulk-updater/csv/export/${col.uid}?${params.toString()}`);
3583
3886
  const csvStr = res.data?.csvContent || (typeof res.data === "string" ? res.data : "");
3584
3887
  if (!csvStr) {
@@ -3588,7 +3891,8 @@ const BulkUpdater = () => {
3588
3891
  });
3589
3892
  return;
3590
3893
  }
3591
- const blob = new Blob([csvStr], { type: "text/csv;charset=utf-8;" });
3894
+ const safeContent = csvStr.startsWith("\uFEFF") ? csvStr : "\uFEFF" + csvStr;
3895
+ const blob = new Blob([safeContent], { type: "text/csv;charset=utf-8;" });
3592
3896
  const url = URL.createObjectURL(blob);
3593
3897
  const link = document.createElement("a");
3594
3898
  link.href = url;
@@ -3599,7 +3903,7 @@ const BulkUpdater = () => {
3599
3903
  URL.revokeObjectURL(url);
3600
3904
  toggleNotification({
3601
3905
  type: "success",
3602
- message: `Exported ${col.displayName} entries to CSV`
3906
+ message: `Exported ${res.data?.count ?? (colSelectedDocIds.length || col.entries.length)} ${col.displayName} entries to Excel CSV`
3603
3907
  });
3604
3908
  } catch (e) {
3605
3909
  toggleNotification({
@@ -3676,7 +3980,7 @@ const BulkUpdater = () => {
3676
3980
  ),
3677
3981
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { className: "bulk-updater-table-container", style: { width: "100%", maxWidth: "100%", minWidth: 0, overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: activeColumns.length, rowCount: visibleEntries.length + 1, style: { width: "100%" }, children: [
3678
3982
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: activeColumns.map(
3679
- (colDef) => renderHeaderCell(colDef, col, currentSort, selectedAttribute, handleHeaderSortClick)
3983
+ (colDef) => renderHeaderCell(colDef, col, currentSort, selectedAttribute, searchAttribute, handleHeaderSortClick)
3680
3984
  ) }) }),
3681
3985
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: visibleEntries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: activeColumns.length, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 4, style: { textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No entries in this content type match the active filter criteria." }) }) }) }) : visibleEntries.map((entry) => {
3682
3986
  const key = `${col.uid}${KEY_SEP}${entry.documentId}`;
@@ -3694,6 +3998,7 @@ const BulkUpdater = () => {
3694
3998
  entry,
3695
3999
  col,
3696
4000
  selectedAttribute,
4001
+ searchAttribute,
3697
4002
  isMatch,
3698
4003
  hasAttr,
3699
4004
  isSelected,
@@ -4667,4 +4972,4 @@ exports.default = BulkUpdater;
4667
4972
  exports.formatDisplayValue = formatDisplayValue;
4668
4973
  exports.getAvailableColumnsForCollection = getAvailableColumnsForCollection;
4669
4974
  exports.getNestedValue = getNestedValue;
4670
- //# sourceMappingURL=BulkUpdater-CQ9zGdat.js.map
4975
+ //# sourceMappingURL=BulkUpdater-CRvwX326.js.map