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