@open-mercato/ui 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3287.1.450f4ffb56
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +23 -14
- package/dist/backend/AppShell.js +8 -32
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +18 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +149 -53
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterBar.js +11 -14
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -6
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/filters/ActiveFilterChips.js +142 -0
- package/dist/backend/filters/ActiveFilterChips.js.map +7 -0
- package/dist/backend/filters/AdvancedFilterBuilder.js +689 -163
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterPanel.js +354 -0
- package/dist/backend/filters/AdvancedFilterPanel.js.map +7 -0
- package/dist/backend/filters/FilterEmptyState.js +41 -0
- package/dist/backend/filters/FilterEmptyState.js.map +7 -0
- package/dist/backend/filters/FilterFieldPicker.js +148 -0
- package/dist/backend/filters/FilterFieldPicker.js.map +7 -0
- package/dist/backend/filters/FilteredEmptyResults.js +26 -0
- package/dist/backend/filters/FilteredEmptyResults.js.map +7 -0
- package/dist/backend/filters/QuickFilters.js +41 -0
- package/dist/backend/filters/QuickFilters.js.map +7 -0
- package/dist/backend/filters/filterValidation.js +47 -0
- package/dist/backend/filters/filterValidation.js.map +7 -0
- package/dist/backend/filters/treeReducer.js +159 -0
- package/dist/backend/filters/treeReducer.js.map +7 -0
- package/dist/backend/hooks/useAdvancedFilter.js +82 -1
- package/dist/backend/hooks/useAdvancedFilter.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +11 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventPatternInput.js +5 -2
- package/dist/backend/inputs/EventPatternInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +10 -4
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +51 -47
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +196 -45
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +2 -2
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/utils/api.js +18 -1
- package/dist/backend/utils/api.js.map +2 -2
- package/dist/backend/utils/serverErrors.js +6 -2
- package/dist/backend/utils/serverErrors.js.map +2 -2
- package/dist/backend/utils/useAutoDiscoveredFields.js +1 -0
- package/dist/backend/utils/useAutoDiscoveredFields.js.map +2 -2
- package/dist/backend/utils/useCurrentUserId.js +31 -0
- package/dist/backend/utils/useCurrentUserId.js.map +7 -0
- package/dist/portal/utils/nav.js +17 -0
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/amount-input.js +128 -0
- package/dist/primitives/amount-input.js.map +7 -0
- package/dist/primitives/button-input.js +47 -0
- package/dist/primitives/button-input.js.map +7 -0
- package/dist/primitives/card-input.js +133 -0
- package/dist/primitives/card-input.js.map +7 -0
- package/dist/primitives/dialog.js +7 -5
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/email-input.js +29 -0
- package/dist/primitives/email-input.js.map +7 -0
- package/dist/primitives/password-input.js +64 -0
- package/dist/primitives/password-input.js.map +7 -0
- package/dist/primitives/search-input.js +57 -0
- package/dist/primitives/search-input.js.map +7 -0
- package/dist/primitives/select.js +43 -5
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/tag.js +4 -2
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/website-input.js +56 -0
- package/dist/primitives/website-input.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -30
- package/src/backend/CrudForm.tsx +17 -0
- package/src/backend/DataTable.tsx +307 -63
- package/src/backend/FilterBar.tsx +5 -6
- package/src/backend/__tests__/DataTable.extensions.test.tsx +170 -0
- package/src/backend/__tests__/serverErrors.test.ts +79 -1
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -5
- package/src/backend/filters/ActiveFilterChips.tsx +179 -0
- package/src/backend/filters/AdvancedFilterBuilder.tsx +840 -237
- package/src/backend/filters/AdvancedFilterPanel.tsx +456 -0
- package/src/backend/filters/FilterEmptyState.tsx +51 -0
- package/src/backend/filters/FilterFieldPicker.tsx +139 -0
- package/src/backend/filters/FilteredEmptyResults.tsx +40 -0
- package/src/backend/filters/QuickFilters.tsx +62 -0
- package/src/backend/filters/__tests__/ActiveFilterChips.test.tsx +75 -0
- package/src/backend/filters/__tests__/AdvancedFilterBuilder.test.tsx +210 -0
- package/src/backend/filters/__tests__/AdvancedFilterPanel.test.tsx +212 -0
- package/src/backend/filters/__tests__/FilterEmptyState.test.tsx +31 -0
- package/src/backend/filters/__tests__/FilterFieldPicker.test.tsx +61 -0
- package/src/backend/filters/__tests__/FilteredEmptyResults.test.tsx +35 -0
- package/src/backend/filters/__tests__/QuickFilters.test.tsx +34 -0
- package/src/backend/filters/__tests__/filterValidation.test.ts +69 -0
- package/src/backend/filters/__tests__/treeReducer.test.ts +189 -0
- package/src/backend/filters/filterValidation.ts +61 -0
- package/src/backend/filters/treeReducer.ts +201 -0
- package/src/backend/hooks/__tests__/useAdvancedFilter.test.tsx +213 -0
- package/src/backend/hooks/useAdvancedFilter.ts +130 -0
- package/src/backend/inputs/ComboboxInput.tsx +32 -26
- package/src/backend/inputs/EventPatternInput.tsx +11 -2
- package/src/backend/inputs/EventSelect.tsx +13 -4
- package/src/backend/inputs/LookupSelect.tsx +75 -53
- package/src/backend/inputs/PhoneNumberField.tsx +237 -47
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +30 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +4 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +4 -0
- package/src/backend/messages/message-compose-form-groups.tsx +2 -2
- package/src/backend/utils/api.ts +27 -2
- package/src/backend/utils/serverErrors.ts +15 -8
- package/src/backend/utils/useAutoDiscoveredFields.ts +2 -0
- package/src/backend/utils/useCurrentUserId.ts +40 -0
- package/src/portal/utils/__tests__/nav.test.ts +80 -0
- package/src/portal/utils/nav.ts +23 -1
- package/src/primitives/__tests__/password-input.test.tsx +93 -0
- package/src/primitives/__tests__/search-input.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +26 -0
- package/src/primitives/amount-input.tsx +184 -0
- package/src/primitives/button-input.tsx +68 -0
- package/src/primitives/card-input.tsx +193 -0
- package/src/primitives/dialog.tsx +18 -7
- package/src/primitives/email-input.tsx +36 -0
- package/src/primitives/password-input.tsx +100 -0
- package/src/primitives/search-input.tsx +87 -0
- package/src/primitives/select.tsx +98 -11
- package/src/primitives/tag.tsx +3 -1
- package/src/primitives/website-input.tsx +72 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { ListFilter
|
|
4
|
+
import { ListFilter } from "lucide-react";
|
|
5
5
|
import { Button } from "../primitives/button.js";
|
|
6
|
+
import { SearchInput } from "../primitives/search-input.js";
|
|
6
7
|
import { FilterOverlay } from "./FilterOverlay.js";
|
|
7
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
9
|
function FilterBar({
|
|
@@ -54,19 +55,15 @@ function FilterBar({
|
|
|
54
55
|
}, [values]);
|
|
55
56
|
const containerClass = `flex flex-col ${layout === "inline" ? "gap-1 sm:gap-2" : "gap-2"} w-full`;
|
|
56
57
|
const searchBlock = onSearchChange ? /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-2 ${searchAlign === "right" ? "sm:ml-auto" : ""}`, children: [
|
|
57
|
-
/* @__PURE__ */
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
),
|
|
68
|
-
/* @__PURE__ */ jsx(Search, { "aria-hidden": "true", className: "absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" })
|
|
69
|
-
] }),
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-72 lg:w-80", children: /* @__PURE__ */ jsx(
|
|
59
|
+
SearchInput,
|
|
60
|
+
{
|
|
61
|
+
value: searchDraft,
|
|
62
|
+
onChange: setSearchDraft,
|
|
63
|
+
placeholder: resolvedSearchPlaceholder,
|
|
64
|
+
suppressHydrationWarning: true
|
|
65
|
+
}
|
|
66
|
+
) }),
|
|
70
67
|
searchTrailing ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: searchTrailing }) : null
|
|
71
68
|
] }) : null;
|
|
72
69
|
const controls = /* @__PURE__ */ jsxs("div", { className: `flex flex-wrap items-center gap-2 ${searchAlign === "left" && searchBlock ? "sm:ml-auto" : ""}`, children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/FilterBar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { ListFilter
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { ListFilter } from 'lucide-react'\nimport { Button } from '../primitives/button'\nimport { SearchInput } from '../primitives/search-input'\nimport { FilterDef, FilterOverlay, FilterValues } from './FilterOverlay'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type FilterBarProps = {\n searchValue?: string\n onSearchChange?: (v: string) => void\n searchPlaceholder?: string\n searchAlign?: 'left' | 'right'\n filters?: FilterDef[]\n values?: FilterValues\n onApply?: (values: FilterValues) => void\n onClear?: () => void\n className?: string\n leadingItems?: React.ReactNode\n trailingItems?: React.ReactNode\n /**\n * Items rendered immediately after the search input on the same row.\n * Intended for compact, icon-sized triggers (AI assistants, saved view\n * shortcuts). Stays adjacent to the search input regardless of\n * `searchAlign` and is suppressed when no search input is rendered.\n */\n searchTrailing?: React.ReactNode\n layout?: 'stacked' | 'inline'\n filtersExtraContent?: React.ReactNode\n}\n\nexport function FilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n searchAlign = 'left',\n filters = [],\n values = {},\n onApply,\n onClear,\n className,\n leadingItems,\n trailingItems,\n searchTrailing,\n layout = 'stacked',\n filtersExtraContent,\n}: FilterBarProps) {\n const t = useT()\n const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.filterBar.searchPlaceholder', 'Search')\n const [open, setOpen] = React.useState(false)\n const [searchDraft, setSearchDraft] = React.useState(searchValue ?? '')\n const lastAppliedSearchRef = React.useRef(searchValue ?? '')\n\n React.useEffect(() => {\n const next = searchValue ?? ''\n lastAppliedSearchRef.current = next\n setSearchDraft((prev) => (prev === next ? prev : next))\n }, [searchValue])\n\n React.useEffect(() => {\n if (!onSearchChange) return\n const handle = window.setTimeout(() => {\n if (lastAppliedSearchRef.current === searchDraft) return\n lastAppliedSearchRef.current = searchDraft\n onSearchChange(searchDraft)\n }, 1000)\n return () => {\n window.clearTimeout(handle)\n }\n }, [searchDraft, onSearchChange])\n\n const activeCount = React.useMemo(() => {\n const isActive = (v: any) => {\n if (v == null) return false\n if (typeof v === 'string') return v.trim() !== ''\n if (Array.isArray(v)) return v.length > 0\n if (typeof v === 'object') return Object.values(v).some((x) => x != null && x !== '')\n return Boolean(v)\n }\n return Object.values(values).filter(isActive).length\n }, [values])\n\n const containerClass = `flex flex-col ${layout === 'inline' ? 'gap-1 sm:gap-2' : 'gap-2'} w-full`\n const searchBlock = onSearchChange ? (\n <div className={`flex items-center gap-2 ${searchAlign === 'right' ? 'sm:ml-auto' : ''}`}>\n <div className=\"w-full sm:w-72 lg:w-80\">\n <SearchInput\n value={searchDraft}\n onChange={setSearchDraft}\n placeholder={resolvedSearchPlaceholder}\n suppressHydrationWarning\n />\n </div>\n {searchTrailing ? (\n <div className=\"flex items-center gap-1\">{searchTrailing}</div>\n ) : null}\n </div>\n ) : null\n const controls = (\n <div className={`flex flex-wrap items-center gap-2 ${searchAlign === 'left' && searchBlock ? 'sm:ml-auto' : ''}`}>\n {filters.length > 0 && (\n <Button variant=\"outline\" onClick={() => setOpen(true)}>\n <ListFilter aria-hidden=\"true\" className=\"size-4 opacity-80\" />\n {activeCount\n ? t('ui.filterBar.filtersWithCount', 'Filters {count}', { count: activeCount })\n : t('ui.filterBar.filters', 'Filters')\n }\n </Button>\n )}\n {leadingItems}\n {trailingItems}\n </div>\n )\n\n return (\n <div className={`${containerClass} ${className ?? ''}`}>\n <div className=\"flex flex-wrap items-center gap-2 w-full\">\n {searchAlign === 'left' ? searchBlock : null}\n {controls}\n {searchAlign === 'right' ? searchBlock : null}\n </div>\n {/* Active filter chips */}\n {filters.length > 0 && activeCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1\">\n {filters.map((f) => {\n const v = (values as any)[f.id]\n if (v == null || v === '' || (Array.isArray(v) && v.length === 0)) return null\n const toLabel = (val: any) => {\n if (typeof f.formatValue === 'function' && (typeof val === 'string' || typeof val === 'number')) {\n const formatted = f.formatValue(String(val))\n if (formatted) return formatted\n }\n if (f.type === 'select' && f.options) {\n const o = f.options.find((o) => o.value === val)\n return o ? o.label : String(val)\n }\n if (typeof val === 'object' && val.from == null && val.to == null) return null\n if (typeof val === 'object') {\n const from = val.from ?? ''\n const to = val.to ? ` \u2192 ${val.to}` : ''\n return `${from}${to}`.trim()\n }\n if (val === true) return t('common.yes', 'Yes')\n if (val === false) return t('common.no', 'No')\n return String(val)\n }\n const removeValue = (val?: any) => {\n const next = { ...(values || {}) }\n if (Array.isArray(v) && val !== undefined) next[f.id] = v.filter((x: any) => x !== val)\n else delete (next as any)[f.id]\n onApply?.(next)\n }\n if (Array.isArray(v)) {\n return v.map((item) => (\n <Button key={`${f.id}:${item}`} size=\"sm\" variant=\"outline\" className=\"max-w-[calc(100vw-4rem)] truncate\" onClick={() => removeValue(item)}>\n {f.label}: {toLabel(item)} \u00D7\n </Button>\n ))\n }\n const label = toLabel(v)\n if (!label) return null\n return (\n <Button key={f.id} size=\"sm\" variant=\"outline\" className=\"max-w-[calc(100vw-4rem)] truncate\" onClick={() => removeValue()}>\n {f.label}: {label} \u00D7\n </Button>\n )\n })}\n </div>\n )}\n <FilterOverlay\n title={t('ui.filterOverlay.title', 'Filters')}\n filters={filters}\n initialValues={values}\n open={open}\n onOpenChange={setOpen}\n onApply={(v) => onApply?.(v)}\n onClear={onClear}\n extraContent={filtersExtraContent}\n />\n </div>\n )\n}\n\nexport type { FilterDef, FilterValues } from './FilterOverlay'\n"],
|
|
5
|
+
"mappings": ";AAoFI,SAEI,KAFJ;AAnFJ,YAAY,WAAW;AACvB,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAoB,qBAAmC;AACvD,SAAS,YAAY;AAyBd,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,UAAU,CAAC;AAAA,EACX,SAAS,CAAC;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAAmB;AACjB,QAAM,IAAI,KAAK;AACf,QAAM,4BAA4B,qBAAqB,EAAE,kCAAkC,QAAQ;AACnG,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,eAAe,EAAE;AACtE,QAAM,uBAAuB,MAAM,OAAO,eAAe,EAAE;AAE3D,QAAM,UAAU,MAAM;AACpB,UAAM,OAAO,eAAe;AAC5B,yBAAqB,UAAU;AAC/B,mBAAe,CAAC,SAAU,SAAS,OAAO,OAAO,IAAK;AAAA,EACxD,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,eAAgB;AACrB,UAAM,SAAS,OAAO,WAAW,MAAM;AACrC,UAAI,qBAAqB,YAAY,YAAa;AAClD,2BAAqB,UAAU;AAC/B,qBAAe,WAAW;AAAA,IAC5B,GAAG,GAAI;AACP,WAAO,MAAM;AACX,aAAO,aAAa,MAAM;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,aAAa,cAAc,CAAC;AAEhC,QAAM,cAAc,MAAM,QAAQ,MAAM;AACtC,UAAM,WAAW,CAAC,MAAW;AAC3B,UAAI,KAAK,KAAM,QAAO;AACtB,UAAI,OAAO,MAAM,SAAU,QAAO,EAAE,KAAK,MAAM;AAC/C,UAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,SAAS;AACxC,UAAI,OAAO,MAAM,SAAU,QAAO,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,KAAK,QAAQ,MAAM,EAAE;AACpF,aAAO,QAAQ,CAAC;AAAA,IAClB;AACA,WAAO,OAAO,OAAO,MAAM,EAAE,OAAO,QAAQ,EAAE;AAAA,EAChD,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,iBAAiB,iBAAiB,WAAW,WAAW,mBAAmB,OAAO;AACxF,QAAM,cAAc,iBAClB,qBAAC,SAAI,WAAW,2BAA2B,gBAAgB,UAAU,eAAe,EAAE,IACpF;AAAA,wBAAC,SAAI,WAAU,0BACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU;AAAA,QACV,aAAa;AAAA,QACb,0BAAwB;AAAA;AAAA,IAC1B,GACF;AAAA,IACC,iBACC,oBAAC,SAAI,WAAU,2BAA2B,0BAAe,IACvD;AAAA,KACN,IACE;AACJ,QAAM,WACJ,qBAAC,SAAI,WAAW,qCAAqC,gBAAgB,UAAU,cAAc,eAAe,EAAE,IAC3G;AAAA,YAAQ,SAAS,KAChB,qBAAC,UAAO,SAAQ,WAAU,SAAS,MAAM,QAAQ,IAAI,GACnD;AAAA,0BAAC,cAAW,eAAY,QAAO,WAAU,qBAAoB;AAAA,MAC5D,cACG,EAAE,iCAAiC,mBAAmB,EAAE,OAAO,YAAY,CAAC,IAC5E,EAAE,wBAAwB,SAAS;AAAA,OAEzC;AAAA,IAED;AAAA,IACA;AAAA,KACH;AAGF,SACE,qBAAC,SAAI,WAAW,GAAG,cAAc,IAAI,aAAa,EAAE,IAClD;AAAA,yBAAC,SAAI,WAAU,4CACZ;AAAA,sBAAgB,SAAS,cAAc;AAAA,MACvC;AAAA,MACA,gBAAgB,UAAU,cAAc;AAAA,OAC3C;AAAA,IAEC,QAAQ,SAAS,KAAK,cAAc,KACnC,oBAAC,SAAI,WAAU,qCACZ,kBAAQ,IAAI,CAAC,MAAM;AAClB,YAAM,IAAK,OAAe,EAAE,EAAE;AAC9B,UAAI,KAAK,QAAQ,MAAM,MAAO,MAAM,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAI,QAAO;AAC1E,YAAM,UAAU,CAAC,QAAa;AAC5B,YAAI,OAAO,EAAE,gBAAgB,eAAe,OAAO,QAAQ,YAAY,OAAO,QAAQ,WAAW;AAC/F,gBAAM,YAAY,EAAE,YAAY,OAAO,GAAG,CAAC;AAC3C,cAAI,UAAW,QAAO;AAAA,QACxB;AACA,YAAI,EAAE,SAAS,YAAY,EAAE,SAAS;AACpC,gBAAM,IAAI,EAAE,QAAQ,KAAK,CAACA,OAAMA,GAAE,UAAU,GAAG;AAC/C,iBAAO,IAAI,EAAE,QAAQ,OAAO,GAAG;AAAA,QACjC;AACA,YAAI,OAAO,QAAQ,YAAY,IAAI,QAAQ,QAAQ,IAAI,MAAM,KAAM,QAAO;AAC1E,YAAI,OAAO,QAAQ,UAAU;AAC3B,gBAAM,OAAO,IAAI,QAAQ;AACzB,gBAAM,KAAK,IAAI,KAAK,WAAM,IAAI,EAAE,KAAK;AACrC,iBAAO,GAAG,IAAI,GAAG,EAAE,GAAG,KAAK;AAAA,QAC7B;AACA,YAAI,QAAQ,KAAM,QAAO,EAAE,cAAc,KAAK;AAC9C,YAAI,QAAQ,MAAO,QAAO,EAAE,aAAa,IAAI;AAC7C,eAAO,OAAO,GAAG;AAAA,MACnB;AACA,YAAM,cAAc,CAAC,QAAc;AACjC,cAAM,OAAO,EAAE,GAAI,UAAU,CAAC,EAAG;AACjC,YAAI,MAAM,QAAQ,CAAC,KAAK,QAAQ,OAAW,MAAK,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,MAAW,MAAM,GAAG;AAAA,YACjF,QAAQ,KAAa,EAAE,EAAE;AAC9B,kBAAU,IAAI;AAAA,MAChB;AACA,UAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,eAAO,EAAE,IAAI,CAAC,SACZ,qBAAC,UAA+B,MAAK,MAAK,SAAQ,WAAU,WAAU,qCAAoC,SAAS,MAAM,YAAY,IAAI,GACtI;AAAA,YAAE;AAAA,UAAM;AAAA,UAAG,QAAQ,IAAI;AAAA,UAAE;AAAA,aADf,GAAG,EAAE,EAAE,IAAI,IAAI,EAE5B,CACD;AAAA,MACH;AACA,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,CAAC,MAAO,QAAO;AACnB,aACE,qBAAC,UAAkB,MAAK,MAAK,SAAQ,WAAU,WAAU,qCAAoC,SAAS,MAAM,YAAY,GACrH;AAAA,UAAE;AAAA,QAAM;AAAA,QAAG;AAAA,QAAM;AAAA,WADP,EAAE,EAEf;AAAA,IAEJ,CAAC,GACH;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,EAAE,0BAA0B,SAAS;AAAA,QAC5C;AAAA,QACA,eAAe;AAAA,QACf;AAAA,QACA,cAAc;AAAA,QACd,SAAS,CAAC,MAAM,UAAU,CAAC;AAAA,QAC3B;AAAA,QACA,cAAc;AAAA;AAAA,IAChB;AAAA,KACF;AAEJ;",
|
|
6
6
|
"names": ["o"]
|
|
7
7
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { GripVertical, X, ChevronRight } from "lucide-react";
|
|
5
5
|
import { Button } from "../../primitives/button.js";
|
|
6
6
|
import { IconButton } from "../../primitives/icon-button.js";
|
|
7
7
|
import { Switch } from "../../primitives/switch.js";
|
|
8
|
-
import {
|
|
8
|
+
import { SearchInput } from "../../primitives/search-input.js";
|
|
9
9
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
10
10
|
import {
|
|
11
11
|
DndContext,
|
|
@@ -130,14 +130,12 @@ function ColumnChooserSection({
|
|
|
130
130
|
}, [selectedColumns, onToggleColumn]);
|
|
131
131
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
132
132
|
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-t", children: /* @__PURE__ */ jsx(
|
|
133
|
-
|
|
133
|
+
SearchInput,
|
|
134
134
|
{
|
|
135
|
-
type: "text",
|
|
136
135
|
size: "sm",
|
|
137
|
-
leftIcon: /* @__PURE__ */ jsx(Search, {}),
|
|
138
136
|
placeholder: t("ui.columnChooser.search", "Search columns..."),
|
|
139
137
|
value: searchQuery,
|
|
140
|
-
onChange:
|
|
138
|
+
onChange: setSearchQuery
|
|
141
139
|
}
|
|
142
140
|
) }),
|
|
143
141
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/columns/ColumnChooserPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Search, GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || t('ui.columnChooser.ungrouped', 'Other')\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet, t])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <Input\n type=\"text\"\n size=\"sm\"\n leftIcon={<Search />}\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{group}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { SearchInput } from '../../primitives/search-input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || t('ui.columnChooser.ungrouped', 'Other')\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet, t])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <SearchInput\n size=\"sm\"\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={setSearchQuery}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{group}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { Tag } from "../../primitives/tag.js";
|
|
6
|
+
import { IconButton } from "../../primitives/icon-button.js";
|
|
7
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
|
+
import { isValuelessOperator } from "@open-mercato/shared/lib/query/advanced-filter";
|
|
9
|
+
import { validateTreeForApply } from "./filterValidation.js";
|
|
10
|
+
const MAX_LABEL = 24;
|
|
11
|
+
function truncate(s) {
|
|
12
|
+
if (s.length <= MAX_LABEL) return s;
|
|
13
|
+
return s.slice(0, MAX_LABEL) + "\u2026";
|
|
14
|
+
}
|
|
15
|
+
function getOption(field, value) {
|
|
16
|
+
if (!field?.options) return void 0;
|
|
17
|
+
return field.options.find((o) => o.value === value);
|
|
18
|
+
}
|
|
19
|
+
function valueText(field, value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
if (value.length === 0) return { text: "" };
|
|
22
|
+
const first = String(value[0]);
|
|
23
|
+
const opt2 = getOption(field, first);
|
|
24
|
+
const display = opt2?.label ?? first;
|
|
25
|
+
const more = value.length - 1;
|
|
26
|
+
return {
|
|
27
|
+
text: more > 0 ? `${truncate(display)} +${more}` : truncate(display),
|
|
28
|
+
tone: opt2?.tone
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (value == null || value === "") return { text: "" };
|
|
32
|
+
const str = String(value);
|
|
33
|
+
const opt = getOption(field, str);
|
|
34
|
+
return { text: truncate(opt?.label ?? str), tone: opt?.tone };
|
|
35
|
+
}
|
|
36
|
+
const VALUELESS_OPERATOR_LABEL = {
|
|
37
|
+
is_empty: "is empty",
|
|
38
|
+
is_not_empty: "is not empty",
|
|
39
|
+
is_true: "is true",
|
|
40
|
+
is_false: "is false"
|
|
41
|
+
};
|
|
42
|
+
function ruleChip(rule, fields, t) {
|
|
43
|
+
const field = fields.find((f) => f.key === rule.field);
|
|
44
|
+
const fieldLabel = field?.label ?? rule.field;
|
|
45
|
+
if (isValuelessOperator(rule.operator)) {
|
|
46
|
+
const fallback = VALUELESS_OPERATOR_LABEL[rule.operator] ?? rule.operator;
|
|
47
|
+
const opLabel = t(`ui.advancedFilter.operator.${rule.operator}`, fallback);
|
|
48
|
+
return { label: `${fieldLabel} ${opLabel}` };
|
|
49
|
+
}
|
|
50
|
+
if (rule.operator === "between" && Array.isArray(rule.value)) {
|
|
51
|
+
const [start, end] = rule.value;
|
|
52
|
+
const startStr = start == null ? "" : String(start);
|
|
53
|
+
const endStr = end == null ? "" : String(end);
|
|
54
|
+
return { label: `${fieldLabel}: ${truncate(startStr)} \u2013 ${truncate(endStr)}` };
|
|
55
|
+
}
|
|
56
|
+
const v = valueText(field, rule.value);
|
|
57
|
+
return { label: `${fieldLabel}: ${v.text}`, tone: v.tone };
|
|
58
|
+
}
|
|
59
|
+
function groupChip(group, fields, invalidRuleIds, t) {
|
|
60
|
+
const ruleChildren = [];
|
|
61
|
+
for (const c of group.children) if (c.type === "rule" && !invalidRuleIds.has(c.id)) ruleChildren.push(c);
|
|
62
|
+
if (!ruleChildren.length) return { label: "", remaining: 0 };
|
|
63
|
+
const firstChip = ruleChip(ruleChildren[0], fields, t);
|
|
64
|
+
const remaining = ruleChildren.length - 1;
|
|
65
|
+
return {
|
|
66
|
+
label: remaining > 0 ? `${firstChip.label} +${remaining}` : firstChip.label,
|
|
67
|
+
tone: firstChip.tone,
|
|
68
|
+
remaining
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function isNodeFullyInvalid(node, invalidRuleIds) {
|
|
72
|
+
if (node.type === "rule") return invalidRuleIds.has(node.id);
|
|
73
|
+
if (!node.children.length) return true;
|
|
74
|
+
return node.children.every((c) => isNodeFullyInvalid(c, invalidRuleIds));
|
|
75
|
+
}
|
|
76
|
+
function ActiveFilterChips({ tree, fields, popoverOpen, onRemoveNode, onOpen }) {
|
|
77
|
+
const t = useT();
|
|
78
|
+
const invalidRuleIds = React.useMemo(() => {
|
|
79
|
+
const result = validateTreeForApply(tree, fields);
|
|
80
|
+
if (result.ok) return /* @__PURE__ */ new Set();
|
|
81
|
+
return new Set(result.errors.map((e) => e.ruleId));
|
|
82
|
+
}, [tree, fields]);
|
|
83
|
+
const visibleChildren = React.useMemo(
|
|
84
|
+
() => tree.root.children.filter((child) => !isNodeFullyInvalid(child, invalidRuleIds)),
|
|
85
|
+
[tree.root.children, invalidRuleIds]
|
|
86
|
+
);
|
|
87
|
+
if (popoverOpen) return null;
|
|
88
|
+
if (!visibleChildren.length) return null;
|
|
89
|
+
return /* @__PURE__ */ jsx(
|
|
90
|
+
"div",
|
|
91
|
+
{
|
|
92
|
+
className: "flex items-center gap-2 px-4 py-2 overflow-x-auto bg-background border-b border-border",
|
|
93
|
+
"data-testid": "active-filter-chips",
|
|
94
|
+
children: visibleChildren.map((child) => {
|
|
95
|
+
if (child.type === "rule") {
|
|
96
|
+
const c2 = ruleChip(child, fields, t);
|
|
97
|
+
return /* @__PURE__ */ jsx(
|
|
98
|
+
ChipWithRemove,
|
|
99
|
+
{
|
|
100
|
+
label: c2.label,
|
|
101
|
+
tone: c2.tone,
|
|
102
|
+
onClick: () => onOpen(child.id),
|
|
103
|
+
onRemove: () => onRemoveNode(child.id),
|
|
104
|
+
removeLabel: t("ui.advancedFilter.chips.remove", "Remove filter")
|
|
105
|
+
},
|
|
106
|
+
child.id
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const c = groupChip(child, fields, invalidRuleIds, t);
|
|
110
|
+
return /* @__PURE__ */ jsx(
|
|
111
|
+
ChipWithRemove,
|
|
112
|
+
{
|
|
113
|
+
label: c.label,
|
|
114
|
+
tone: c.tone,
|
|
115
|
+
onClick: () => onOpen(child.id),
|
|
116
|
+
onRemove: () => onRemoveNode(child.id),
|
|
117
|
+
removeLabel: t("ui.advancedFilter.chips.removeGroup", "Remove filter group")
|
|
118
|
+
},
|
|
119
|
+
child.id
|
|
120
|
+
);
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
function ChipWithRemove({ label, tone, onClick, onRemove, removeLabel }) {
|
|
126
|
+
return /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1", "data-testid": "active-filter-chip", "aria-label": label, children: [
|
|
127
|
+
/* @__PURE__ */ jsx(
|
|
128
|
+
"button",
|
|
129
|
+
{
|
|
130
|
+
type: "button",
|
|
131
|
+
onClick,
|
|
132
|
+
className: "inline-flex rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
133
|
+
children: /* @__PURE__ */ jsx(Tag, { variant: tone ?? "default", dot: tone != null, children: label })
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
/* @__PURE__ */ jsx(IconButton, { type: "button", variant: "ghost", size: "xs", "aria-label": removeLabel, onClick: onRemove, children: /* @__PURE__ */ jsx(X, { className: "size-3" }) })
|
|
137
|
+
] });
|
|
138
|
+
}
|
|
139
|
+
export {
|
|
140
|
+
ActiveFilterChips
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=ActiveFilterChips.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/filters/ActiveFilterChips.tsx"],
|
|
4
|
+
"sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { X } from 'lucide-react'\nimport { Tag, type TagVariant } from '../../primitives/tag'\nimport { IconButton } from '../../primitives/icon-button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { AdvancedFilterTree, FilterRule, FilterGroup } from '@open-mercato/shared/lib/query/advanced-filter-tree'\nimport type { FilterFieldDef, FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { isValuelessOperator } from '@open-mercato/shared/lib/query/advanced-filter'\nimport { validateTreeForApply } from './filterValidation'\n\nexport type ActiveFilterChipsProps = {\n tree: AdvancedFilterTree\n fields: FilterFieldDef[]\n popoverOpen: boolean\n onRemoveNode: (id: string) => void\n onOpen: (focusNodeId?: string) => void\n}\n\nconst MAX_LABEL = 24\n\nfunction truncate(s: string): string {\n if (s.length <= MAX_LABEL) return s\n return s.slice(0, MAX_LABEL) + '\u2026'\n}\n\nfunction getOption(field: FilterFieldDef | undefined, value: string): FilterOption | undefined {\n if (!field?.options) return undefined\n return field.options.find((o) => o.value === value)\n}\n\nfunction valueText(field: FilterFieldDef | undefined, value: unknown): { text: string; tone?: TagVariant } {\n if (Array.isArray(value)) {\n if (value.length === 0) return { text: '' }\n const first = String(value[0])\n const opt = getOption(field, first)\n const display = opt?.label ?? first\n const more = value.length - 1\n return {\n text: more > 0 ? `${truncate(display)} +${more}` : truncate(display),\n tone: opt?.tone,\n }\n }\n if (value == null || value === '') return { text: '' }\n const str = String(value)\n const opt = getOption(field, str)\n return { text: truncate(opt?.label ?? str), tone: opt?.tone }\n}\n\n// Operator labels in chips read as natural English: \"Email is empty\" beats\n// \"Email: \" (trailing colon, blank value). Keep the map narrow \u2014 only valueless\n// operators need a phrase form here; value-bearing operators show the value.\nconst VALUELESS_OPERATOR_LABEL: Record<string, string> = {\n is_empty: 'is empty',\n is_not_empty: 'is not empty',\n is_true: 'is true',\n is_false: 'is false',\n}\n\ntype Translator = ReturnType<typeof useT>\n\nfunction ruleChip(rule: FilterRule, fields: FilterFieldDef[], t: Translator): { label: string; tone?: TagVariant } {\n const field = fields.find((f) => f.key === rule.field)\n const fieldLabel = field?.label ?? rule.field\n if (isValuelessOperator(rule.operator)) {\n const fallback = VALUELESS_OPERATOR_LABEL[rule.operator] ?? rule.operator\n const opLabel = t(`ui.advancedFilter.operator.${rule.operator}`, fallback)\n return { label: `${fieldLabel} ${opLabel}` }\n }\n // `between` stores `[start, end]` \u2014 render it as a range, not as the\n // multi-select \"first +N\" summary that `valueText` would produce.\n if (rule.operator === 'between' && Array.isArray(rule.value)) {\n const [start, end] = rule.value\n const startStr = start == null ? '' : String(start)\n const endStr = end == null ? '' : String(end)\n return { label: `${fieldLabel}: ${truncate(startStr)} \u2013 ${truncate(endStr)}` }\n }\n const v = valueText(field, rule.value)\n return { label: `${fieldLabel}: ${v.text}`, tone: v.tone }\n}\n\nfunction groupChip(group: FilterGroup, fields: FilterFieldDef[], invalidRuleIds: Set<string>, t: Translator): { label: string; tone?: TagVariant; remaining: number } {\n const ruleChildren: FilterRule[] = []\n for (const c of group.children) if (c.type === 'rule' && !invalidRuleIds.has(c.id)) ruleChildren.push(c)\n if (!ruleChildren.length) return { label: '', remaining: 0 }\n const firstChip = ruleChip(ruleChildren[0], fields, t)\n const remaining = ruleChildren.length - 1\n return {\n label: remaining > 0 ? `${firstChip.label} +${remaining}` : firstChip.label,\n tone: firstChip.tone,\n remaining,\n }\n}\n\nfunction isNodeFullyInvalid(node: FilterRule | FilterGroup, invalidRuleIds: Set<string>): boolean {\n if (node.type === 'rule') return invalidRuleIds.has(node.id)\n if (!node.children.length) return true\n return node.children.every((c) => isNodeFullyInvalid(c, invalidRuleIds))\n}\n\nexport function ActiveFilterChips({ tree, fields, popoverOpen, onRemoveNode, onOpen }: ActiveFilterChipsProps) {\n const t = useT()\n // Suppress chips for rules/groups whose values won't actually filter (matches\n // the same gate the auto-apply hook uses). Stops the \"chip is shown but data\n // appears unfiltered\" mismatch when a substring text rule has < 2 characters\n // or a multi-value rule has no picks yet.\n const invalidRuleIds = React.useMemo(() => {\n const result = validateTreeForApply(tree, fields)\n if (result.ok) return new Set<string>()\n return new Set(result.errors.map((e) => e.ruleId))\n }, [tree, fields])\n const visibleChildren = React.useMemo(\n () => tree.root.children.filter((child) => !isNodeFullyInvalid(child, invalidRuleIds)),\n [tree.root.children, invalidRuleIds],\n )\n if (popoverOpen) return null\n if (!visibleChildren.length) return null\n return (\n <div\n className=\"flex items-center gap-2 px-4 py-2 overflow-x-auto bg-background border-b border-border\"\n data-testid=\"active-filter-chips\"\n >\n {visibleChildren.map((child) => {\n if (child.type === 'rule') {\n const c = ruleChip(child, fields, t)\n return (\n <ChipWithRemove\n key={child.id}\n label={c.label}\n tone={c.tone}\n onClick={() => onOpen(child.id)}\n onRemove={() => onRemoveNode(child.id)}\n removeLabel={t('ui.advancedFilter.chips.remove', 'Remove filter')}\n />\n )\n }\n const c = groupChip(child, fields, invalidRuleIds, t)\n return (\n <ChipWithRemove\n key={child.id}\n label={c.label}\n tone={c.tone}\n onClick={() => onOpen(child.id)}\n onRemove={() => onRemoveNode(child.id)}\n removeLabel={t('ui.advancedFilter.chips.removeGroup', 'Remove filter group')}\n />\n )\n })}\n </div>\n )\n}\n\ntype ChipWithRemoveProps = {\n label: string\n tone?: TagVariant\n onClick: () => void\n onRemove: () => void\n removeLabel: string\n}\n\nfunction ChipWithRemove({ label, tone, onClick, onRemove, removeLabel }: ChipWithRemoveProps) {\n return (\n <div className=\"inline-flex items-center gap-1\" data-testid=\"active-filter-chip\" aria-label={label}>\n {/* Raw <button> required: <Tag> is a presentational pill primitive (not a Button), so wrapping\n it in <Button> would inject Button's own styling around it. The native button keeps Tag's\n visual contract intact while adding click + keyboard semantics; DS focus-visible ring is applied. */}\n <button\n type=\"button\"\n onClick={onClick}\n className=\"inline-flex rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n >\n <Tag variant={tone ?? 'default'} dot={tone != null}>{label}</Tag>\n </button>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={removeLabel} onClick={onRemove}>\n <X className=\"size-3\" />\n </IconButton>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["opt", "c"]
|
|
7
|
+
}
|