strapi-plugin-bulk-updater 1.2.8 → 1.2.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.
@@ -1117,6 +1117,330 @@ function ProcessingDashboardModal({
1117
1117
  }
1118
1118
  );
1119
1119
  }
1120
+ const BulkUpdatePreviewModal = ({
1121
+ isOpen,
1122
+ onClose,
1123
+ onProceed,
1124
+ loading,
1125
+ previewData,
1126
+ attribute,
1127
+ targetValueDisplay
1128
+ }) => {
1129
+ const [filterTab, setFilterTab] = React.useState("all");
1130
+ const [searchQuery, setSearchQuery] = React.useState("");
1131
+ const filteredDiffs = React.useMemo(() => {
1132
+ if (!previewData || !previewData.diffs) return [];
1133
+ let items = previewData.diffs;
1134
+ if (filterTab !== "all") {
1135
+ items = items.filter((d) => d.status === filterTab);
1136
+ }
1137
+ if (searchQuery.trim()) {
1138
+ const q = searchQuery.toLowerCase().trim();
1139
+ items = items.filter(
1140
+ (d) => d.label.toLowerCase().includes(q) || d.documentId.toLowerCase().includes(q) || d.collectionName.toLowerCase().includes(q) || String(d.oldDisplay || "").toLowerCase().includes(q) || String(d.newDisplay || "").toLowerCase().includes(q)
1141
+ );
1142
+ }
1143
+ return items;
1144
+ }, [previewData, filterTab, searchQuery]);
1145
+ if (!isOpen) return null;
1146
+ return /* @__PURE__ */ jsxRuntime.jsx(
1147
+ "div",
1148
+ {
1149
+ style: {
1150
+ position: "fixed",
1151
+ inset: 0,
1152
+ backgroundColor: "rgba(20, 20, 30, 0.7)",
1153
+ backdropFilter: "blur(3px)",
1154
+ display: "flex",
1155
+ alignItems: "center",
1156
+ justifyContent: "center",
1157
+ zIndex: 9999,
1158
+ padding: 16,
1159
+ boxSizing: "border-box"
1160
+ },
1161
+ onClick: onClose,
1162
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
1163
+ designSystem.Box,
1164
+ {
1165
+ background: "neutral0",
1166
+ hasRadius: true,
1167
+ shadow: "popupShadow",
1168
+ style: {
1169
+ maxWidth: 1040,
1170
+ width: "95%",
1171
+ maxHeight: "calc(100vh - 48px)",
1172
+ display: "flex",
1173
+ flexDirection: "column",
1174
+ overflow: "hidden",
1175
+ position: "relative",
1176
+ boxShadow: "0 24px 48px rgba(0, 0, 0, 0.35), 0 0 1px rgba(0, 0, 0, 0.2)"
1177
+ },
1178
+ onClick: (e) => e.stopPropagation(),
1179
+ children: [
1180
+ /* @__PURE__ */ jsxRuntime.jsx(
1181
+ designSystem.Box,
1182
+ {
1183
+ padding: 5,
1184
+ background: "neutral100",
1185
+ style: {
1186
+ borderBottom: "1px solid rgba(120, 120, 150, 0.2)",
1187
+ flexShrink: 0
1188
+ },
1189
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
1190
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1191
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", marginBottom: 1, children: [
1192
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Pre-Update Change Preview" }),
1193
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
1194
+ "Target: ",
1195
+ attribute
1196
+ ] })
1197
+ ] }),
1198
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Inspect exact before & after values across target records before applying changes to the database." })
1199
+ ] }),
1200
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
1201
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "New Value:" }),
1202
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: targetValueDisplay || "(empty)" })
1203
+ ] })
1204
+ ] })
1205
+ }
1206
+ ),
1207
+ /* @__PURE__ */ jsxRuntime.jsx(
1208
+ designSystem.Box,
1209
+ {
1210
+ padding: 5,
1211
+ style: {
1212
+ flex: "1 1 auto",
1213
+ minHeight: 0,
1214
+ overflowY: "auto",
1215
+ WebkitOverflowScrolling: "touch"
1216
+ },
1217
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 8, style: { textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "center", gap: 3, children: [
1218
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Inspecting target records from database..." }),
1219
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "semiBold", children: "Inspecting Target Records..." }),
1220
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Computing proposed diffs and verifying against database schema." })
1221
+ ] }) }) : !previewData ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, style: { textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "No preview data available." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1222
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, marginBottom: 4, wrap: "wrap", children: [
1223
+ /* @__PURE__ */ jsxRuntime.jsxs(
1224
+ designSystem.Box,
1225
+ {
1226
+ padding: 3,
1227
+ background: "neutral100",
1228
+ hasRadius: true,
1229
+ style: { flex: 1, minWidth: 140, border: "1px solid rgba(120, 120, 150, 0.15)" },
1230
+ children: [
1231
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", display: "block", children: "TOTAL TARGETS" }),
1232
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", fontWeight: "bold", textColor: "neutral800", children: previewData.summary.total })
1233
+ ]
1234
+ }
1235
+ ),
1236
+ /* @__PURE__ */ jsxRuntime.jsxs(
1237
+ designSystem.Box,
1238
+ {
1239
+ padding: 3,
1240
+ background: "success100",
1241
+ hasRadius: true,
1242
+ style: { flex: 1, minWidth: 140, border: "1px solid #b7eb8f" },
1243
+ children: [
1244
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "success700", display: "block", children: "WILL UPDATE" }),
1245
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", fontWeight: "bold", textColor: "success700", children: previewData.summary.willUpdate })
1246
+ ]
1247
+ }
1248
+ ),
1249
+ /* @__PURE__ */ jsxRuntime.jsxs(
1250
+ designSystem.Box,
1251
+ {
1252
+ padding: 3,
1253
+ background: "neutral100",
1254
+ hasRadius: true,
1255
+ style: { flex: 1, minWidth: 140, border: "1px solid rgba(120, 120, 150, 0.15)" },
1256
+ children: [
1257
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", display: "block", children: "ALREADY UP-TO-DATE" }),
1258
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", fontWeight: "bold", textColor: "neutral600", children: previewData.summary.unchanged })
1259
+ ]
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsxRuntime.jsxs(
1263
+ designSystem.Box,
1264
+ {
1265
+ padding: 3,
1266
+ background: previewData.summary.skipped > 0 ? "warning100" : "neutral100",
1267
+ hasRadius: true,
1268
+ style: { flex: 1, minWidth: 140, border: "1px solid rgba(120, 120, 150, 0.15)" },
1269
+ children: [
1270
+ /* @__PURE__ */ jsxRuntime.jsx(
1271
+ designSystem.Typography,
1272
+ {
1273
+ variant: "sigma",
1274
+ textColor: previewData.summary.skipped > 0 ? "warning700" : "neutral600",
1275
+ display: "block",
1276
+ children: "WILL SKIP (NO FIELD)"
1277
+ }
1278
+ ),
1279
+ /* @__PURE__ */ jsxRuntime.jsx(
1280
+ designSystem.Typography,
1281
+ {
1282
+ variant: "beta",
1283
+ fontWeight: "bold",
1284
+ textColor: previewData.summary.skipped > 0 ? "warning700" : "neutral600",
1285
+ children: previewData.summary.skipped
1286
+ }
1287
+ )
1288
+ ]
1289
+ }
1290
+ )
1291
+ ] }),
1292
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, wrap: "wrap", gap: 3, children: [
1293
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, wrap: "wrap", children: [
1294
+ /* @__PURE__ */ jsxRuntime.jsxs(
1295
+ designSystem.Button,
1296
+ {
1297
+ variant: filterTab === "all" ? "default" : "tertiary",
1298
+ size: "S",
1299
+ onClick: () => setFilterTab("all"),
1300
+ children: [
1301
+ "All (",
1302
+ previewData.summary.total,
1303
+ ")"
1304
+ ]
1305
+ }
1306
+ ),
1307
+ /* @__PURE__ */ jsxRuntime.jsxs(
1308
+ designSystem.Button,
1309
+ {
1310
+ variant: filterTab === "will_update" ? "default" : "tertiary",
1311
+ size: "S",
1312
+ onClick: () => setFilterTab("will_update"),
1313
+ children: [
1314
+ "Will Update (",
1315
+ previewData.summary.willUpdate,
1316
+ ")"
1317
+ ]
1318
+ }
1319
+ ),
1320
+ /* @__PURE__ */ jsxRuntime.jsxs(
1321
+ designSystem.Button,
1322
+ {
1323
+ variant: filterTab === "unchanged" ? "default" : "tertiary",
1324
+ size: "S",
1325
+ onClick: () => setFilterTab("unchanged"),
1326
+ children: [
1327
+ "Unchanged (",
1328
+ previewData.summary.unchanged,
1329
+ ")"
1330
+ ]
1331
+ }
1332
+ ),
1333
+ previewData.summary.skipped > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1334
+ designSystem.Button,
1335
+ {
1336
+ variant: filterTab === "will_skip" ? "default" : "tertiary",
1337
+ size: "S",
1338
+ onClick: () => setFilterTab("will_skip"),
1339
+ children: [
1340
+ "Skipped (",
1341
+ previewData.summary.skipped,
1342
+ ")"
1343
+ ]
1344
+ }
1345
+ )
1346
+ ] }),
1347
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 260 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1348
+ designSystem.TextInput,
1349
+ {
1350
+ placeholder: "Search records or values...",
1351
+ size: "S",
1352
+ value: searchQuery,
1353
+ onChange: (e) => setSearchQuery(e.target.value)
1354
+ }
1355
+ ) })
1356
+ ] }),
1357
+ /* @__PURE__ */ jsxRuntime.jsx(
1358
+ designSystem.Box,
1359
+ {
1360
+ hasRadius: true,
1361
+ style: {
1362
+ border: "1px solid #eaeaef",
1363
+ maxHeight: 380,
1364
+ overflowY: "auto",
1365
+ overflowX: "auto"
1366
+ },
1367
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Table, { colCount: 5, rowCount: filteredDiffs.length + 1, style: { width: "100%", minWidth: 780 }, children: [
1368
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Thead, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
1369
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { width: 150 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "COLLECTION" }) }),
1370
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { minWidth: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "RECORD IDENTIFIER" }) }),
1371
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "CURRENT VALUE (BEFORE)" }) }),
1372
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "NEW VALUE (AFTER)" }) }),
1373
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { width: 140, textAlign: "right" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: "IMPACT" }) })
1374
+ ] }) }),
1375
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: filteredDiffs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: 5, style: { textAlign: "center", padding: "24px" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "No records match the current filter or search criteria." }) }) }) : filteredDiffs.map((diff) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
1376
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral700", children: diff.collectionName }) }),
1377
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
1378
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "bold", display: "block", children: diff.label }),
1379
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: diff.documentId }) })
1380
+ ] }) }),
1381
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(
1382
+ designSystem.Typography,
1383
+ {
1384
+ variant: "omega",
1385
+ style: {
1386
+ color: diff.hasChange ? "#666687" : "#272635",
1387
+ textDecoration: diff.hasChange ? "line-through" : "none",
1388
+ wordBreak: "break-all"
1389
+ },
1390
+ children: diff.oldDisplay || "(empty)"
1391
+ }
1392
+ ) }),
1393
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: diff.status === "will_skip" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "warning700", children: "— Skipped (field absent) —" }) : diff.hasChange ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", textColor: "success700", style: { wordBreak: "break-all" }, children: diff.newDisplay || "(empty)" }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral600", children: diff.newDisplay || "(already matching)" }) }),
1394
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Td, { style: { textAlign: "right" }, children: [
1395
+ diff.status === "will_update" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: "✓ Will Update" }),
1396
+ diff.status === "unchanged" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: "= Unchanged" }),
1397
+ diff.status === "will_skip" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: "⚠ Will Skip" })
1398
+ ] })
1399
+ ] }, `${diff.uid}-${diff.documentId}`)) })
1400
+ ] })
1401
+ }
1402
+ )
1403
+ ] })
1404
+ }
1405
+ ),
1406
+ /* @__PURE__ */ jsxRuntime.jsx(
1407
+ designSystem.Box,
1408
+ {
1409
+ padding: 4,
1410
+ background: "neutral100",
1411
+ style: {
1412
+ borderTop: "1px solid rgba(120, 120, 150, 0.2)",
1413
+ flexShrink: 0
1414
+ },
1415
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
1416
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", size: "M", onClick: onClose, children: "Close Preview" }),
1417
+ /* @__PURE__ */ jsxRuntime.jsxs(
1418
+ designSystem.Button,
1419
+ {
1420
+ variant: "default",
1421
+ size: "M",
1422
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Check, {}),
1423
+ onClick: () => {
1424
+ onClose();
1425
+ onProceed();
1426
+ },
1427
+ disabled: loading || !previewData || previewData.summary.total === 0,
1428
+ children: [
1429
+ "Proceed to Execute (",
1430
+ previewData?.summary.willUpdate || 0,
1431
+ " updates)"
1432
+ ]
1433
+ }
1434
+ )
1435
+ ] })
1436
+ }
1437
+ )
1438
+ ]
1439
+ }
1440
+ )
1441
+ }
1442
+ );
1443
+ };
1120
1444
  function getNestedValue(obj, path) {
1121
1445
  if (!obj || !path) return void 0;
1122
1446
  if (!path.includes(".")) return obj[path];
@@ -1311,6 +1635,9 @@ const BulkUpdater = () => {
1311
1635
  const [dashboardBatchId, setDashboardBatchId] = React.useState("");
1312
1636
  const [dashboardJob, setDashboardJob] = React.useState(null);
1313
1637
  const [isSubmittingBatch, setIsSubmittingBatch] = React.useState(false);
1638
+ const [showPreviewModal, setShowPreviewModal] = React.useState(false);
1639
+ const [previewLoading, setPreviewLoading] = React.useState(false);
1640
+ const [previewData, setPreviewData] = React.useState(null);
1314
1641
  const MODELS_CACHE_KEY = "strapi_bulk_updater_models_cache";
1315
1642
  const LAST_ATTR_KEY = "strapi_bulk_updater_last_attr";
1316
1643
  const getInitialAttribute = () => {
@@ -1432,7 +1759,7 @@ const BulkUpdater = () => {
1432
1759
  storedAttr = localStorage.getItem(LAST_ATTR_KEY);
1433
1760
  } catch {
1434
1761
  }
1435
- const resolvedAttr = storedAttr && attrs.includes(storedAttr) ? storedAttr : attrs.includes(selectedAttribute) ? selectedAttribute : attrs[0] || "metaDescription";
1762
+ const resolvedAttr = storedAttr && attrs.includes(storedAttr) ? storedAttr : selectedAttribute && attrs.includes(selectedAttribute) ? selectedAttribute : "";
1436
1763
  setSelectedAttribute(resolvedAttr);
1437
1764
  if (resolvedAttr && data.relationsMeta && data.relationsMeta[resolvedAttr]) {
1438
1765
  fetchRelationOptions(data.relationsMeta[resolvedAttr].targetUid);
@@ -1630,7 +1957,11 @@ const BulkUpdater = () => {
1630
1957
  if (newAttr !== selectedAttribute) {
1631
1958
  setSelectedAttribute(newAttr);
1632
1959
  try {
1633
- localStorage.setItem(LAST_ATTR_KEY, newAttr);
1960
+ if (newAttr) {
1961
+ localStorage.setItem(LAST_ATTR_KEY, newAttr);
1962
+ } else {
1963
+ localStorage.removeItem(LAST_ATTR_KEY);
1964
+ }
1634
1965
  } catch {
1635
1966
  }
1636
1967
  setSelectedEntries({});
@@ -1639,17 +1970,19 @@ const BulkUpdater = () => {
1639
1970
  setEntrySearch("");
1640
1971
  setSelectedRelationDocId("");
1641
1972
  setAttributeValue("");
1642
- if (relationsMeta[newAttr]) {
1973
+ if (newAttr && relationsMeta[newAttr]) {
1643
1974
  fetchRelationOptions(relationsMeta[newAttr].targetUid);
1644
1975
  } else {
1645
1976
  setRelationOptions([]);
1646
1977
  }
1647
- const collectionsToLoad = collections.filter(
1648
- (col) => visibleTypeUids[col.uid] !== false && col.attributes.includes(newAttr) && (!collectionPages[col.uid]?.entries || collectionPages[col.uid].entries.length === 0)
1649
- );
1650
- collectionsToLoad.forEach((col) => {
1651
- loadCollectionPage(col.uid, 1);
1652
- });
1978
+ if (newAttr) {
1979
+ const collectionsToLoad = collections.filter(
1980
+ (col) => visibleTypeUids[col.uid] !== false && col.attributes.includes(newAttr) && (!collectionPages[col.uid]?.entries || collectionPages[col.uid].entries.length === 0)
1981
+ );
1982
+ collectionsToLoad.forEach((col) => {
1983
+ loadCollectionPage(col.uid, 1);
1984
+ });
1985
+ }
1653
1986
  }
1654
1987
  };
1655
1988
  const filteredCollections = collections.filter(
@@ -1686,8 +2019,30 @@ const BulkUpdater = () => {
1686
2019
  });
1687
2020
  setVisibleTypeUids(newVis);
1688
2021
  };
2022
+ const [attributeSearch, setAttributeSearch] = React.useState("");
2023
+ const filteredAttributes = React.useMemo(() => {
2024
+ if (!attributeSearch.trim()) {
2025
+ return attributes;
2026
+ }
2027
+ const q = attributeSearch.toLowerCase().trim();
2028
+ const cleanQ = q.replace(/[^a-z0-9]/g, "");
2029
+ return attributes.filter((attr) => {
2030
+ const lower = attr.toLowerCase();
2031
+ const cleanLower = lower.replace(/[^a-z0-9]/g, "");
2032
+ const rel = relationsMeta[attr];
2033
+ const relTarget = (rel?.targetDisplayName || "").toLowerCase();
2034
+ return lower.includes(q) || cleanLower.includes(cleanQ) || relTarget.includes(q);
2035
+ });
2036
+ }, [attributes, attributeSearch, relationsMeta]);
1689
2037
  const memoizedComboboxOptions = React.useMemo(() => {
1690
- return attributes.map((attr) => {
2038
+ if (filteredAttributes.length === 0) {
2039
+ 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: [
2040
+ 'No matching attributes found for "',
2041
+ attributeSearch,
2042
+ '"'
2043
+ ] }) });
2044
+ }
2045
+ return filteredAttributes.map((attr) => {
1691
2046
  const rel = relationsMeta[attr];
1692
2047
  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: [
1693
2048
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: attr === selectedAttribute ? "bold" : "regular", children: attr }),
@@ -1697,7 +2052,7 @@ const BulkUpdater = () => {
1697
2052
  ] }) : attr.includes(".") ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral700", size: "S", children: "component" }) : null
1698
2053
  ] }) }, attr);
1699
2054
  });
1700
- }, [attributes, relationsMeta, selectedAttribute]);
2055
+ }, [filteredAttributes, relationsMeta, selectedAttribute, attributeSearch]);
1701
2056
  const handleGlobalPageSizeChange = (newSize) => {
1702
2057
  const capped = Math.min(500, Math.max(1, newSize));
1703
2058
  setDefaultPageSize(capped);
@@ -1869,6 +2224,56 @@ const BulkUpdater = () => {
1869
2224
  }
1870
2225
  setShowConfirmModal(true);
1871
2226
  };
2227
+ const handleOpenPreviewModal = async () => {
2228
+ if (!selectedAttribute) {
2229
+ toggleNotification({
2230
+ type: "warning",
2231
+ message: "Please select an attribute from the dropdown"
2232
+ });
2233
+ return;
2234
+ }
2235
+ const targets = [];
2236
+ Object.keys(selectedEntries).forEach((key) => {
2237
+ if (selectedEntries[key]) {
2238
+ const parts = key.split(KEY_SEP);
2239
+ if (parts.length === 2) {
2240
+ targets.push({ uid: parts[0], documentId: parts[1] });
2241
+ }
2242
+ }
2243
+ });
2244
+ if (targets.length === 0) {
2245
+ toggleNotification({
2246
+ type: "warning",
2247
+ message: "Please select at least one collection type or entry to preview"
2248
+ });
2249
+ return;
2250
+ }
2251
+ setShowPreviewModal(true);
2252
+ setPreviewLoading(true);
2253
+ try {
2254
+ const res = await post("/bulk-updater/preview", {
2255
+ attribute: selectedAttribute,
2256
+ value: attributeValue,
2257
+ selectedEntries: targets,
2258
+ isRegex: useRegex,
2259
+ regexPattern,
2260
+ publishOnApproval
2261
+ });
2262
+ if (res.data && res.data.success) {
2263
+ setPreviewData(res.data);
2264
+ } else {
2265
+ throw new Error(res.data?.error || "Failed to generate preview");
2266
+ }
2267
+ } catch (err) {
2268
+ toggleNotification({
2269
+ type: "danger",
2270
+ message: err.message || "Failed to fetch preview diffs"
2271
+ });
2272
+ setShowPreviewModal(false);
2273
+ } finally {
2274
+ setPreviewLoading(false);
2275
+ }
2276
+ };
1872
2277
  const executeBatchWithMode = async (executionMode, forceImmediate = false) => {
1873
2278
  const targets = [];
1874
2279
  Object.keys(selectedEntries).forEach((key) => {
@@ -2353,15 +2758,28 @@ const BulkUpdater = () => {
2353
2758
  /* @__PURE__ */ jsxRuntime.jsx(
2354
2759
  designSystem.Combobox,
2355
2760
  {
2356
- autocomplete: { type: "list", filter: "contains" },
2761
+ autocomplete: "none",
2357
2762
  placeholder: "Search attribute by name or relation...",
2358
- value: selectedAttribute,
2359
- textValue: selectedAttribute,
2763
+ value: selectedAttribute || void 0,
2764
+ clearLabel: "Clear attribute selection",
2765
+ onClear: () => {
2766
+ handleAttributeChange("");
2767
+ setAttributeSearch("");
2768
+ },
2769
+ onInputChange: (e) => {
2770
+ setAttributeSearch(e.target.value);
2771
+ },
2772
+ onOpenChange: (isOpen) => {
2773
+ if (!isOpen) {
2774
+ setAttributeSearch("");
2775
+ }
2776
+ },
2360
2777
  loading: loading && attributes.length === 0,
2361
2778
  loadingMessage: "Discovering schema attributes...",
2362
2779
  onChange: (val) => {
2363
- if (val && typeof val === "string") {
2780
+ if (val && typeof val === "string" && val !== "__none__") {
2364
2781
  handleAttributeChange(val);
2782
+ setAttributeSearch("");
2365
2783
  }
2366
2784
  },
2367
2785
  children: memoizedComboboxOptions
@@ -2444,22 +2862,40 @@ const BulkUpdater = () => {
2444
2862
  onChange: (e) => setAttributeValue(e.target.value)
2445
2863
  }
2446
2864
  ) }),
2447
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
2448
- designSystem.Button,
2449
- {
2450
- startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Play, {}),
2451
- variant: "default",
2452
- size: "L",
2453
- onClick: handleOpenConfirmModal,
2454
- loading: updating,
2455
- disabled: loading || totalSelectedCount === 0,
2456
- children: [
2457
- "Update Selected (",
2458
- totalSelectedCount,
2459
- ")"
2460
- ]
2461
- }
2462
- ) })
2865
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
2866
+ /* @__PURE__ */ jsxRuntime.jsxs(
2867
+ designSystem.Button,
2868
+ {
2869
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Eye, {}),
2870
+ variant: "secondary",
2871
+ size: "L",
2872
+ onClick: handleOpenPreviewModal,
2873
+ loading: previewLoading,
2874
+ disabled: loading || totalSelectedCount === 0 || !selectedAttribute,
2875
+ children: [
2876
+ "Preview Changes (",
2877
+ totalSelectedCount,
2878
+ ")"
2879
+ ]
2880
+ }
2881
+ ),
2882
+ /* @__PURE__ */ jsxRuntime.jsxs(
2883
+ designSystem.Button,
2884
+ {
2885
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Play, {}),
2886
+ variant: "default",
2887
+ size: "L",
2888
+ onClick: handleOpenConfirmModal,
2889
+ loading: updating,
2890
+ disabled: loading || totalSelectedCount === 0 || !selectedAttribute,
2891
+ children: [
2892
+ "Update Selected (",
2893
+ totalSelectedCount,
2894
+ ")"
2895
+ ]
2896
+ }
2897
+ )
2898
+ ] })
2463
2899
  ] }),
2464
2900
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginTop: 4, padding: 4, background: "neutral100", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", wrap: "wrap", gap: 3, children: [
2465
2901
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, alignItems: "center", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -2606,7 +3042,7 @@ const BulkUpdater = () => {
2606
3042
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: 220, minWidth: 180 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
2607
3043
  designSystem.SingleSelect,
2608
3044
  {
2609
- label: `Filter condition for "${selectedAttribute}"`,
3045
+ label: selectedAttribute ? `Filter condition for "${selectedAttribute}"` : "Filter condition",
2610
3046
  value: filterOperator,
2611
3047
  onChange: (val) => setFilterOperator(String(val)),
2612
3048
  children: [
@@ -2622,7 +3058,7 @@ const BulkUpdater = () => {
2622
3058
  (filterOperator === "equals" || filterOperator === "contains" || filterOperator === "starts_with") && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2623
3059
  designSystem.TextInput,
2624
3060
  {
2625
- label: `Value for "${selectedAttribute}"`,
3061
+ label: selectedAttribute ? `Value for "${selectedAttribute}"` : "Value to match",
2626
3062
  placeholder: "Type value to match...",
2627
3063
  value: filterQuery,
2628
3064
  onChange: (e) => setFilterQuery(e.target.value)
@@ -2750,7 +3186,7 @@ const BulkUpdater = () => {
2750
3186
  (col) => !typeVisibilitySearch.trim() || col.displayName.toLowerCase().includes(typeVisibilitySearch.toLowerCase().trim()) || col.uid.toLowerCase().includes(typeVisibilitySearch.toLowerCase().trim())
2751
3187
  ).map((col) => {
2752
3188
  const isVisible = visibleTypeUids[col.uid] !== false;
2753
- const hasAttr = col.attributes.includes(selectedAttribute);
3189
+ const hasAttr = selectedAttribute ? col.attributes.includes(selectedAttribute) : false;
2754
3190
  return /* @__PURE__ */ jsxRuntime.jsx(
2755
3191
  designSystem.Box,
2756
3192
  {
@@ -2824,9 +3260,9 @@ const BulkUpdater = () => {
2824
3260
  const page = pageState ? pageState.page : 1;
2825
3261
  const pageCount = pageState ? pageState.pageCount : 1;
2826
3262
  const pageSize = pageState ? pageState.pageSize : defaultPageSize;
2827
- const hasAttr = col.attributes.includes(selectedAttribute);
3263
+ const hasAttr = selectedAttribute ? col.attributes.includes(selectedAttribute) : false;
2828
3264
  const visibleEntries = getVisibleEntries(col);
2829
- entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length;
3265
+ selectedAttribute ? entries.filter((e) => entryMatchesFilter(e, selectedAttribute)).length : 0;
2830
3266
  const isFullySelected = isCollectionFullySelected(col);
2831
3267
  const isPartiallySelected = isCollectionPartiallySelected(col);
2832
3268
  const currentSort = collectionSorts[col.uid] || { field: "createdAt", order: "desc" };
@@ -2860,14 +3296,14 @@ const BulkUpdater = () => {
2860
3296
  ] })
2861
3297
  }
2862
3298
  ),
2863
- col.isSingleType ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "Single Type" }) : /* @__PURE__ */ jsxRuntime.jsx(
3299
+ col.isSingleType ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: "Single Type" }) : selectedAttribute ? /* @__PURE__ */ jsxRuntime.jsx(
2864
3300
  designSystem.Badge,
2865
3301
  {
2866
3302
  backgroundColor: hasAttr ? "success100" : "warning100",
2867
3303
  textColor: hasAttr ? "success700" : "warning700",
2868
3304
  children: hasAttr ? `Schema has "${selectedAttribute}"` : `Schema lacks "${selectedAttribute}" (will skip)`
2869
3305
  }
2870
- )
3306
+ ) : null
2871
3307
  ] }),
2872
3308
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, alignItems: "center", wrap: "wrap", children: [
2873
3309
  !col.isSingleType && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
@@ -3636,6 +4072,22 @@ const BulkUpdater = () => {
3636
4072
  children: publishOnApproval ? "Publish Immediately" : "Keep as Draft"
3637
4073
  }
3638
4074
  )
4075
+ ] }),
4076
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginTop: 2, paddingTop: 2, style: { borderTop: "1px solid #eaeaef" }, children: [
4077
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Need to inspect before & after values?" }),
4078
+ /* @__PURE__ */ jsxRuntime.jsx(
4079
+ designSystem.Button,
4080
+ {
4081
+ variant: "secondary",
4082
+ size: "S",
4083
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Eye, {}),
4084
+ onClick: () => {
4085
+ setShowConfirmModal(false);
4086
+ handleOpenPreviewModal();
4087
+ },
4088
+ children: "Open Change Preview"
4089
+ }
4090
+ )
3639
4091
  ] })
3640
4092
  ] })
3641
4093
  ] }),
@@ -3772,6 +4224,21 @@ const BulkUpdater = () => {
3772
4224
  )
3773
4225
  }
3774
4226
  ),
4227
+ /* @__PURE__ */ jsxRuntime.jsx(
4228
+ BulkUpdatePreviewModal,
4229
+ {
4230
+ isOpen: showPreviewModal,
4231
+ onClose: () => setShowPreviewModal(false),
4232
+ onProceed: () => {
4233
+ setShowPreviewModal(false);
4234
+ handleOpenConfirmModal();
4235
+ },
4236
+ loading: previewLoading,
4237
+ previewData,
4238
+ attribute: selectedAttribute,
4239
+ targetValueDisplay: relationsMeta[selectedAttribute] ? selectedRelationDocId === "__clear__" ? "Clear / Unlink Relation" : relationOptions.find((o) => o.documentId === selectedRelationDocId)?.label || selectedRelationDocId || "(none)" : useRegex ? `Replace /${regexPattern}/g with "${attributeValue}"` : attributeValue
4240
+ }
4241
+ ),
3775
4242
  /* @__PURE__ */ jsxRuntime.jsx(
3776
4243
  ProcessingDashboardModal,
3777
4244
  {
@@ -3790,4 +4257,4 @@ exports.default = BulkUpdater;
3790
4257
  exports.formatDisplayValue = formatDisplayValue;
3791
4258
  exports.getAvailableColumnsForCollection = getAvailableColumnsForCollection;
3792
4259
  exports.getNestedValue = getNestedValue;
3793
- //# sourceMappingURL=BulkUpdater-2TTEWMXI.js.map
4260
+ //# sourceMappingURL=BulkUpdater-EI4sKaLN.js.map