@open-mercato/ui 0.6.3-develop.3709.1.0f15e09a5f → 0.6.3-develop.3734.1.766f3e785b
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.
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { format } from "date-fns/format";
|
|
5
|
+
import { Info } from "lucide-react";
|
|
5
6
|
import { Button } from "../primitives/button.js";
|
|
6
7
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
7
8
|
import { DateRangePicker } from "../primitives/date-range-picker.js";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
import { ComboboxInput } from "./inputs/ComboboxInput.js";
|
|
16
17
|
import { TagsInput } from "./inputs/TagsInput.js";
|
|
17
18
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
19
|
+
import { SimpleTooltip } from "../primitives/tooltip.js";
|
|
18
20
|
const EMPTY_FILTER_VALUES = {};
|
|
19
21
|
function isPlainObject(value) {
|
|
20
22
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -171,7 +173,10 @@ function FilterOverlay({
|
|
|
171
173
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 space-y-4", children: [
|
|
172
174
|
extraContent ? /* @__PURE__ */ jsx("div", { className: "space-y-2 rounded-md border bg-muted/30 p-3", children: extraContent }) : null,
|
|
173
175
|
filters.map((f) => /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
174
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-sm font-medium", children: [
|
|
177
|
+
f.label,
|
|
178
|
+
f.tooltip ? /* @__PURE__ */ jsx(SimpleTooltip, { content: f.tooltip, children: /* @__PURE__ */ jsx(Info, { className: "size-3.5 text-muted-foreground", "aria-label": "More information" }) }) : null
|
|
179
|
+
] }),
|
|
175
180
|
f.type === "text" && /* @__PURE__ */ jsx(
|
|
176
181
|
"input",
|
|
177
182
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/FilterOverlay.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { format } from 'date-fns/format'\nimport { Button } from '../primitives/button'\nimport { Checkbox } from '../primitives/checkbox'\nimport { DateRangePicker } from '../primitives/date-range-picker'\nimport type { DateRange } from './date-range/dateRanges'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { ComboboxInput } from './inputs/ComboboxInput'\nimport { TagsInput, type TagsInputOption } from './inputs/TagsInput'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type FilterOption = { value: string; label: string; description?: string | null }\n\nexport type FilterDef = {\n id: string\n label: string\n type: 'text' | 'select' | 'checkbox' | 'dateRange' | 'tags' | 'combobox'\n options?: FilterOption[]\n // Optional async loader for options (used by select/tags/combobox)\n loadOptions?: (query?: string) => Promise<FilterOption[]>\n multiple?: boolean\n placeholder?: string\n group?: string\n formatValue?: (value: string) => string\n formatDescription?: (value: string) => string | null | undefined\n}\n\nexport type FilterValues = Record<string, any>\n\nexport type FilterOverlayProps = {\n title?: string\n filters: FilterDef[]\n initialValues: FilterValues\n open: boolean\n onOpenChange: (open: boolean) => void\n onApply: (values: FilterValues) => void\n onClear?: () => void\n extraContent?: React.ReactNode\n}\n\nconst EMPTY_FILTER_VALUES: FilterValues = {}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction normalizeKeys(source: FilterValues | null | undefined): string[] {\n if (!source) return []\n return Object.keys(source).filter((key) => source[key] !== undefined)\n}\n\nfunction areFieldValuesEqual(a: any, b: any): boolean {\n if (a === b) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n if (!areFieldValuesEqual(a[i], b[i])) return false\n }\n return true\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const keysA = normalizeKeys(a as FilterValues)\n const keysB = normalizeKeys(b as FilterValues)\n if (keysA.length !== keysB.length) return false\n for (const key of keysA) {\n if (!keysB.includes(key)) return false\n if (!areFieldValuesEqual((a as FilterValues)[key], (b as FilterValues)[key])) return false\n }\n return true\n }\n return false\n}\n\nfunction areFilterValuesEqual(a?: FilterValues | null, b?: FilterValues | null): boolean {\n if (a === b) return true\n const keysA = normalizeKeys(a || EMPTY_FILTER_VALUES)\n const keysB = normalizeKeys(b || EMPTY_FILTER_VALUES)\n if (keysA.length !== keysB.length) return false\n for (const key of keysA) {\n if (!keysB.includes(key)) return false\n if (!areFieldValuesEqual(a?.[key], b?.[key])) return false\n }\n return true\n}\n\nexport function FilterOverlay({\n title,\n filters,\n initialValues,\n open,\n onOpenChange,\n onApply,\n onClear,\n extraContent,\n}: FilterOverlayProps) {\n const t = useT()\n const defaultTitle = title ?? t('ui.filters.title', 'Filters')\n const [values, setValues] = React.useState<FilterValues>(initialValues)\n React.useEffect(() => {\n setValues((prev) => (areFilterValuesEqual(prev, initialValues) ? prev : initialValues))\n }, [initialValues])\n const filtersSignature = React.useMemo(\n () => filters.map((f) => `${f.id}:${f.type}:${Boolean((f as any).loadOptions)}:${(f.options || []).length}`).join('|'),\n [filters]\n )\n const lastLoadedSignatureRef = React.useRef<string | null>(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const stableFilters = React.useMemo(() => filters, [filtersSignature])\n\n // Load dynamic options for filters that request it\n const [dynamicOptions, setDynamicOptions] = React.useState<Record<string, FilterOption[]>>({})\n React.useEffect(() => {\n if (!open) return\n if (lastLoadedSignatureRef.current === filtersSignature) return\n lastLoadedSignatureRef.current = filtersSignature\n setDynamicOptions({})\n let cancelled = false\n const loadAll = async () => {\n const loaders = filters\n .filter((f): f is FilterDef & { loadOptions: (query?: string) => Promise<FilterOption[]> } => (f as any).loadOptions != null)\n .map(async (f) => {\n try {\n const opts = await (f as any).loadOptions()\n if (!cancelled) setDynamicOptions((prev) => ({ ...prev, [f.id]: opts }))\n } catch {\n // ignore\n }\n })\n await Promise.all(loaders)\n }\n loadAll()\n return () => {\n cancelled = true\n }\n }, [filters, filtersSignature, open])\n React.useEffect(() => {\n if (!open) {\n lastLoadedSignatureRef.current = null\n }\n }, [open])\n\n const setValue = (id: string, v: any) => setValues((prev) => ({ ...prev, [id]: v }))\n\n const handleApply = () => {\n onApply(values)\n onOpenChange(false)\n }\n\n const handleClear = () => {\n setValues({})\n onClear?.()\n }\n\n const tagLoaders = React.useMemo(() => {\n const map = new Map<string, (q?: string) => Promise<Array<string | TagsInputOption>>>()\n for (const f of stableFilters) {\n if (f.type === 'tags' && typeof f.loadOptions === 'function') {\n const fieldId = f.id\n const load = f.loadOptions as (query?: string) => Promise<FilterOption[]>\n map.set(fieldId, async (q?: string) => {\n const query = (q ?? '').trim()\n if (!query.length) return []\n try {\n const opts = await load(query)\n setDynamicOptions((prev) => ({ ...prev, [fieldId]: opts }))\n return opts.map((o) => ({ value: o.value, label: o.label, description: o.description ?? null }))\n } catch {\n return []\n }\n })\n }\n }\n return map\n }, [stableFilters])\n\n const comboboxLoaders = React.useMemo(() => {\n const map = new Map<string, (q?: string) => Promise<FilterOption[]>>()\n for (const f of stableFilters) {\n if (f.type === 'combobox' && typeof f.loadOptions === 'function') {\n map.set(f.id, async (query?: string) => {\n try {\n const opts = await f.loadOptions?.(query)\n setDynamicOptions((prev) => ({ ...prev, [f.id]: opts ?? [] }))\n return opts ?? []\n } catch {\n return []\n }\n })\n }\n }\n return map\n }, [stableFilters])\n\n return (\n <>\n {open && (\n <div className=\"fixed inset-0 z-modal\">\n <div className=\"absolute inset-0 bg-black/20\" onClick={() => onOpenChange(false)} role=\"presentation\" />\n <div className=\"absolute left-0 top-0 h-full w-full sm:w-[380px] bg-background shadow-xl border-r flex flex-col\">\n <div className=\"flex items-center justify-between p-4 border-b\">\n <h2 className=\"text-base font-semibold\">{defaultTitle}</h2>\n <Button variant=\"muted\" size=\"sm\" onClick={() => onOpenChange(false)}>{t('common.close')}</Button>\n </div>\n {/* Top actions: duplicate Clear/Apply */}\n <div className=\"px-4 py-2 border-b flex items-center justify-between gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleClear}>{t('ui.filters.actions.clear', 'Clear')}</Button>\n <Button size=\"sm\" onClick={handleApply}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden=\"true\" className=\"opacity-80\"><path d=\"M3 4h18\"/><path d=\"M6 8h12l-3 8H9L6 8z\"/></svg>\n {t('ui.filters.actions.apply', 'Apply')}\n </Button>\n </div>\n <div className=\"flex-1 overflow-auto p-4 space-y-4\">\n {extraContent ? <div className=\"space-y-2 rounded-md border bg-muted/30 p-3\">{extraContent}</div> : null}\n {filters.map((f) => (\n <div key={f.id} className=\"space-y-2\">\n <div className=\"text-sm font-medium\">{f.label}</div>\n {f.type === 'text' && (\n <input\n type=\"text\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n placeholder={f.placeholder}\n value={values[f.id] ?? ''}\n onChange={(e) => setValue(f.id, e.target.value || undefined)}\n />\n )}\n {f.type === 'dateRange' && (() => {\n const fromStr = values[f.id]?.from\n const toStr = values[f.id]?.to\n const parseISODate = (input: unknown): Date | null => {\n if (typeof input !== 'string' || !input.length) return null\n const candidate = new Date(input)\n return Number.isNaN(candidate.getTime()) ? null : candidate\n }\n const fromDate = parseISODate(fromStr)\n const toDate = parseISODate(toStr)\n const rangeValue: DateRange | null =\n fromDate && toDate\n ? { start: fromDate, end: toDate }\n : fromDate\n ? { start: fromDate, end: fromDate }\n : toDate\n ? { start: toDate, end: toDate }\n : null\n return (\n <DateRangePicker\n value={rangeValue}\n onChange={(next) => {\n if (!next) {\n setValue(f.id, { from: undefined, to: undefined })\n return\n }\n setValue(f.id, {\n from: format(next.start, 'yyyy-MM-dd'),\n to: format(next.end, 'yyyy-MM-dd'),\n })\n }}\n placeholder={t('ui.filters.dateRange.placeholder', 'Pick a date range')}\n />\n )\n })()}\n {f.type === 'select' && (\n <div className=\"space-y-1\">\n {f.multiple ? (\n <div className=\"flex flex-col gap-1\">\n {(f.options || dynamicOptions[f.id] || []).map((opt) => {\n const arr: string[] = Array.isArray(values[f.id]) ? values[f.id] : []\n const checked = arr.includes(opt.value)\n return (\n <label key={opt.value} className=\"inline-flex items-center gap-2 cursor-pointer\">\n <Checkbox\n checked={checked}\n onCheckedChange={(next) => {\n const set = new Set(arr)\n if (next === true) set.add(opt.value)\n else set.delete(opt.value)\n setValue(f.id, Array.from(set))\n }}\n />\n <span className=\"text-sm\">{opt.label}</span>\n </label>\n )\n })}\n </div>\n ) : (\n <Select\n value={values[f.id] || undefined}\n onValueChange={(next) => setValue(f.id, next || undefined)}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue placeholder={t('ui.forms.select.emptyOption', '\u2014')} />\n </SelectTrigger>\n <SelectContent>\n {(f.options || dynamicOptions[f.id] || [])\n .filter((opt) => opt.value !== '')\n .map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n )}\n {f.type === 'combobox' && (() => {\n const staticOptions = f.options || []\n const dynamic = dynamicOptions[f.id] || []\n const optionMap = new Map<string, FilterOption>()\n staticOptions.forEach((opt) => optionMap.set(opt.value, opt))\n dynamic.forEach((opt) => optionMap.set(opt.value, opt))\n const currentValue = typeof values[f.id] === 'string' ? values[f.id] : ''\n const suggestions = Array.from(optionMap.values()).map((opt) => ({\n value: opt.value,\n label: opt.label,\n description: opt.description ?? null,\n }))\n const loadSuggestions = comboboxLoaders.get(f.id)\n return (\n <div className=\"flex items-start gap-2\">\n <div className=\"min-w-0 flex-1\">\n <ComboboxInput\n value={currentValue}\n onChange={(next) => setValue(f.id, next.trim().length ? next : undefined)}\n suggestions={suggestions}\n loadSuggestions={loadSuggestions}\n resolveLabel={(value) => f.formatValue?.(value) ?? optionMap.get(value)?.label ?? value}\n resolveDescription={(value) => optionMap.get(value)?.description ?? null}\n placeholder={f.placeholder}\n allowCustomValues={false}\n />\n </div>\n {currentValue ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0\"\n onClick={() => setValue(f.id, undefined)}\n >\n {t('ui.filters.actions.clear', 'Clear')}\n </Button>\n ) : null}\n </div>\n )\n })()}\n {f.type === 'tags' && (() => {\n const arr: string[] = Array.isArray(values[f.id]) ? values[f.id] : []\n const staticOptions = f.options || []\n const dynamic = dynamicOptions[f.id] || []\n const optionMap = new Map<string, FilterOption>()\n staticOptions.forEach((opt) => optionMap.set(opt.value, opt))\n dynamic.forEach((opt) => optionMap.set(opt.value, opt))\n const loadSuggestions = tagLoaders.get(f.id)\n const resolveTagLabel = f.formatValue\n ? (val: string) => f.formatValue!(val)\n : (val: string) => optionMap.get(val)?.label ?? val\n const resolveTagDescription = f.formatDescription\n ? (val: string) => f.formatDescription!(val) ?? null\n : (val: string) => optionMap.get(val)?.description ?? null\n const suggestionList: TagsInputOption[] = Array.from(optionMap.values()).map((opt) => ({\n value: opt.value,\n label: opt.label,\n description: opt.description ?? null,\n }))\n return (\n <TagsInput\n value={arr}\n suggestions={suggestionList}\n loadSuggestions={loadSuggestions}\n allowCustomValues={false}\n resolveLabel={resolveTagLabel}\n resolveDescription={resolveTagDescription}\n placeholder={f.placeholder}\n onChange={(next) => setValue(f.id, next.length ? next : undefined)}\n />\n )\n })()}\n {f.type === 'checkbox' && (\n <div>\n <Select\n value={values[f.id] === true ? 'true' : values[f.id] === false ? 'false' : undefined}\n onValueChange={(next) => {\n if (!next) setValue(f.id, undefined)\n else if (next === 'true') setValue(f.id, true)\n else if (next === 'false') setValue(f.id, false)\n }}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue placeholder={t('ui.forms.select.emptyOption', '\u2014')} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">{t('common.yes', 'Yes')}</SelectItem>\n <SelectItem value=\"false\">{t('common.no', 'No')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"p-4 border-t flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={handleClear}>{t('ui.filters.actions.clear', 'Clear')}</Button>\n <Button onClick={handleApply}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden=\"true\" className=\"opacity-80\"><path d=\"M3 4h18\"/><path d=\"M6 8h12l-3 8H9L6 8z\"/></svg>\n {t('ui.filters.actions.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </div>\n )}\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { format } from 'date-fns/format'\nimport { Info } from 'lucide-react'\nimport { Button } from '../primitives/button'\nimport { Checkbox } from '../primitives/checkbox'\nimport { DateRangePicker } from '../primitives/date-range-picker'\nimport type { DateRange } from './date-range/dateRanges'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { ComboboxInput } from './inputs/ComboboxInput'\nimport { TagsInput, type TagsInputOption } from './inputs/TagsInput'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { SimpleTooltip } from '../primitives/tooltip'\n\nexport type FilterOption = { value: string; label: string; description?: string | null }\n\nexport type FilterDef = {\n id: string\n label: string\n tooltip?: string\n type: 'text' | 'select' | 'checkbox' | 'dateRange' | 'tags' | 'combobox'\n options?: FilterOption[]\n // Optional async loader for options (used by select/tags/combobox)\n loadOptions?: (query?: string) => Promise<FilterOption[]>\n multiple?: boolean\n placeholder?: string\n group?: string\n formatValue?: (value: string) => string\n formatDescription?: (value: string) => string | null | undefined\n}\n\nexport type FilterValues = Record<string, any>\n\nexport type FilterOverlayProps = {\n title?: string\n filters: FilterDef[]\n initialValues: FilterValues\n open: boolean\n onOpenChange: (open: boolean) => void\n onApply: (values: FilterValues) => void\n onClear?: () => void\n extraContent?: React.ReactNode\n}\n\nconst EMPTY_FILTER_VALUES: FilterValues = {}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value != null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction normalizeKeys(source: FilterValues | null | undefined): string[] {\n if (!source) return []\n return Object.keys(source).filter((key) => source[key] !== undefined)\n}\n\nfunction areFieldValuesEqual(a: any, b: any): boolean {\n if (a === b) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i += 1) {\n if (!areFieldValuesEqual(a[i], b[i])) return false\n }\n return true\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const keysA = normalizeKeys(a as FilterValues)\n const keysB = normalizeKeys(b as FilterValues)\n if (keysA.length !== keysB.length) return false\n for (const key of keysA) {\n if (!keysB.includes(key)) return false\n if (!areFieldValuesEqual((a as FilterValues)[key], (b as FilterValues)[key])) return false\n }\n return true\n }\n return false\n}\n\nfunction areFilterValuesEqual(a?: FilterValues | null, b?: FilterValues | null): boolean {\n if (a === b) return true\n const keysA = normalizeKeys(a || EMPTY_FILTER_VALUES)\n const keysB = normalizeKeys(b || EMPTY_FILTER_VALUES)\n if (keysA.length !== keysB.length) return false\n for (const key of keysA) {\n if (!keysB.includes(key)) return false\n if (!areFieldValuesEqual(a?.[key], b?.[key])) return false\n }\n return true\n}\n\nexport function FilterOverlay({\n title,\n filters,\n initialValues,\n open,\n onOpenChange,\n onApply,\n onClear,\n extraContent,\n}: FilterOverlayProps) {\n const t = useT()\n const defaultTitle = title ?? t('ui.filters.title', 'Filters')\n const [values, setValues] = React.useState<FilterValues>(initialValues)\n React.useEffect(() => {\n setValues((prev) => (areFilterValuesEqual(prev, initialValues) ? prev : initialValues))\n }, [initialValues])\n const filtersSignature = React.useMemo(\n () => filters.map((f) => `${f.id}:${f.type}:${Boolean((f as any).loadOptions)}:${(f.options || []).length}`).join('|'),\n [filters]\n )\n const lastLoadedSignatureRef = React.useRef<string | null>(null)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const stableFilters = React.useMemo(() => filters, [filtersSignature])\n\n // Load dynamic options for filters that request it\n const [dynamicOptions, setDynamicOptions] = React.useState<Record<string, FilterOption[]>>({})\n React.useEffect(() => {\n if (!open) return\n if (lastLoadedSignatureRef.current === filtersSignature) return\n lastLoadedSignatureRef.current = filtersSignature\n setDynamicOptions({})\n let cancelled = false\n const loadAll = async () => {\n const loaders = filters\n .filter((f): f is FilterDef & { loadOptions: (query?: string) => Promise<FilterOption[]> } => (f as any).loadOptions != null)\n .map(async (f) => {\n try {\n const opts = await (f as any).loadOptions()\n if (!cancelled) setDynamicOptions((prev) => ({ ...prev, [f.id]: opts }))\n } catch {\n // ignore\n }\n })\n await Promise.all(loaders)\n }\n loadAll()\n return () => {\n cancelled = true\n }\n }, [filters, filtersSignature, open])\n React.useEffect(() => {\n if (!open) {\n lastLoadedSignatureRef.current = null\n }\n }, [open])\n\n const setValue = (id: string, v: any) => setValues((prev) => ({ ...prev, [id]: v }))\n\n const handleApply = () => {\n onApply(values)\n onOpenChange(false)\n }\n\n const handleClear = () => {\n setValues({})\n onClear?.()\n }\n\n const tagLoaders = React.useMemo(() => {\n const map = new Map<string, (q?: string) => Promise<Array<string | TagsInputOption>>>()\n for (const f of stableFilters) {\n if (f.type === 'tags' && typeof f.loadOptions === 'function') {\n const fieldId = f.id\n const load = f.loadOptions as (query?: string) => Promise<FilterOption[]>\n map.set(fieldId, async (q?: string) => {\n const query = (q ?? '').trim()\n if (!query.length) return []\n try {\n const opts = await load(query)\n setDynamicOptions((prev) => ({ ...prev, [fieldId]: opts }))\n return opts.map((o) => ({ value: o.value, label: o.label, description: o.description ?? null }))\n } catch {\n return []\n }\n })\n }\n }\n return map\n }, [stableFilters])\n\n const comboboxLoaders = React.useMemo(() => {\n const map = new Map<string, (q?: string) => Promise<FilterOption[]>>()\n for (const f of stableFilters) {\n if (f.type === 'combobox' && typeof f.loadOptions === 'function') {\n map.set(f.id, async (query?: string) => {\n try {\n const opts = await f.loadOptions?.(query)\n setDynamicOptions((prev) => ({ ...prev, [f.id]: opts ?? [] }))\n return opts ?? []\n } catch {\n return []\n }\n })\n }\n }\n return map\n }, [stableFilters])\n\n return (\n <>\n {open && (\n <div className=\"fixed inset-0 z-modal\">\n <div className=\"absolute inset-0 bg-black/20\" onClick={() => onOpenChange(false)} role=\"presentation\" />\n <div className=\"absolute left-0 top-0 h-full w-full sm:w-[380px] bg-background shadow-xl border-r flex flex-col\">\n <div className=\"flex items-center justify-between p-4 border-b\">\n <h2 className=\"text-base font-semibold\">{defaultTitle}</h2>\n <Button variant=\"muted\" size=\"sm\" onClick={() => onOpenChange(false)}>{t('common.close')}</Button>\n </div>\n {/* Top actions: duplicate Clear/Apply */}\n <div className=\"px-4 py-2 border-b flex items-center justify-between gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleClear}>{t('ui.filters.actions.clear', 'Clear')}</Button>\n <Button size=\"sm\" onClick={handleApply}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden=\"true\" className=\"opacity-80\"><path d=\"M3 4h18\"/><path d=\"M6 8h12l-3 8H9L6 8z\"/></svg>\n {t('ui.filters.actions.apply', 'Apply')}\n </Button>\n </div>\n <div className=\"flex-1 overflow-auto p-4 space-y-4\">\n {extraContent ? <div className=\"space-y-2 rounded-md border bg-muted/30 p-3\">{extraContent}</div> : null}\n {filters.map((f) => (\n <div key={f.id} className=\"space-y-2\">\n <div className=\"flex items-center gap-1.5 text-sm font-medium\">\n {f.label}\n {f.tooltip ? (\n <SimpleTooltip content={f.tooltip}>\n <Info className=\"size-3.5 text-muted-foreground\" aria-label=\"More information\" />\n </SimpleTooltip>\n ) : null}\n </div>\n {f.type === 'text' && (\n <input\n type=\"text\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n placeholder={f.placeholder}\n value={values[f.id] ?? ''}\n onChange={(e) => setValue(f.id, e.target.value || undefined)}\n />\n )}\n {f.type === 'dateRange' && (() => {\n const fromStr = values[f.id]?.from\n const toStr = values[f.id]?.to\n const parseISODate = (input: unknown): Date | null => {\n if (typeof input !== 'string' || !input.length) return null\n const candidate = new Date(input)\n return Number.isNaN(candidate.getTime()) ? null : candidate\n }\n const fromDate = parseISODate(fromStr)\n const toDate = parseISODate(toStr)\n const rangeValue: DateRange | null =\n fromDate && toDate\n ? { start: fromDate, end: toDate }\n : fromDate\n ? { start: fromDate, end: fromDate }\n : toDate\n ? { start: toDate, end: toDate }\n : null\n return (\n <DateRangePicker\n value={rangeValue}\n onChange={(next) => {\n if (!next) {\n setValue(f.id, { from: undefined, to: undefined })\n return\n }\n setValue(f.id, {\n from: format(next.start, 'yyyy-MM-dd'),\n to: format(next.end, 'yyyy-MM-dd'),\n })\n }}\n placeholder={t('ui.filters.dateRange.placeholder', 'Pick a date range')}\n />\n )\n })()}\n {f.type === 'select' && (\n <div className=\"space-y-1\">\n {f.multiple ? (\n <div className=\"flex flex-col gap-1\">\n {(f.options || dynamicOptions[f.id] || []).map((opt) => {\n const arr: string[] = Array.isArray(values[f.id]) ? values[f.id] : []\n const checked = arr.includes(opt.value)\n return (\n <label key={opt.value} className=\"inline-flex items-center gap-2 cursor-pointer\">\n <Checkbox\n checked={checked}\n onCheckedChange={(next) => {\n const set = new Set(arr)\n if (next === true) set.add(opt.value)\n else set.delete(opt.value)\n setValue(f.id, Array.from(set))\n }}\n />\n <span className=\"text-sm\">{opt.label}</span>\n </label>\n )\n })}\n </div>\n ) : (\n <Select\n value={values[f.id] || undefined}\n onValueChange={(next) => setValue(f.id, next || undefined)}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue placeholder={t('ui.forms.select.emptyOption', '\u2014')} />\n </SelectTrigger>\n <SelectContent>\n {(f.options || dynamicOptions[f.id] || [])\n .filter((opt) => opt.value !== '')\n .map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n )}\n </div>\n )}\n {f.type === 'combobox' && (() => {\n const staticOptions = f.options || []\n const dynamic = dynamicOptions[f.id] || []\n const optionMap = new Map<string, FilterOption>()\n staticOptions.forEach((opt) => optionMap.set(opt.value, opt))\n dynamic.forEach((opt) => optionMap.set(opt.value, opt))\n const currentValue = typeof values[f.id] === 'string' ? values[f.id] : ''\n const suggestions = Array.from(optionMap.values()).map((opt) => ({\n value: opt.value,\n label: opt.label,\n description: opt.description ?? null,\n }))\n const loadSuggestions = comboboxLoaders.get(f.id)\n return (\n <div className=\"flex items-start gap-2\">\n <div className=\"min-w-0 flex-1\">\n <ComboboxInput\n value={currentValue}\n onChange={(next) => setValue(f.id, next.trim().length ? next : undefined)}\n suggestions={suggestions}\n loadSuggestions={loadSuggestions}\n resolveLabel={(value) => f.formatValue?.(value) ?? optionMap.get(value)?.label ?? value}\n resolveDescription={(value) => optionMap.get(value)?.description ?? null}\n placeholder={f.placeholder}\n allowCustomValues={false}\n />\n </div>\n {currentValue ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"shrink-0\"\n onClick={() => setValue(f.id, undefined)}\n >\n {t('ui.filters.actions.clear', 'Clear')}\n </Button>\n ) : null}\n </div>\n )\n })()}\n {f.type === 'tags' && (() => {\n const arr: string[] = Array.isArray(values[f.id]) ? values[f.id] : []\n const staticOptions = f.options || []\n const dynamic = dynamicOptions[f.id] || []\n const optionMap = new Map<string, FilterOption>()\n staticOptions.forEach((opt) => optionMap.set(opt.value, opt))\n dynamic.forEach((opt) => optionMap.set(opt.value, opt))\n const loadSuggestions = tagLoaders.get(f.id)\n const resolveTagLabel = f.formatValue\n ? (val: string) => f.formatValue!(val)\n : (val: string) => optionMap.get(val)?.label ?? val\n const resolveTagDescription = f.formatDescription\n ? (val: string) => f.formatDescription!(val) ?? null\n : (val: string) => optionMap.get(val)?.description ?? null\n const suggestionList: TagsInputOption[] = Array.from(optionMap.values()).map((opt) => ({\n value: opt.value,\n label: opt.label,\n description: opt.description ?? null,\n }))\n return (\n <TagsInput\n value={arr}\n suggestions={suggestionList}\n loadSuggestions={loadSuggestions}\n allowCustomValues={false}\n resolveLabel={resolveTagLabel}\n resolveDescription={resolveTagDescription}\n placeholder={f.placeholder}\n onChange={(next) => setValue(f.id, next.length ? next : undefined)}\n />\n )\n })()}\n {f.type === 'checkbox' && (\n <div>\n <Select\n value={values[f.id] === true ? 'true' : values[f.id] === false ? 'false' : undefined}\n onValueChange={(next) => {\n if (!next) setValue(f.id, undefined)\n else if (next === 'true') setValue(f.id, true)\n else if (next === 'false') setValue(f.id, false)\n }}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue placeholder={t('ui.forms.select.emptyOption', '\u2014')} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">{t('common.yes', 'Yes')}</SelectItem>\n <SelectItem value=\"false\">{t('common.no', 'No')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"p-4 border-t flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={handleClear}>{t('ui.filters.actions.clear', 'Clear')}</Button>\n <Button onClick={handleApply}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" aria-hidden=\"true\" className=\"opacity-80\"><path d=\"M3 4h18\"/><path d=\"M6 8h12l-3 8H9L6 8z\"/></svg>\n {t('ui.filters.actions.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </div>\n )}\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3734.1.766f3e785b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -141,13 +141,13 @@
|
|
|
141
141
|
"remark-gfm": "^4.0.1"
|
|
142
142
|
},
|
|
143
143
|
"peerDependencies": {
|
|
144
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
144
|
+
"@open-mercato/shared": "0.6.3-develop.3734.1.766f3e785b",
|
|
145
145
|
"react": ">=18.0.0",
|
|
146
146
|
"react-dom": ">=18.0.0",
|
|
147
147
|
"react-is": ">=18.0.0"
|
|
148
148
|
},
|
|
149
149
|
"devDependencies": {
|
|
150
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
150
|
+
"@open-mercato/shared": "0.6.3-develop.3734.1.766f3e785b",
|
|
151
151
|
"@testing-library/dom": "^10.4.1",
|
|
152
152
|
"@testing-library/jest-dom": "^6.9.1",
|
|
153
153
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { format } from 'date-fns/format'
|
|
4
|
+
import { Info } from 'lucide-react'
|
|
4
5
|
import { Button } from '../primitives/button'
|
|
5
6
|
import { Checkbox } from '../primitives/checkbox'
|
|
6
7
|
import { DateRangePicker } from '../primitives/date-range-picker'
|
|
@@ -15,12 +16,14 @@ import {
|
|
|
15
16
|
import { ComboboxInput } from './inputs/ComboboxInput'
|
|
16
17
|
import { TagsInput, type TagsInputOption } from './inputs/TagsInput'
|
|
17
18
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
19
|
+
import { SimpleTooltip } from '../primitives/tooltip'
|
|
18
20
|
|
|
19
21
|
export type FilterOption = { value: string; label: string; description?: string | null }
|
|
20
22
|
|
|
21
23
|
export type FilterDef = {
|
|
22
24
|
id: string
|
|
23
25
|
label: string
|
|
26
|
+
tooltip?: string
|
|
24
27
|
type: 'text' | 'select' | 'checkbox' | 'dateRange' | 'tags' | 'combobox'
|
|
25
28
|
options?: FilterOption[]
|
|
26
29
|
// Optional async loader for options (used by select/tags/combobox)
|
|
@@ -220,7 +223,14 @@ export function FilterOverlay({
|
|
|
220
223
|
{extraContent ? <div className="space-y-2 rounded-md border bg-muted/30 p-3">{extraContent}</div> : null}
|
|
221
224
|
{filters.map((f) => (
|
|
222
225
|
<div key={f.id} className="space-y-2">
|
|
223
|
-
<div className="text-sm font-medium">
|
|
226
|
+
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
227
|
+
{f.label}
|
|
228
|
+
{f.tooltip ? (
|
|
229
|
+
<SimpleTooltip content={f.tooltip}>
|
|
230
|
+
<Info className="size-3.5 text-muted-foreground" aria-label="More information" />
|
|
231
|
+
</SimpleTooltip>
|
|
232
|
+
) : null}
|
|
233
|
+
</div>
|
|
224
234
|
{f.type === 'text' && (
|
|
225
235
|
<input
|
|
226
236
|
type="text"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { render, screen } from '@testing-library/react'
|
|
7
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
8
|
+
import { FilterOverlay, type FilterDef, type FilterValues } from '../FilterOverlay'
|
|
9
|
+
|
|
10
|
+
function renderOverlay({
|
|
11
|
+
filters,
|
|
12
|
+
initialValues = {},
|
|
13
|
+
onApply = jest.fn(),
|
|
14
|
+
onClear = jest.fn(),
|
|
15
|
+
}: {
|
|
16
|
+
filters: FilterDef[]
|
|
17
|
+
initialValues?: FilterValues
|
|
18
|
+
onApply?: (v: FilterValues) => void
|
|
19
|
+
onClear?: () => void
|
|
20
|
+
}) {
|
|
21
|
+
return render(
|
|
22
|
+
<I18nProvider locale="en" dict={{}}>
|
|
23
|
+
<FilterOverlay
|
|
24
|
+
open={true}
|
|
25
|
+
onOpenChange={() => {}}
|
|
26
|
+
filters={filters}
|
|
27
|
+
initialValues={initialValues}
|
|
28
|
+
onApply={onApply}
|
|
29
|
+
onClear={onClear}
|
|
30
|
+
/>
|
|
31
|
+
</I18nProvider>,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('FilterOverlay tooltip rendering', () => {
|
|
36
|
+
const filterWithTooltip: FilterDef = {
|
|
37
|
+
id: 'hasObjects',
|
|
38
|
+
label: 'Has related records',
|
|
39
|
+
tooltip: 'Shows messages that have Open Mercato records attached — such as orders, quotes, or customers.',
|
|
40
|
+
type: 'select',
|
|
41
|
+
options: [
|
|
42
|
+
{ value: '', label: 'All' },
|
|
43
|
+
{ value: 'true', label: 'Yes' },
|
|
44
|
+
{ value: 'false', label: 'No' },
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const filterWithoutTooltip: FilterDef = {
|
|
49
|
+
id: 'hasAttachments',
|
|
50
|
+
label: 'Has attachments',
|
|
51
|
+
type: 'select',
|
|
52
|
+
options: [
|
|
53
|
+
{ value: '', label: 'All' },
|
|
54
|
+
{ value: 'true', label: 'Yes' },
|
|
55
|
+
{ value: 'false', label: 'No' },
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it('renders Info icon when tooltip is set on a FilterDef', () => {
|
|
60
|
+
renderOverlay({ filters: [filterWithTooltip] })
|
|
61
|
+
expect(screen.getByLabelText('More information')).toBeInTheDocument()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('does not render Info icon when tooltip is absent', () => {
|
|
65
|
+
renderOverlay({ filters: [filterWithoutTooltip] })
|
|
66
|
+
expect(screen.queryByLabelText('More information')).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('renders label text alongside the tooltip icon', () => {
|
|
70
|
+
renderOverlay({ filters: [filterWithTooltip] })
|
|
71
|
+
expect(screen.getByText('Has related records')).toBeInTheDocument()
|
|
72
|
+
expect(screen.getByLabelText('More information')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('filter without tooltip still renders label correctly', () => {
|
|
76
|
+
renderOverlay({ filters: [filterWithoutTooltip] })
|
|
77
|
+
expect(screen.getByText('Has attachments')).toBeInTheDocument()
|
|
78
|
+
expect(screen.queryByLabelText('More information')).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders tooltip icon for each filter that has tooltip set (multiple filters)', () => {
|
|
82
|
+
const actionFilter: FilterDef = {
|
|
83
|
+
id: 'hasActions',
|
|
84
|
+
label: 'Has action requests',
|
|
85
|
+
tooltip: 'Shows messages where one or more attached records require a response.',
|
|
86
|
+
type: 'select',
|
|
87
|
+
options: [
|
|
88
|
+
{ value: '', label: 'All' },
|
|
89
|
+
{ value: 'true', label: 'Yes' },
|
|
90
|
+
{ value: 'false', label: 'No' },
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
renderOverlay({ filters: [filterWithTooltip, filterWithoutTooltip, actionFilter] })
|
|
94
|
+
const infoIcons = screen.getAllByLabelText('More information')
|
|
95
|
+
expect(infoIcons).toHaveLength(2)
|
|
96
|
+
})
|
|
97
|
+
})
|