strapi-plugin-magic-mark 3.3.2 → 3.4.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.
@@ -160,7 +160,7 @@ const FilterChip = styled__default.default.div`
160
160
  gap: 4px;
161
161
  color: ${(props) => props.theme.colors.neutral800};
162
162
  `;
163
- const LogicBadge = styled__default.default.span`
163
+ const LogicBadge$1 = styled__default.default.span`
164
164
  font-weight: bold;
165
165
  color: ${(props) => props.logic === "OR" ? props.theme.colors.warning700 : props.theme.colors.primary700};
166
166
  font-size: 10px;
@@ -197,7 +197,7 @@ const FilterPreview = ({ query }) => {
197
197
  parsed.filters.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 2, children: [
198
198
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", style: { marginBottom: "8px", display: "block" }, children: "Filters:" }),
199
199
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { wrap: "wrap", gap: 1, children: parsed.filters.map((filter, idx) => /* @__PURE__ */ jsxRuntime.jsxs(FilterChip, { logic: filter.logic, children: [
200
- filter.logic && /* @__PURE__ */ jsxRuntime.jsx(LogicBadge, { logic: filter.logic, children: filter.logic }),
200
+ filter.logic && /* @__PURE__ */ jsxRuntime.jsx(LogicBadge$1, { logic: filter.logic, children: filter.logic }),
201
201
  /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
202
202
  filter.field,
203
203
  " ",
@@ -360,7 +360,7 @@ const ModalBody = styled__default.default.div`
360
360
  overflow-y: auto;
361
361
  flex: 1;
362
362
  `;
363
- const ModalFooter = styled__default.default.div`
363
+ const ModalFooter$1 = styled__default.default.div`
364
364
  padding: 16px 28px;
365
365
  background: ${(p) => p.theme.colors.neutral100};
366
366
  border-top: 1px solid rgba(128, 128, 128, 0.25);
@@ -1012,7 +1012,7 @@ const CreateEditModal = ({
1012
1012
  )
1013
1013
  ] })
1014
1014
  ] }),
1015
- /* @__PURE__ */ jsxRuntime.jsxs(ModalFooter, { children: [
1015
+ /* @__PURE__ */ jsxRuntime.jsxs(ModalFooter$1, { children: [
1016
1016
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: onClose, variant: "tertiary", children: formatMessage({ id: `${pluginId2}.button.cancel`, defaultMessage: "Cancel" }) }),
1017
1017
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleSubmit, loading: isSubmitting, children: isEditing ? formatMessage({ id: `${pluginId2}.button.update`, defaultMessage: "Update" }) : formatMessage({ id: `${pluginId2}.button.save`, defaultMessage: "Save Bookmark" }) })
1018
1018
  ] })
@@ -1459,7 +1459,8 @@ const SelectContainer = styled__default.default.div`
1459
1459
  `;
1460
1460
  const SelectButton = styled__default.default.button`
1461
1461
  width: 100%;
1462
- padding: 10px 36px 10px 12px;
1462
+ height: 32px;
1463
+ padding: 0 36px 0 12px;
1463
1464
  border: 1px solid ${(props) => props.$isOpen ? "#4945ff" : "rgba(128, 128, 128, 0.25)"};
1464
1465
  border-radius: 4px;
1465
1466
  background: ${(p) => p.theme.colors.neutral0};
@@ -1482,12 +1483,13 @@ const SelectButton = styled__default.default.button`
1482
1483
  }
1483
1484
 
1484
1485
  @media (max-width: 768px) {
1485
- padding: 8px 32px 8px 10px;
1486
+ height: 32px;
1487
+ padding: 0 32px 0 10px;
1486
1488
  font-size: 13px;
1487
1489
  }
1488
1490
  `;
1489
1491
  const SelectText = styled__default.default.span`
1490
- color: ${(props) => props.$isPlaceholder ? "#8e8ea9" : "#212134"};
1492
+ color: ${(props) => props.$isPlaceholder ? props.theme.colors.neutral500 : props.theme.colors.neutral800};
1491
1493
  overflow: hidden;
1492
1494
  text-overflow: ellipsis;
1493
1495
  white-space: nowrap;
@@ -1508,7 +1510,7 @@ const DropdownList = styled__default.default.div`
1508
1510
  top: ${(props) => props.top}px;
1509
1511
  left: ${(props) => props.left}px;
1510
1512
  width: ${(props) => props.width}px;
1511
- max-height: 300px;
1513
+ max-height: 400px;
1512
1514
  overflow-y: auto;
1513
1515
  background: ${(p) => p.theme.colors.neutral0};
1514
1516
  border: 1px solid rgba(128, 128, 128, 0.25);
@@ -1518,14 +1520,14 @@ const DropdownList = styled__default.default.div`
1518
1520
  display: ${(props) => props.$isOpen ? "block" : "none"};
1519
1521
 
1520
1522
  @media (max-width: 768px) {
1521
- max-height: 250px;
1523
+ max-height: 320px;
1522
1524
  }
1523
1525
  `;
1524
1526
  const DropdownItem = styled__default.default.button`
1525
1527
  width: 100%;
1526
1528
  padding: 10px 12px;
1527
1529
  border: none;
1528
- background: ${(props) => props.$isSelected ? "#f0f0ff" : props.theme.colors.neutral0};
1530
+ background: ${(props) => props.$isSelected ? "rgba(73, 69, 255, 0.06)" : props.theme.colors.neutral0};
1529
1531
  text-align: left;
1530
1532
  cursor: pointer;
1531
1533
  font-size: 14px;
@@ -1552,6 +1554,8 @@ const SearchInput = styled__default.default.input`
1552
1554
  border: none;
1553
1555
  border-bottom: 1px solid rgba(128, 128, 128, 0.25);
1554
1556
  font-size: 14px;
1557
+ background: ${(p) => p.theme.colors.neutral0};
1558
+ color: ${(p) => p.theme.colors.neutral800};
1555
1559
 
1556
1560
  &:focus {
1557
1561
  outline: none;
@@ -1763,18 +1767,110 @@ const useRelationSchema = () => {
1763
1767
  errors
1764
1768
  };
1765
1769
  };
1766
- const buildFilterFromPath = (fieldPath, operator, value) => {
1770
+ const BASE_FILTERS = [
1771
+ { value: "eq", label: "is" },
1772
+ { value: "ne", label: "is not" },
1773
+ { value: "null", label: "is null" },
1774
+ { value: "notNull", label: "is not null" }
1775
+ ];
1776
+ const NUMERIC_FILTERS = [
1777
+ { value: "gt", label: "is greater than" },
1778
+ { value: "gte", label: "is greater than or equal to" },
1779
+ { value: "lt", label: "is less than" },
1780
+ { value: "lte", label: "is less than or equal to" }
1781
+ ];
1782
+ const CONTAINS_FILTERS = [
1783
+ { value: "contains", label: "contains" },
1784
+ { value: "containsi", label: "contains (case insensitive)" },
1785
+ { value: "notContains", label: "not contains" },
1786
+ { value: "notContainsi", label: "not contains (case insensitive)" }
1787
+ ];
1788
+ const STRING_PARSE_FILTERS = [
1789
+ { value: "startsWith", label: "starts with" },
1790
+ { value: "startsWithi", label: "starts with (case insensitive)" },
1791
+ { value: "endsWith", label: "ends with" },
1792
+ { value: "endsWithi", label: "ends with (case insensitive)" }
1793
+ ];
1794
+ const IS_SENSITIVE_FILTERS = [
1795
+ { value: "eqi", label: "is (case insensitive)" },
1796
+ { value: "nei", label: "is not (case insensitive)" }
1797
+ ];
1798
+ const ARRAY_FILTERS = [
1799
+ { value: "in", label: "is in" },
1800
+ { value: "notIn", label: "is not in" }
1801
+ ];
1802
+ const RANGE_FILTERS = [
1803
+ { value: "between", label: "is between" }
1804
+ ];
1805
+ function getOperatorsForType(fieldType) {
1806
+ const t = (fieldType || "string").toLowerCase();
1807
+ if (["string", "text", "email", "uid", "richtext"].includes(t)) {
1808
+ return [
1809
+ ...BASE_FILTERS,
1810
+ ...IS_SENSITIVE_FILTERS,
1811
+ ...CONTAINS_FILTERS,
1812
+ ...STRING_PARSE_FILTERS,
1813
+ ...ARRAY_FILTERS
1814
+ ];
1815
+ }
1816
+ if (["integer", "float", "decimal", "biginteger"].includes(t)) {
1817
+ return [
1818
+ ...BASE_FILTERS,
1819
+ ...NUMERIC_FILTERS,
1820
+ ...ARRAY_FILTERS,
1821
+ ...RANGE_FILTERS
1822
+ ];
1823
+ }
1824
+ if (["datetime"].includes(t)) {
1825
+ return [
1826
+ ...BASE_FILTERS,
1827
+ ...NUMERIC_FILTERS,
1828
+ ...RANGE_FILTERS
1829
+ ];
1830
+ }
1831
+ if (["date", "time"].includes(t)) {
1832
+ return [
1833
+ ...BASE_FILTERS,
1834
+ ...NUMERIC_FILTERS,
1835
+ ...CONTAINS_FILTERS,
1836
+ ...RANGE_FILTERS
1837
+ ];
1838
+ }
1839
+ if (["boolean"].includes(t)) {
1840
+ return BASE_FILTERS;
1841
+ }
1842
+ if (["enumeration"].includes(t)) {
1843
+ return [...BASE_FILTERS, ...ARRAY_FILTERS];
1844
+ }
1845
+ return [...BASE_FILTERS, ...IS_SENSITIVE_FILTERS];
1846
+ }
1847
+ const buildFilterFromPath = (fieldPath, operator, value, valueTo, negate) => {
1767
1848
  const parts = fieldPath.split(".").filter(Boolean);
1768
1849
  if (parts.length === 0) return {};
1769
1850
  const opKey = operator.startsWith("$") ? operator : `$${operator}`;
1770
- const filterValue = ["null", "notNull"].includes(operator) ? true : value;
1851
+ let filterValue;
1852
+ if (["null", "notNull"].includes(operator)) {
1853
+ filterValue = true;
1854
+ } else if (["in", "notIn"].includes(operator)) {
1855
+ filterValue = value.split(",").map((v) => v.trim()).filter(Boolean);
1856
+ } else if (operator === "between" && valueTo) {
1857
+ filterValue = [value, valueTo];
1858
+ } else {
1859
+ filterValue = value;
1860
+ }
1861
+ let result;
1771
1862
  if (parts.length === 1) {
1772
- return { [parts[0]]: { [opKey]: filterValue } };
1863
+ result = { [parts[0]]: { [opKey]: filterValue } };
1864
+ } else {
1865
+ const [first, ...rest] = parts;
1866
+ result = {
1867
+ [first]: buildFilterFromPath(rest.join("."), operator, value, valueTo, false)
1868
+ };
1773
1869
  }
1774
- const [first, ...rest] = parts;
1775
- return {
1776
- [first]: buildFilterFromPath(rest.join("."), operator, value)
1777
- };
1870
+ if (negate) {
1871
+ return { $not: result };
1872
+ }
1873
+ return result;
1778
1874
  };
1779
1875
  const extractRelationFromField = (field) => {
1780
1876
  if (!field || !field.includes(".")) return null;
@@ -1788,12 +1884,12 @@ const rowsToFilterStructure = (rows, connectors) => {
1788
1884
  });
1789
1885
  if (validRows.length === 0) return void 0;
1790
1886
  if (validRows.length === 1) {
1791
- return buildFilterFromPath(validRows[0].field, validRows[0].operator, validRows[0].value);
1887
+ return buildFilterFromPath(validRows[0].field, validRows[0].operator, validRows[0].value, validRows[0].valueTo, validRows[0].negate);
1792
1888
  }
1793
1889
  const groups = [];
1794
1890
  let currentAndGroup = [];
1795
1891
  validRows.forEach((row, i) => {
1796
- const filter = buildFilterFromPath(row.field, row.operator, row.value);
1892
+ const filter = buildFilterFromPath(row.field, row.operator, row.value, row.valueTo, row.negate);
1797
1893
  currentAndGroup.push(filter);
1798
1894
  const nextConnector = connectors[i];
1799
1895
  if (nextConnector?.logic === "OR" || !nextConnector) {
@@ -1835,6 +1931,58 @@ const generateFromRows = (rows, connectors) => {
1835
1931
  }
1836
1932
  return qs__default.default.stringify(queryObject, { encodeValuesOnly: true });
1837
1933
  };
1934
+ const buildFilterFromGroup = (group) => {
1935
+ const validItems = group.items.filter((item) => {
1936
+ if ("isGroup" in item && item.isGroup) return true;
1937
+ const row = item;
1938
+ if (!row.field) return false;
1939
+ if (["null", "notNull"].includes(row.operator)) return true;
1940
+ if (row.operator === "between") return row.value?.trim() && row.valueTo?.trim();
1941
+ return row.value?.trim();
1942
+ });
1943
+ if (validItems.length === 0) return void 0;
1944
+ const conditions = validItems.map((item) => {
1945
+ if ("isGroup" in item && item.isGroup) {
1946
+ return buildFilterFromGroup(item);
1947
+ }
1948
+ const row = item;
1949
+ return buildFilterFromPath(row.field, row.operator, row.value, row.valueTo, row.negate);
1950
+ }).filter(Boolean);
1951
+ if (conditions.length === 0) return void 0;
1952
+ if (conditions.length === 1) return conditions[0];
1953
+ const logicKey = group.logic === "AND" ? "$and" : "$or";
1954
+ return { [logicKey]: conditions };
1955
+ };
1956
+ const extractRelationsFromGroup = (group) => {
1957
+ const relations = /* @__PURE__ */ new Set();
1958
+ group.items.forEach((item) => {
1959
+ if ("isGroup" in item && item.isGroup) {
1960
+ const nested = extractRelationsFromGroup(item);
1961
+ nested.forEach((r) => relations.add(r));
1962
+ } else {
1963
+ const row = item;
1964
+ const rel = extractRelationFromField(row.field);
1965
+ if (rel) relations.add(rel);
1966
+ }
1967
+ });
1968
+ return relations;
1969
+ };
1970
+ const generateFromGroup = (group) => {
1971
+ const queryObject = {};
1972
+ const filterStructure = buildFilterFromGroup(group);
1973
+ if (filterStructure) {
1974
+ queryObject.filters = filterStructure;
1975
+ }
1976
+ const relations = extractRelationsFromGroup(group);
1977
+ if (relations.size > 0) {
1978
+ const populate = {};
1979
+ relations.forEach((rel) => {
1980
+ populate[rel] = true;
1981
+ });
1982
+ queryObject.populate = populate;
1983
+ }
1984
+ return qs__default.default.stringify(queryObject, { encodeValuesOnly: true });
1985
+ };
1838
1986
  const extractFieldPathAndOperator = (parts) => {
1839
1987
  let operatorIndex = -1;
1840
1988
  for (let i = parts.length - 1; i >= 0; i--) {
@@ -2009,7 +2157,7 @@ const ModalOverlay = styled__default.default.div`
2009
2157
  display: flex;
2010
2158
  align-items: center;
2011
2159
  justify-content: center;
2012
- z-index: 999;
2160
+ z-index: 400;
2013
2161
  padding: 20px;
2014
2162
  @media (max-width: 768px) {
2015
2163
  padding: 10px;
@@ -2019,61 +2167,328 @@ const ModalContent = styled__default.default(designSystem.Box)`
2019
2167
  background: ${(p) => p.theme.colors.neutral0};
2020
2168
  border-radius: 8px;
2021
2169
  max-height: 90vh;
2022
- overflow-y: auto;
2023
- max-width: 560px;
2170
+ max-width: 720px;
2024
2171
  width: 100%;
2172
+ min-height: 400px;
2025
2173
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
2174
+ display: flex;
2175
+ flex-direction: column;
2176
+ overflow: hidden;
2026
2177
  @media (max-width: 768px) {
2027
2178
  max-width: 100%;
2028
- max-height: 95vh;
2179
+ max-height: 92vh;
2029
2180
  }
2030
2181
  `;
2182
+ const ModalScrollArea = styled__default.default.div`
2183
+ flex: 1;
2184
+ overflow-y: auto;
2185
+ overflow-x: hidden;
2186
+ `;
2031
2187
  const RowWrapper = styled__default.default.div`
2032
2188
  display: flex;
2033
2189
  flex-direction: column;
2034
2190
  gap: 8px;
2035
2191
  margin-bottom: 8px;
2036
2192
  `;
2037
- const FilterRowContainer = styled__default.default.div`
2193
+ const FilterRowCard = styled__default.default.div`
2194
+ display: flex;
2195
+ flex-direction: column;
2196
+ gap: 10px;
2197
+ padding: 12px;
2198
+ background: ${(p) => p.theme.colors.neutral100};
2199
+ border-radius: 6px;
2200
+ border: 1px solid ${(p) => p.theme.colors.neutral200};
2201
+ `;
2202
+ const FilterRowTop = styled__default.default.div`
2038
2203
  display: flex;
2039
2204
  align-items: center;
2040
2205
  gap: 8px;
2041
2206
  flex-wrap: wrap;
2042
2207
  `;
2043
- const ValueInput = styled__default.default.input`
2044
- flex: 1;
2045
- min-width: 120px;
2046
- padding: 8px 12px;
2047
- border: 1px solid rgba(128, 128, 128, 0.25);
2208
+ const NegateCheckbox = styled__default.default.label`
2209
+ display: flex;
2210
+ align-items: center;
2211
+ gap: 6px;
2212
+ font-size: 12px;
2213
+ color: ${(p) => p.theme.colors.neutral700};
2214
+ cursor: pointer;
2215
+ user-select: none;
2216
+ input {
2217
+ cursor: pointer;
2218
+ }
2219
+ `;
2220
+ const FilterRowValue = styled__default.default.div`
2221
+ width: 100%;
2222
+ padding-top: 14px;
2223
+ padding-bottom: 4px;
2224
+ border-top: 1px solid ${(p) => p.theme.colors.neutral200};
2225
+ `;
2226
+ const ValueLabel = styled__default.default.span`
2227
+ display: block;
2228
+ font-size: 12px;
2229
+ font-weight: 500;
2230
+ color: ${(p) => p.theme.colors.neutral600};
2231
+ margin-bottom: 8px;
2232
+ `;
2233
+ styled__default.default.span`
2234
+ font-size: 10px;
2235
+ font-weight: 500;
2236
+ padding: 2px 6px;
2237
+ border-radius: 4px;
2238
+ background: ${(p) => p.theme.colors.neutral200};
2239
+ color: ${(p) => p.theme.colors.neutral700};
2240
+ text-transform: uppercase;
2241
+ `;
2242
+ const MultiValueWrapper = styled__default.default.div`
2243
+ display: flex;
2244
+ flex-wrap: wrap;
2245
+ gap: 6px;
2246
+ min-height: 32px;
2247
+ padding: 6px 8px;
2248
+ border: 1px solid ${(p) => p.theme.colors.neutral200};
2048
2249
  border-radius: 4px;
2250
+ background: ${(p) => p.theme.colors.neutral0};
2251
+ &:focus-within {
2252
+ border-color: #4945ff;
2253
+ box-shadow: 0 0 0 2px rgba(73, 69, 255, 0.1);
2254
+ }
2255
+ `;
2256
+ const MultiValueInput = styled__default.default.input`
2257
+ flex: 1;
2258
+ min-width: 80px;
2259
+ border: none;
2260
+ background: transparent;
2049
2261
  font-size: 14px;
2262
+ color: ${(p) => p.theme.colors.neutral800};
2050
2263
  &:focus {
2051
2264
  outline: none;
2052
- border-color: #4945ff;
2053
- box-shadow: 0 0 0 2px rgba(73, 69, 255, 0.1);
2054
2265
  }
2266
+ &::placeholder {
2267
+ color: ${(p) => p.theme.colors.neutral500};
2268
+ }
2269
+ `;
2270
+ const BetweenWrapper = styled__default.default.div`
2271
+ display: flex;
2272
+ align-items: center;
2273
+ gap: 8px;
2274
+ width: 100%;
2055
2275
  `;
2056
- const ConnectorRow = styled__default.default.div`
2276
+ const BetweenSeparator = styled__default.default.span`
2277
+ font-size: 13px;
2278
+ font-weight: 500;
2279
+ color: ${(p) => p.theme.colors.neutral600};
2280
+ `;
2281
+ function SmartValueInput({
2282
+ fieldType,
2283
+ operator,
2284
+ value,
2285
+ valueTo,
2286
+ onChange,
2287
+ onValueToChange,
2288
+ placeholder = "Value"
2289
+ }) {
2290
+ const t = (fieldType || "string").toLowerCase();
2291
+ const [inputValue, setInputValue] = React__default.default.useState("");
2292
+ if (["in", "notIn"].includes(operator)) {
2293
+ const values = value ? value.split(",").map((v) => v.trim()).filter(Boolean) : [];
2294
+ const handleAddValue = (e) => {
2295
+ if (e.key === "Enter" || e.key === ",") {
2296
+ e.preventDefault();
2297
+ const newVal = inputValue.trim();
2298
+ if (newVal && !values.includes(newVal)) {
2299
+ const updated = [...values, newVal].join(",");
2300
+ onChange(updated);
2301
+ setInputValue("");
2302
+ }
2303
+ }
2304
+ };
2305
+ const handleRemoveValue = (val) => {
2306
+ const updated = values.filter((v) => v !== val).join(",");
2307
+ onChange(updated);
2308
+ };
2309
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsxs(MultiValueWrapper, { children: [
2310
+ values.map((val) => /* @__PURE__ */ jsxRuntime.jsx(
2311
+ designSystem.Tag,
2312
+ {
2313
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {}),
2314
+ onClick: () => handleRemoveValue(val),
2315
+ children: val
2316
+ },
2317
+ val
2318
+ )),
2319
+ /* @__PURE__ */ jsxRuntime.jsx(
2320
+ MultiValueInput,
2321
+ {
2322
+ value: inputValue,
2323
+ onChange: (e) => setInputValue(e.target.value),
2324
+ onKeyDown: handleAddValue,
2325
+ placeholder: values.length === 0 ? "Type and press Enter" : ""
2326
+ }
2327
+ )
2328
+ ] }) });
2329
+ }
2330
+ if (operator === "between") {
2331
+ if (["date"].includes(t)) {
2332
+ const dateVal1 = value ? (() => {
2333
+ try {
2334
+ const d = new Date(value);
2335
+ return isNaN(d.getTime()) ? void 0 : d;
2336
+ } catch {
2337
+ return void 0;
2338
+ }
2339
+ })() : void 0;
2340
+ const dateVal2 = valueTo ? (() => {
2341
+ try {
2342
+ const d = new Date(valueTo);
2343
+ return isNaN(d.getTime()) ? void 0 : d;
2344
+ } catch {
2345
+ return void 0;
2346
+ }
2347
+ })() : void 0;
2348
+ return /* @__PURE__ */ jsxRuntime.jsxs(BetweenWrapper, { children: [
2349
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.DatePicker, { value: dateVal1, onChange: (d) => onChange(d ? d.toISOString().slice(0, 10) : ""), clearLabel: "Clear", size: "S" }) }),
2350
+ /* @__PURE__ */ jsxRuntime.jsx(BetweenSeparator, { children: "and" }),
2351
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.DatePicker, { value: dateVal2, onChange: (d) => onValueToChange?.(d ? d.toISOString().slice(0, 10) : ""), clearLabel: "Clear", size: "S" }) })
2352
+ ] });
2353
+ }
2354
+ if (["integer", "float", "decimal", "biginteger"].includes(t)) {
2355
+ const num1 = value === "" ? void 0 : (() => {
2356
+ const n = parseFloat(value);
2357
+ return isNaN(n) ? void 0 : n;
2358
+ })();
2359
+ const num2 = valueTo === "" || !valueTo ? void 0 : (() => {
2360
+ const n = parseFloat(valueTo);
2361
+ return isNaN(n) ? void 0 : n;
2362
+ })();
2363
+ return /* @__PURE__ */ jsxRuntime.jsxs(BetweenWrapper, { children: [
2364
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.NumberInput, { value: num1, onValueChange: (v) => onChange(v !== void 0 ? String(v) : ""), size: "S" }) }),
2365
+ /* @__PURE__ */ jsxRuntime.jsx(BetweenSeparator, { children: "and" }),
2366
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.NumberInput, { value: num2, onValueChange: (v) => onValueToChange?.(v !== void 0 ? String(v) : ""), size: "S" }) })
2367
+ ] });
2368
+ }
2369
+ return /* @__PURE__ */ jsxRuntime.jsxs(BetweenWrapper, { children: [
2370
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.TextInput, { value, onChange: (e) => onChange(e.target.value), placeholder: "From", size: "S" }) }),
2371
+ /* @__PURE__ */ jsxRuntime.jsx(BetweenSeparator, { children: "and" }),
2372
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.TextInput, { value: valueTo || "", onChange: (e) => onValueToChange?.(e.target.value), placeholder: "To", size: "S" }) })
2373
+ ] });
2374
+ }
2375
+ if (["date"].includes(t)) {
2376
+ const dateVal = value ? (() => {
2377
+ try {
2378
+ const d = new Date(value);
2379
+ return isNaN(d.getTime()) ? void 0 : d;
2380
+ } catch {
2381
+ return void 0;
2382
+ }
2383
+ })() : void 0;
2384
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 140 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2385
+ designSystem.DatePicker,
2386
+ {
2387
+ value: dateVal,
2388
+ onChange: (d) => onChange(d ? d.toISOString().slice(0, 10) : ""),
2389
+ clearLabel: "Clear",
2390
+ size: "S"
2391
+ }
2392
+ ) });
2393
+ }
2394
+ if (["datetime"].includes(t)) {
2395
+ const dateVal = value ? (() => {
2396
+ try {
2397
+ const d = new Date(value);
2398
+ return isNaN(d.getTime()) ? void 0 : d;
2399
+ } catch {
2400
+ return void 0;
2401
+ }
2402
+ })() : void 0;
2403
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2404
+ designSystem.DateTimePicker,
2405
+ {
2406
+ value: dateVal ?? void 0,
2407
+ onChange: (d) => onChange(d ? d.toISOString() : ""),
2408
+ clearLabel: "Clear",
2409
+ size: "S"
2410
+ }
2411
+ ) });
2412
+ }
2413
+ if (["time"].includes(t)) {
2414
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 120 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2415
+ designSystem.TimePicker,
2416
+ {
2417
+ value: value || void 0,
2418
+ onChange: (v) => onChange(v ?? ""),
2419
+ size: "S"
2420
+ }
2421
+ ) });
2422
+ }
2423
+ if (["integer", "float", "decimal", "biginteger"].includes(t)) {
2424
+ const numVal = value === "" ? void 0 : (() => {
2425
+ const n = parseFloat(value);
2426
+ return isNaN(n) ? void 0 : n;
2427
+ })();
2428
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 120 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2429
+ designSystem.NumberInput,
2430
+ {
2431
+ value: numVal,
2432
+ onValueChange: (v) => onChange(v !== void 0 ? String(v) : ""),
2433
+ size: "S"
2434
+ }
2435
+ ) });
2436
+ }
2437
+ if (["boolean"].includes(t)) {
2438
+ const selVal = value === "true" ? "true" : value === "false" ? "false" : "";
2439
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 120 }, children: /* @__PURE__ */ jsxRuntime.jsxs(
2440
+ designSystem.SingleSelect,
2441
+ {
2442
+ value: selVal || void 0,
2443
+ onChange: (v) => onChange(v != null ? String(v) : ""),
2444
+ placeholder,
2445
+ size: "S",
2446
+ children: [
2447
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "true", children: "true" }),
2448
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "false", children: "false" })
2449
+ ]
2450
+ }
2451
+ ) });
2452
+ }
2453
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { width: "100%", minWidth: 120 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2454
+ designSystem.TextInput,
2455
+ {
2456
+ value,
2457
+ onChange: (e) => onChange(e.target.value),
2458
+ placeholder,
2459
+ size: "S"
2460
+ }
2461
+ ) });
2462
+ }
2463
+ const ConnectorPill = styled__default.default.div`
2057
2464
  display: flex;
2058
2465
  align-items: center;
2059
2466
  justify-content: center;
2060
- padding: 4px 0;
2467
+ padding: 6px 0;
2061
2468
  `;
2062
2469
  const ConnectorButton = styled__default.default.button`
2063
- padding: 4px 12px;
2470
+ padding: 6px 14px;
2064
2471
  font-size: 12px;
2065
2472
  font-weight: 500;
2066
- border: 1px solid ${(props) => props.$active ? "#4945ff" : "rgba(128, 128, 128, 0.25)"};
2067
- border-radius: 4px;
2068
- background: ${(props) => props.$active ? "#EEF0FF" : "transparent"};
2069
- color: ${(props) => props.$active ? "#4945ff" : "#32324d"};
2473
+ border: 1px solid ${(p) => p.$active ? "#4945ff" : p.theme.colors.neutral300};
2474
+ border-radius: 999px;
2475
+ background: ${(p) => p.$active ? "#EEF0FF" : "transparent"};
2476
+ color: ${(p) => p.$active ? "#4945ff" : p.theme.colors.neutral700};
2070
2477
  cursor: pointer;
2071
2478
  transition: all 0.15s ease;
2072
2479
  &:hover {
2073
2480
  border-color: #4945ff;
2074
- background: ${(p) => p.theme.colors.neutral100};
2075
2481
  }
2076
2482
  `;
2483
+ const ActiveBadge = styled__default.default.span`
2484
+ font-size: 12px;
2485
+ font-weight: 500;
2486
+ padding: 2px 8px;
2487
+ border-radius: 999px;
2488
+ background: rgba(73, 69, 255, 0.12);
2489
+ color: #4945ff;
2490
+ margin-left: 8px;
2491
+ `;
2077
2492
  const DeleteButton = styled__default.default.button`
2078
2493
  display: flex;
2079
2494
  align-items: center;
@@ -2118,34 +2533,101 @@ const InfoText = styled__default.default.div`
2118
2533
  margin-top: 8px;
2119
2534
  font-style: italic;
2120
2535
  `;
2121
- const OPERATORS = [
2122
- { value: "eq", label: "is" },
2123
- { value: "ne", label: "is not" },
2124
- { value: "contains", label: "contains" },
2125
- { value: "notContains", label: "not contains" },
2126
- { value: "startsWith", label: "starts with" },
2127
- { value: "endsWith", label: "ends with" },
2128
- { value: "gt", label: "is greater than" },
2129
- { value: "lt", label: "is less than" },
2130
- { value: "null", label: "is null" },
2131
- { value: "notNull", label: "is not null" }
2132
- ];
2536
+ const ModalFooter = styled__default.default.div`
2537
+ flex-shrink: 0;
2538
+ padding-top: 16px;
2539
+ border-top: 1px solid ${(p) => p.theme.colors.neutral200};
2540
+ margin-top: 8px;
2541
+ `;
2542
+ const GroupContainer = styled__default.default.div`
2543
+ border: 2px solid ${(p) => p.$logic === "AND" ? "#0EA5E9" : "#F97316"};
2544
+ border-radius: 8px;
2545
+ padding: 16px;
2546
+ margin: ${(p) => p.$level > 0 ? "12px 0 12px 20px" : "0"};
2547
+ background: ${(p) => p.$logic === "AND" ? "rgba(14, 165, 233, 0.03)" : "rgba(249, 115, 22, 0.03)"};
2548
+ position: relative;
2549
+ ${(p) => p.$level > 0 && `border-style: dashed;`}
2550
+ `;
2551
+ const GroupHeader = styled__default.default.div`
2552
+ display: flex;
2553
+ align-items: center;
2554
+ justify-content: space-between;
2555
+ margin-bottom: 12px;
2556
+ padding-bottom: 10px;
2557
+ border-bottom: 1px solid ${(p) => p.theme.colors.neutral200};
2558
+ `;
2559
+ const LogicBadge = styled__default.default.div`
2560
+ display: inline-flex;
2561
+ align-items: center;
2562
+ gap: 8px;
2563
+ font-size: 12px;
2564
+ font-weight: 600;
2565
+ color: ${(p) => p.$logic === "AND" ? "#0284C7" : "#EA580C"};
2566
+ `;
2567
+ const LogicToggleButton = styled__default.default.button`
2568
+ padding: 4px 12px;
2569
+ border-radius: 999px;
2570
+ border: 1px solid ${(p) => p.$logic === "AND" ? "#0EA5E9" : "#F97316"};
2571
+ background: ${(p) => p.$active ? p.$logic === "AND" ? "#0EA5E9" : "#F97316" : "transparent"};
2572
+ color: ${(p) => p.$active ? "white" : p.$logic === "AND" ? "#0284C7" : "#EA580C"};
2573
+ font-size: 11px;
2574
+ font-weight: 600;
2575
+ cursor: pointer;
2576
+ transition: all 0.15s ease;
2577
+ &:hover {
2578
+ transform: scale(1.05);
2579
+ }
2580
+ `;
2581
+ const GroupActions = styled__default.default.div`
2582
+ display: flex;
2583
+ gap: 6px;
2584
+ align-items: center;
2585
+ `;
2586
+ const SmallButton = styled__default.default.button`
2587
+ padding: 4px 10px;
2588
+ font-size: 11px;
2589
+ font-weight: 500;
2590
+ border: 1px solid ${(p) => p.theme.colors.neutral300};
2591
+ border-radius: 4px;
2592
+ background: ${(p) => p.theme.colors.neutral0};
2593
+ color: ${(p) => p.theme.colors.neutral700};
2594
+ cursor: pointer;
2595
+ &:hover {
2596
+ border-color: #4945ff;
2597
+ background: rgba(73, 69, 255, 0.06);
2598
+ }
2599
+ &:disabled {
2600
+ opacity: 0.5;
2601
+ cursor: not-allowed;
2602
+ }
2603
+ `;
2604
+ const RemoveGroupButton = styled__default.default(SmallButton)`
2605
+ color: #dc2626;
2606
+ border-color: rgba(239, 68, 68, 0.3);
2607
+ &:hover {
2608
+ border-color: #dc2626;
2609
+ background: rgba(239, 68, 68, 0.06);
2610
+ }
2611
+ `;
2133
2612
  const StrapiStyleFilterModal = ({
2134
2613
  onClose,
2135
2614
  onApply,
2136
2615
  availableFields,
2137
2616
  availableRelations = [],
2138
- currentQuery = ""
2617
+ currentQuery = "",
2618
+ enableNestedGroups = false
2139
2619
  }) => {
2140
2620
  const { getRelationFields } = useRelationSchema();
2141
2621
  const [fieldOptions, setFieldOptions] = React.useState([]);
2142
2622
  const [rows, setRows] = React.useState([]);
2143
2623
  const [connectors, setConnectors] = React.useState([]);
2624
+ const [rootGroup, setRootGroup] = React.useState(null);
2144
2625
  const buildFieldOptions = React.useCallback(async () => {
2145
2626
  const direct = availableFields.map((f) => ({
2146
2627
  value: f.name,
2147
2628
  label: f.name,
2148
- group: "Fields"
2629
+ group: "Fields",
2630
+ type: f.type
2149
2631
  }));
2150
2632
  const relationOptions = [];
2151
2633
  for (const rel of availableRelations) {
@@ -2157,7 +2639,8 @@ const StrapiStyleFilterModal = ({
2157
2639
  relationOptions.push({
2158
2640
  value: `${rel.name}.${f.name}`,
2159
2641
  label: `${rel.name} > ${f.name}`,
2160
- group: "Relations"
2642
+ group: "Relations",
2643
+ type: f.type
2161
2644
  });
2162
2645
  });
2163
2646
  }
@@ -2171,27 +2654,94 @@ const StrapiStyleFilterModal = ({
2171
2654
  buildFieldOptions();
2172
2655
  }, [buildFieldOptions]);
2173
2656
  React.useEffect(() => {
2174
- if (currentQuery) {
2175
- const { rows: parsedRows, connectors: parsedConnectors } = parseQueryToRows(currentQuery);
2176
- if (parsedRows.length > 0) {
2177
- setRows(parsedRows);
2178
- setConnectors(parsedConnectors);
2179
- return;
2657
+ if (enableNestedGroups) {
2658
+ setRootGroup({
2659
+ id: "root",
2660
+ logic: "AND",
2661
+ items: [{
2662
+ id: `row_${Date.now()}`,
2663
+ field: "",
2664
+ operator: "eq",
2665
+ value: ""
2666
+ }],
2667
+ isGroup: true
2668
+ });
2669
+ } else {
2670
+ if (currentQuery) {
2671
+ const { rows: parsedRows, connectors: parsedConnectors } = parseQueryToRows(currentQuery);
2672
+ if (parsedRows.length > 0) {
2673
+ setRows(parsedRows);
2674
+ setConnectors(parsedConnectors);
2675
+ return;
2676
+ }
2180
2677
  }
2678
+ setRows([{
2679
+ id: `row_${Date.now()}`,
2680
+ field: "",
2681
+ operator: "eq",
2682
+ value: ""
2683
+ }]);
2684
+ setConnectors([]);
2181
2685
  }
2182
- setRows([{
2183
- id: `row_${Date.now()}`,
2184
- field: "",
2185
- operator: "eq",
2186
- value: ""
2187
- }]);
2188
- setConnectors([]);
2189
- }, [currentQuery]);
2190
- const relationsInUse = new Set(
2686
+ }, [currentQuery, enableNestedGroups]);
2687
+ React.useEffect(() => {
2688
+ if (fieldOptions.length === 0) return;
2689
+ if (!enableNestedGroups) {
2690
+ setRows((prev) => prev.map((r) => {
2691
+ if (!r.field || r.fieldType) return r;
2692
+ const opt = fieldOptions.find((f) => f.value === r.field);
2693
+ return opt?.type ? { ...r, fieldType: opt.type } : r;
2694
+ }));
2695
+ } else if (rootGroup) {
2696
+ const enrichGroup = (group) => ({
2697
+ ...group,
2698
+ items: group.items.map((item) => {
2699
+ if ("isGroup" in item && item.isGroup) {
2700
+ return enrichGroup(item);
2701
+ }
2702
+ const row = item;
2703
+ if (!row.field || row.fieldType) return row;
2704
+ const opt = fieldOptions.find((f) => f.value === row.field);
2705
+ return opt?.type ? { ...row, fieldType: opt.type } : row;
2706
+ })
2707
+ });
2708
+ setRootGroup((prev) => prev ? enrichGroup(prev) : prev);
2709
+ }
2710
+ }, [fieldOptions, enableNestedGroups]);
2711
+ const relationsInUse = enableNestedGroups && rootGroup ? (() => {
2712
+ const relations = /* @__PURE__ */ new Set();
2713
+ const extract = (group) => {
2714
+ group.items.forEach((item) => {
2715
+ if ("isGroup" in item && item.isGroup) {
2716
+ extract(item);
2717
+ } else {
2718
+ const row = item;
2719
+ if (row.field?.includes(".")) {
2720
+ relations.add(row.field.split(".")[0]);
2721
+ }
2722
+ }
2723
+ });
2724
+ };
2725
+ extract(rootGroup);
2726
+ return relations;
2727
+ })() : new Set(
2191
2728
  rows.map((r) => r.field.includes(".") ? r.field.split(".")[0] : null).filter(Boolean)
2192
2729
  );
2193
2730
  const updateRow = (index2, updates) => {
2194
- setRows((prev) => prev.map((r, i) => i === index2 ? { ...r, ...updates } : r));
2731
+ setRows((prev) => prev.map((r, i) => {
2732
+ if (i !== index2) return r;
2733
+ const merged = { ...r, ...updates };
2734
+ if ("field" in updates && updates.field !== void 0) {
2735
+ const opt = fieldOptions.find((f) => f.value === updates.field);
2736
+ merged.fieldType = opt?.type;
2737
+ const ops = getOperatorsForType(opt?.type || "string");
2738
+ const firstOp = ops[0]?.value ?? "eq";
2739
+ if (!ops.some((o) => o.value === merged.operator)) {
2740
+ merged.operator = firstOp;
2741
+ }
2742
+ }
2743
+ return merged;
2744
+ }));
2195
2745
  };
2196
2746
  const updateConnector = (index2, logic) => {
2197
2747
  setConnectors((prev) => {
@@ -2217,8 +2767,153 @@ const StrapiStyleFilterModal = ({
2217
2767
  return prev.filter((_, i) => i !== connectorIndex);
2218
2768
  });
2219
2769
  };
2770
+ const updateGroupLogic = (groupId, logic) => {
2771
+ if (!rootGroup) return;
2772
+ const update = (group) => {
2773
+ if (group.id === groupId) {
2774
+ return { ...group, logic };
2775
+ }
2776
+ return {
2777
+ ...group,
2778
+ items: group.items.map(
2779
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2780
+ )
2781
+ };
2782
+ };
2783
+ setRootGroup(update(rootGroup));
2784
+ };
2785
+ const updateRowInGroup = (groupId, rowId, updates) => {
2786
+ if (!rootGroup) return;
2787
+ const update = (group) => {
2788
+ if (group.id === groupId) {
2789
+ return {
2790
+ ...group,
2791
+ items: group.items.map((item) => {
2792
+ if (!("isGroup" in item) || !item.isGroup) {
2793
+ const row = item;
2794
+ if (row.id === rowId) {
2795
+ const merged = { ...row, ...updates };
2796
+ if ("field" in updates && updates.field !== void 0) {
2797
+ const opt = fieldOptions.find((f) => f.value === updates.field);
2798
+ merged.fieldType = opt?.type;
2799
+ const ops = getOperatorsForType(opt?.type || "string");
2800
+ const firstOp = ops[0]?.value ?? "eq";
2801
+ if (!ops.some((o) => o.value === merged.operator)) {
2802
+ merged.operator = firstOp;
2803
+ }
2804
+ }
2805
+ return merged;
2806
+ }
2807
+ }
2808
+ return item;
2809
+ })
2810
+ };
2811
+ }
2812
+ return {
2813
+ ...group,
2814
+ items: group.items.map(
2815
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2816
+ )
2817
+ };
2818
+ };
2819
+ setRootGroup(update(rootGroup));
2820
+ };
2821
+ const addRowToGroup = (groupId) => {
2822
+ if (!rootGroup) return;
2823
+ const newRow = {
2824
+ id: `row_${Date.now()}`,
2825
+ field: "",
2826
+ operator: "eq",
2827
+ value: ""
2828
+ };
2829
+ const update = (group) => {
2830
+ if (group.id === groupId) {
2831
+ return { ...group, items: [...group.items, newRow] };
2832
+ }
2833
+ return {
2834
+ ...group,
2835
+ items: group.items.map(
2836
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2837
+ )
2838
+ };
2839
+ };
2840
+ setRootGroup(update(rootGroup));
2841
+ };
2842
+ const removeRowFromGroup = (groupId, rowId) => {
2843
+ if (!rootGroup) return;
2844
+ const update = (group) => {
2845
+ if (group.id === groupId) {
2846
+ return {
2847
+ ...group,
2848
+ items: group.items.filter((item) => {
2849
+ if ("isGroup" in item && item.isGroup) return true;
2850
+ return item.id !== rowId;
2851
+ })
2852
+ };
2853
+ }
2854
+ return {
2855
+ ...group,
2856
+ items: group.items.map(
2857
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2858
+ )
2859
+ };
2860
+ };
2861
+ setRootGroup(update(rootGroup));
2862
+ };
2863
+ const addNestedGroup = (parentGroupId) => {
2864
+ if (!rootGroup) return;
2865
+ const newGroup = {
2866
+ id: `group_${Date.now()}`,
2867
+ logic: "AND",
2868
+ items: [{
2869
+ id: `row_${Date.now()}`,
2870
+ field: "",
2871
+ operator: "eq",
2872
+ value: ""
2873
+ }],
2874
+ isGroup: true
2875
+ };
2876
+ const update = (group) => {
2877
+ if (group.id === parentGroupId) {
2878
+ return { ...group, items: [...group.items, newGroup] };
2879
+ }
2880
+ return {
2881
+ ...group,
2882
+ items: group.items.map(
2883
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2884
+ )
2885
+ };
2886
+ };
2887
+ setRootGroup(update(rootGroup));
2888
+ };
2889
+ const removeGroup = (groupId) => {
2890
+ if (!rootGroup || groupId === "root") return;
2891
+ const update = (group) => ({
2892
+ ...group,
2893
+ items: group.items.filter((item) => {
2894
+ if ("isGroup" in item && item.isGroup) {
2895
+ return item.id !== groupId;
2896
+ }
2897
+ return true;
2898
+ }).map(
2899
+ (item) => "isGroup" in item && item.isGroup ? update(item) : item
2900
+ )
2901
+ });
2902
+ setRootGroup(update(rootGroup));
2903
+ };
2220
2904
  const handleApply = () => {
2221
- const validRows = rows.filter((r) => r.field && (["null", "notNull"].includes(r.operator) || r.value.trim()));
2905
+ if (enableNestedGroups && rootGroup) {
2906
+ const queryString2 = generateFromGroup(rootGroup);
2907
+ onApply(queryString2);
2908
+ onClose();
2909
+ return;
2910
+ }
2911
+ const validRows = rows.filter((r) => {
2912
+ if (!r.field) return false;
2913
+ if (["null", "notNull"].includes(r.operator)) return true;
2914
+ if (r.operator === "between") return r.value.trim() && r.valueTo?.trim();
2915
+ return r.value.trim();
2916
+ });
2222
2917
  if (validRows.length === 0) {
2223
2918
  onApply("");
2224
2919
  onClose();
@@ -2229,100 +2924,322 @@ const StrapiStyleFilterModal = ({
2229
2924
  onClose();
2230
2925
  };
2231
2926
  const handleClearAll = () => {
2232
- setRows([{
2233
- id: `row_${Date.now()}`,
2234
- field: "",
2235
- operator: "eq",
2236
- value: ""
2237
- }]);
2238
- setConnectors([]);
2927
+ if (enableNestedGroups) {
2928
+ setRootGroup({
2929
+ id: "root",
2930
+ logic: "AND",
2931
+ items: [{
2932
+ id: `row_${Date.now()}`,
2933
+ field: "",
2934
+ operator: "eq",
2935
+ value: ""
2936
+ }],
2937
+ isGroup: true
2938
+ });
2939
+ } else {
2940
+ setRows([{
2941
+ id: `row_${Date.now()}`,
2942
+ field: "",
2943
+ operator: "eq",
2944
+ value: ""
2945
+ }]);
2946
+ setConnectors([]);
2947
+ }
2948
+ };
2949
+ const countActiveFilters = (group) => {
2950
+ let count = 0;
2951
+ group.items.forEach((item) => {
2952
+ if ("isGroup" in item && item.isGroup) {
2953
+ count += countActiveFilters(item);
2954
+ } else {
2955
+ const row = item;
2956
+ if (row.field && (["null", "notNull"].includes(row.operator) || row.value && row.value.trim())) {
2957
+ count++;
2958
+ }
2959
+ }
2960
+ });
2961
+ return count;
2962
+ };
2963
+ const activeCount = enableNestedGroups && rootGroup ? countActiveFilters(rootGroup) : rows.filter((r) => r.field && (["null", "notNull"].includes(r.operator) || r.value && r.value.trim())).length;
2964
+ const renderGroup = (group, level = 0) => {
2965
+ return /* @__PURE__ */ jsxRuntime.jsxs(GroupContainer, { $logic: group.logic, $level: level, children: [
2966
+ /* @__PURE__ */ jsxRuntime.jsxs(GroupHeader, { children: [
2967
+ /* @__PURE__ */ jsxRuntime.jsxs(LogicBadge, { $logic: group.logic, children: [
2968
+ /* @__PURE__ */ jsxRuntime.jsx(
2969
+ LogicToggleButton,
2970
+ {
2971
+ $active: group.logic === "AND",
2972
+ $logic: "AND",
2973
+ onClick: () => updateGroupLogic(group.id, "AND"),
2974
+ type: "button",
2975
+ children: "AND"
2976
+ }
2977
+ ),
2978
+ /* @__PURE__ */ jsxRuntime.jsx(
2979
+ LogicToggleButton,
2980
+ {
2981
+ $active: group.logic === "OR",
2982
+ $logic: "OR",
2983
+ onClick: () => updateGroupLogic(group.id, "OR"),
2984
+ type: "button",
2985
+ children: "OR"
2986
+ }
2987
+ )
2988
+ ] }),
2989
+ /* @__PURE__ */ jsxRuntime.jsxs(GroupActions, { children: [
2990
+ /* @__PURE__ */ jsxRuntime.jsx(SmallButton, { onClick: () => addRowToGroup(group.id), type: "button", children: "+ Row" }),
2991
+ /* @__PURE__ */ jsxRuntime.jsx(SmallButton, { onClick: () => addNestedGroup(group.id), type: "button", children: "+ Group" }),
2992
+ group.id !== "root" && /* @__PURE__ */ jsxRuntime.jsx(RemoveGroupButton, { onClick: () => removeGroup(group.id), type: "button", children: "Remove" })
2993
+ ] })
2994
+ ] }),
2995
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: group.items.map((item) => {
2996
+ if ("isGroup" in item && item.isGroup) {
2997
+ return renderGroup(item, level + 1);
2998
+ }
2999
+ const row = item;
3000
+ return /* @__PURE__ */ jsxRuntime.jsxs(FilterRowCard, { children: [
3001
+ /* @__PURE__ */ jsxRuntime.jsxs(FilterRowTop, { children: [
3002
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 200px", minWidth: "140px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
3003
+ CustomSelect,
3004
+ {
3005
+ value: row.field,
3006
+ onChange: (v) => updateRowInGroup(group.id, row.id, { field: v }),
3007
+ options: [
3008
+ { value: "", label: "Select field..." },
3009
+ ...fieldOptions
3010
+ ],
3011
+ placeholder: "Select field",
3012
+ searchable: true
3013
+ }
3014
+ ) }),
3015
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 140px", minWidth: "120px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
3016
+ CustomSelect,
3017
+ {
3018
+ value: row.operator,
3019
+ onChange: (v) => updateRowInGroup(group.id, row.id, { operator: v }),
3020
+ options: getOperatorsForType(row.fieldType).map((o) => ({ value: o.value, label: o.label })),
3021
+ placeholder: "Operator",
3022
+ searchable: false
3023
+ }
3024
+ ) }),
3025
+ /* @__PURE__ */ jsxRuntime.jsxs(NegateCheckbox, { title: "Negate this condition (NOT)", children: [
3026
+ /* @__PURE__ */ jsxRuntime.jsx(
3027
+ "input",
3028
+ {
3029
+ type: "checkbox",
3030
+ checked: row.negate || false,
3031
+ onChange: (e) => updateRowInGroup(group.id, row.id, { negate: e.target.checked })
3032
+ }
3033
+ ),
3034
+ "NOT"
3035
+ ] }),
3036
+ /* @__PURE__ */ jsxRuntime.jsx(
3037
+ DeleteButton,
3038
+ {
3039
+ type: "button",
3040
+ onClick: () => removeRowFromGroup(group.id, row.id),
3041
+ title: "Remove filter",
3042
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {})
3043
+ }
3044
+ )
3045
+ ] }),
3046
+ !["null", "notNull"].includes(row.operator) && /* @__PURE__ */ jsxRuntime.jsxs(FilterRowValue, { children: [
3047
+ /* @__PURE__ */ jsxRuntime.jsx(ValueLabel, { children: ["in", "notIn"].includes(row.operator) ? "Enter values (press Enter after each)" : ["between"].includes(row.operator) ? "Enter range" : "Enter value" }),
3048
+ /* @__PURE__ */ jsxRuntime.jsx(
3049
+ SmartValueInput,
3050
+ {
3051
+ fieldType: row.fieldType,
3052
+ operator: row.operator,
3053
+ value: row.value,
3054
+ valueTo: row.valueTo,
3055
+ onChange: (v) => updateRowInGroup(group.id, row.id, { value: v }),
3056
+ onValueToChange: (v) => updateRowInGroup(group.id, row.id, { valueTo: v }),
3057
+ placeholder: "Value"
3058
+ }
3059
+ )
3060
+ ] })
3061
+ ] }, row.id);
3062
+ }) })
3063
+ ] }, group.id);
2239
3064
  };
2240
- const operatorOptions = OPERATORS.map((o) => ({ value: o.value, label: o.label }));
2241
3065
  return /* @__PURE__ */ jsxRuntime.jsx(ModalOverlay, { onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsxs(ModalContent, { padding: 6, onClick: (e) => e.stopPropagation(), children: [
2242
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, children: [
2243
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { as: "h2", variant: "beta", children: "Filters" }),
3066
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, style: { flexShrink: 0 }, children: [
3067
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", children: [
3068
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { as: "h2", variant: "beta", children: "Advanced Filter" }),
3069
+ activeCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(ActiveBadge, { children: activeCount })
3070
+ ] }),
2244
3071
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: onClose, variant: "ghost", type: "button", children: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {}) })
2245
3072
  ] }),
2246
- /* @__PURE__ */ jsxRuntime.jsx(RowWrapper, { children: rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsxs(React__default.default.Fragment, { children: [
2247
- i > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ConnectorRow, { children: [
2248
- /* @__PURE__ */ jsxRuntime.jsx(
2249
- ConnectorButton,
2250
- {
2251
- type: "button",
2252
- $active: connectors[i - 1]?.logic === "AND",
2253
- onClick: () => updateConnector(i - 1, "AND"),
2254
- children: "and"
2255
- }
2256
- ),
2257
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { margin: "0 8px", color: "var(--colors-neutral500, #8e8ea9)" }, children: "|" }),
2258
- /* @__PURE__ */ jsxRuntime.jsx(
2259
- ConnectorButton,
2260
- {
2261
- type: "button",
2262
- $active: connectors[i - 1]?.logic === "OR",
2263
- onClick: () => updateConnector(i - 1, "OR"),
2264
- children: "or"
2265
- }
2266
- )
2267
- ] }),
2268
- /* @__PURE__ */ jsxRuntime.jsxs(FilterRowContainer, { children: [
2269
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 140px", minWidth: "120px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2270
- CustomSelect,
2271
- {
2272
- value: row.field,
2273
- onChange: (v) => updateRow(i, { field: v }),
2274
- options: [
2275
- { value: "", label: "Select field..." },
2276
- ...fieldOptions
2277
- ],
2278
- placeholder: "Select field",
2279
- searchable: true
2280
- }
2281
- ) }),
2282
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 130px", minWidth: "100px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2283
- CustomSelect,
2284
- {
2285
- value: row.operator,
2286
- onChange: (v) => updateRow(i, { operator: v }),
2287
- options: [
2288
- { value: "eq", label: "is" },
2289
- ...operatorOptions.filter((o) => o.value !== "eq")
2290
- ],
2291
- placeholder: "Operator",
2292
- searchable: false
2293
- }
2294
- ) }),
2295
- !["null", "notNull"].includes(row.operator) && /* @__PURE__ */ jsxRuntime.jsx(
2296
- ValueInput,
2297
- {
2298
- type: "text",
2299
- value: row.value,
2300
- onChange: (e) => updateRow(i, { value: e.target.value }),
2301
- placeholder: "Value"
2302
- }
2303
- ),
2304
- /* @__PURE__ */ jsxRuntime.jsx(
2305
- DeleteButton,
2306
- {
2307
- type: "button",
2308
- onClick: () => removeRow(i),
2309
- title: "Remove filter",
2310
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {})
2311
- }
2312
- )
3073
+ /* @__PURE__ */ jsxRuntime.jsx(ModalScrollArea, { children: enableNestedGroups && rootGroup ? (
3074
+ // Nested Groups Mode
3075
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3076
+ renderGroup(rootGroup, 0),
3077
+ relationsInUse.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs(InfoText, { children: [
3078
+ Array.from(relationsInUse).join(", "),
3079
+ " will be loaded automatically"
3080
+ ] })
2313
3081
  ] })
2314
- ] }, row.id)) }),
2315
- /* @__PURE__ */ jsxRuntime.jsx(AddButton, { type: "button", onClick: addRow, children: "+ Add filter" }),
2316
- relationsInUse.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs(InfoText, { children: [
2317
- Array.from(relationsInUse).join(", "),
2318
- " will be loaded automatically"
2319
- ] }),
2320
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", marginTop: 5, gap: 2, children: [
3082
+ ) : (
3083
+ // Flat Mode
3084
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3085
+ /* @__PURE__ */ jsxRuntime.jsx(RowWrapper, { children: rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsxs(React__default.default.Fragment, { children: [
3086
+ i > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ConnectorPill, { children: [
3087
+ /* @__PURE__ */ jsxRuntime.jsx(
3088
+ ConnectorButton,
3089
+ {
3090
+ type: "button",
3091
+ $active: connectors[i - 1]?.logic === "AND",
3092
+ onClick: () => updateConnector(i - 1, "AND"),
3093
+ children: "and"
3094
+ }
3095
+ ),
3096
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { margin: "0 8px", color: "var(--colors-neutral500, #8e8ea9)" }, children: "|" }),
3097
+ /* @__PURE__ */ jsxRuntime.jsx(
3098
+ ConnectorButton,
3099
+ {
3100
+ type: "button",
3101
+ $active: connectors[i - 1]?.logic === "OR",
3102
+ onClick: () => updateConnector(i - 1, "OR"),
3103
+ children: "or"
3104
+ }
3105
+ )
3106
+ ] }),
3107
+ /* @__PURE__ */ jsxRuntime.jsxs(FilterRowCard, { children: [
3108
+ /* @__PURE__ */ jsxRuntime.jsxs(FilterRowTop, { children: [
3109
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 200px", minWidth: "140px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
3110
+ CustomSelect,
3111
+ {
3112
+ value: row.field,
3113
+ onChange: (v) => updateRow(i, { field: v }),
3114
+ options: [
3115
+ { value: "", label: "Select field..." },
3116
+ ...fieldOptions
3117
+ ],
3118
+ placeholder: "Select field",
3119
+ searchable: true
3120
+ }
3121
+ ) }),
3122
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: "1 1 140px", minWidth: "120px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
3123
+ CustomSelect,
3124
+ {
3125
+ value: row.operator,
3126
+ onChange: (v) => updateRow(i, { operator: v }),
3127
+ options: getOperatorsForType(row.fieldType).map((o) => ({ value: o.value, label: o.label })),
3128
+ placeholder: "Operator",
3129
+ searchable: false
3130
+ }
3131
+ ) }),
3132
+ /* @__PURE__ */ jsxRuntime.jsxs(NegateCheckbox, { title: "Negate this condition (NOT)", children: [
3133
+ /* @__PURE__ */ jsxRuntime.jsx(
3134
+ "input",
3135
+ {
3136
+ type: "checkbox",
3137
+ checked: row.negate || false,
3138
+ onChange: (e) => updateRow(i, { negate: e.target.checked })
3139
+ }
3140
+ ),
3141
+ "NOT"
3142
+ ] }),
3143
+ /* @__PURE__ */ jsxRuntime.jsx(
3144
+ DeleteButton,
3145
+ {
3146
+ type: "button",
3147
+ onClick: () => removeRow(i),
3148
+ title: "Remove filter",
3149
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {})
3150
+ }
3151
+ )
3152
+ ] }),
3153
+ !["null", "notNull"].includes(row.operator) && /* @__PURE__ */ jsxRuntime.jsxs(FilterRowValue, { children: [
3154
+ /* @__PURE__ */ jsxRuntime.jsx(ValueLabel, { children: ["in", "notIn"].includes(row.operator) ? "Enter values (press Enter after each)" : ["between"].includes(row.operator) ? "Enter range" : "Enter value" }),
3155
+ /* @__PURE__ */ jsxRuntime.jsx(
3156
+ SmartValueInput,
3157
+ {
3158
+ fieldType: row.fieldType,
3159
+ operator: row.operator,
3160
+ value: row.value,
3161
+ valueTo: row.valueTo,
3162
+ onChange: (v) => updateRow(i, { value: v }),
3163
+ onValueToChange: (v) => updateRow(i, { valueTo: v }),
3164
+ placeholder: "Value"
3165
+ }
3166
+ )
3167
+ ] })
3168
+ ] })
3169
+ ] }, row.id)) }),
3170
+ relationsInUse.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs(InfoText, { children: [
3171
+ Array.from(relationsInUse).join(", "),
3172
+ " will be loaded automatically"
3173
+ ] })
3174
+ ] })
3175
+ ) }),
3176
+ /* @__PURE__ */ jsxRuntime.jsx(ModalFooter, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
2321
3177
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleClearAll, variant: "tertiary", children: "Clear All" }),
2322
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleApply, variant: "default", children: "Apply Filters" })
2323
- ] })
3178
+ !enableNestedGroups && /* @__PURE__ */ jsxRuntime.jsx(AddButton, { type: "button", onClick: addRow, children: "+ Add Filter" }),
3179
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleApply, variant: "default", children: "Apply" })
3180
+ ] }) })
2324
3181
  ] }) });
2325
3182
  };
3183
+ const FEATURES = {
3184
+ // Free tier features
3185
+ basicBookmarks: { tier: "free", limit: 10 },
3186
+ basicFilters: { tier: "free" },
3187
+ // Premium tier features
3188
+ extendedBookmarks: { tier: "premium", limit: 50 },
3189
+ queryHistory: { tier: "premium" },
3190
+ exportBookmarks: { tier: "premium" },
3191
+ sharedBookmarks: { tier: "premium" },
3192
+ // Advanced tier features
3193
+ unlimitedBookmarks: { tier: "advanced", limit: -1 },
3194
+ advancedFilters: { tier: "advanced" },
3195
+ subGroups: { tier: "advanced" },
3196
+ bulkOperations: { tier: "advanced" },
3197
+ analytics: { tier: "advanced" },
3198
+ customIntegrations: { tier: "advanced" }
3199
+ };
3200
+ const useLicenseInfo = () => {
3201
+ const { get } = admin.useFetchClient();
3202
+ const [limits, setLimits] = React.useState(null);
3203
+ const [isLoading, setIsLoading] = React.useState(true);
3204
+ React.useEffect(() => {
3205
+ const fetchLimits = async () => {
3206
+ try {
3207
+ console.log("[useLicenseInfo] Fetching license limits...");
3208
+ const response = await get("/magic-mark/license/limits");
3209
+ console.log("[useLicenseInfo] Raw API response:", response);
3210
+ console.log("[useLicenseInfo] Response data:", response.data);
3211
+ if (response.data?.success) {
3212
+ console.log("[useLicenseInfo] Setting limits:", response.data.data);
3213
+ setLimits(response.data.data);
3214
+ } else {
3215
+ console.warn("[useLicenseInfo] API returned success=false or missing data");
3216
+ }
3217
+ } catch (error) {
3218
+ console.error("[useLicenseInfo] Error fetching license limits:", error);
3219
+ } finally {
3220
+ setIsLoading(false);
3221
+ }
3222
+ };
3223
+ fetchLimits();
3224
+ }, []);
3225
+ const tier = limits?.tier || "free";
3226
+ const isPremium = tier === "premium" || tier === "advanced";
3227
+ const isAdvanced = tier === "advanced";
3228
+ console.log("[useLicenseInfo] Computed values:", { tier, isPremium, isAdvanced, limits });
3229
+ return {
3230
+ isLoading,
3231
+ limits,
3232
+ tier,
3233
+ isFree: tier === "free",
3234
+ isPremium,
3235
+ isAdvanced,
3236
+ canUseFeature: (feature) => {
3237
+ const featureConfig = FEATURES[feature];
3238
+ const tierOrder = { free: 0, premium: 1, advanced: 2 };
3239
+ return tierOrder[tier] >= tierOrder[featureConfig.tier];
3240
+ }
3241
+ };
3242
+ };
2326
3243
  const FilterButtonGroup = styled__default.default.div`
2327
3244
  display: flex;
2328
3245
  align-items: center;
@@ -2400,6 +3317,7 @@ const AdvancedFilterButton = () => {
2400
3317
  const navigate = reactRouterDom.useNavigate();
2401
3318
  const location = reactRouterDom.useLocation();
2402
3319
  const { get } = admin.useFetchClient();
3320
+ const { isPremium, isAdvanced } = useLicenseInfo();
2403
3321
  const [availableFields, setAvailableFields] = React.useState([]);
2404
3322
  const [availableRelations, setAvailableRelations] = React.useState([]);
2405
3323
  const [hasActiveFilters, setHasActiveFilters] = React.useState(false);
@@ -2544,7 +3462,7 @@ const AdvancedFilterButton = () => {
2544
3462
  title: "Open advanced filter builder",
2545
3463
  children: [
2546
3464
  /* @__PURE__ */ jsxRuntime.jsx(icons.Filter, {}),
2547
- "Filters",
3465
+ "Advanced Filter",
2548
3466
  hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx(ActiveDot, {})
2549
3467
  ]
2550
3468
  }
@@ -2564,7 +3482,8 @@ const AdvancedFilterButton = () => {
2564
3482
  onApply: handleApplyFilters,
2565
3483
  availableFields,
2566
3484
  availableRelations,
2567
- currentQuery: getCurrentFilters()
3485
+ currentQuery: getCurrentFilters(),
3486
+ enableNestedGroups: isPremium || isAdvanced
2568
3487
  }
2569
3488
  )
2570
3489
  ] });
@@ -2587,7 +3506,7 @@ const index = {
2587
3506
  id: `${pluginId}.Admin.MainMenu.PluginName`,
2588
3507
  defaultMessage: name
2589
3508
  },
2590
- Component: () => Promise.resolve().then(() => require("./App-DFDDcq8K.js")),
3509
+ Component: () => Promise.resolve().then(() => require("./App-DNyxeldt.js")),
2591
3510
  permissions: []
2592
3511
  });
2593
3512
  app.createSettingSection(
@@ -2608,7 +3527,7 @@ const index = {
2608
3527
  to: `${pluginId}/upgrade`,
2609
3528
  Component: () => Promise.resolve().then(() => require(
2610
3529
  /* webpackChunkName: "magic-mark-upgrade" */
2611
- "./UpgradePage-CwYDN8bT.js"
3530
+ "./UpgradePage-SzlmS6po.js"
2612
3531
  )),
2613
3532
  permissions: []
2614
3533
  },
@@ -2696,3 +3615,4 @@ exports.CreateEditModal = CreateEditModal;
2696
3615
  exports.getIconById = getIconById;
2697
3616
  exports.index = index;
2698
3617
  exports.pluginId = pluginId;
3618
+ exports.useLicenseInfo = useLicenseInfo;