@open-mercato/ui 0.5.1-develop.2953.6647bb2c43 → 0.5.1-develop.2964.d5ac4a6ebb
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 +8 -0
- package/dist/backend/CrudForm.js +57 -29
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/DataTable.js +32 -14
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/FilterOverlay.js +23 -17
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/JsonBuilder.js +32 -18
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +12 -13
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js +71 -62
- package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +2 -2
- package/dist/backend/date-range/DateRangeSelect.js +11 -10
- package/dist/backend/date-range/DateRangeSelect.js.map +2 -2
- package/dist/backend/date-range/InlineDateRangeSelect.js +10 -22
- package/dist/backend/date-range/InlineDateRangeSelect.js.map +2 -2
- package/dist/backend/detail/ActivitiesSection.js +20 -12
- package/dist/backend/detail/ActivitiesSection.js.map +2 -2
- package/dist/backend/detail/AddressEditor.js +24 -7
- package/dist/backend/detail/AddressEditor.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +12 -6
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +20 -14
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/filters/AdvancedFilterBuilder.js +52 -24
- package/dist/backend/filters/AdvancedFilterBuilder.js.map +2 -2
- package/dist/backend/injection/InjectedField.js +12 -7
- package/dist/backend/injection/InjectedField.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +22 -6
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +2 -2
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +9 -10
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +12 -7
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/messages/useMessageCompose.js +7 -1
- package/dist/backend/messages/useMessageCompose.js.map +2 -2
- package/dist/frontend/LanguageSwitcher.js +19 -14
- package/dist/frontend/LanguageSwitcher.js.map +2 -2
- package/dist/index.js +5 -0
- package/dist/index.js.map +2 -2
- package/dist/primitives/checkbox-field.js +17 -5
- package/dist/primitives/checkbox-field.js.map +2 -2
- package/dist/primitives/input.js +71 -14
- package/dist/primitives/input.js.map +2 -2
- package/dist/primitives/radio-field.js +74 -0
- package/dist/primitives/radio-field.js.map +7 -0
- package/dist/primitives/radio.js +37 -0
- package/dist/primitives/radio.js.map +7 -0
- package/dist/primitives/select.js +155 -0
- package/dist/primitives/select.js.map +7 -0
- package/dist/primitives/switch-field.js +76 -0
- package/dist/primitives/switch-field.js.map +7 -0
- package/dist/primitives/switch.js +17 -3
- package/dist/primitives/switch.js.map +2 -2
- package/dist/primitives/textarea.js +48 -12
- package/dist/primitives/textarea.js.map +2 -2
- package/dist/primitives/tooltip.js +44 -15
- package/dist/primitives/tooltip.js.map +2 -2
- package/package.json +5 -3
- package/src/backend/CrudForm.tsx +104 -37
- package/src/backend/DataTable.tsx +38 -20
- package/src/backend/FilterOverlay.tsx +35 -21
- package/src/backend/JsonBuilder.tsx +38 -20
- package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +23 -6
- package/src/backend/columns/ColumnChooserPanel.tsx +9 -10
- package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +120 -87
- package/src/backend/date-range/DateRangeSelect.tsx +19 -12
- package/src/backend/date-range/InlineDateRangeSelect.tsx +16 -20
- package/src/backend/detail/ActivitiesSection.tsx +35 -23
- package/src/backend/detail/AddressEditor.tsx +30 -16
- package/src/backend/detail/InlineEditors.tsx +21 -11
- package/src/backend/detail/NotesSection.tsx +35 -25
- package/src/backend/filters/AdvancedFilterBuilder.tsx +60 -34
- package/src/backend/injection/InjectedField.tsx +21 -12
- package/src/backend/inputs/ComboboxInput.tsx +4 -0
- package/src/backend/inputs/EventSelect.tsx +30 -17
- package/src/backend/inputs/PhoneNumberField.tsx +2 -2
- package/src/backend/inputs/TimeInput.tsx +9 -10
- package/src/backend/messages/message-compose-form-groups.tsx +21 -12
- package/src/backend/messages/useMessageCompose.ts +20 -1
- package/src/frontend/LanguageSwitcher.tsx +20 -17
- package/src/index.ts +5 -0
- package/src/primitives/checkbox-field.tsx +10 -2
- package/src/primitives/input.tsx +73 -12
- package/src/primitives/radio-field.tsx +92 -0
- package/src/primitives/radio.tsx +42 -0
- package/src/primitives/select.tsx +200 -0
- package/src/primitives/switch-field.tsx +100 -0
- package/src/primitives/switch.tsx +17 -4
- package/src/primitives/textarea.tsx +67 -11
- package/src/primitives/tooltip.tsx +68 -24
|
@@ -3,6 +3,13 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Button } from "../primitives/button.js";
|
|
5
5
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue
|
|
12
|
+
} from "../primitives/select.js";
|
|
6
13
|
import { ComboboxInput } from "./inputs/ComboboxInput.js";
|
|
7
14
|
import { TagsInput } from "./inputs/TagsInput.js";
|
|
8
15
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
@@ -218,14 +225,13 @@ function FilterOverlay({
|
|
|
218
225
|
/* @__PURE__ */ jsx("span", { className: "text-sm", children: opt.label })
|
|
219
226
|
] }, opt.value);
|
|
220
227
|
}) }) : /* @__PURE__ */ jsxs(
|
|
221
|
-
|
|
228
|
+
Select,
|
|
222
229
|
{
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
onChange: (e) => setValue(f.id, e.target.value || void 0),
|
|
230
|
+
value: values[f.id] || void 0,
|
|
231
|
+
onValueChange: (next) => setValue(f.id, next || void 0),
|
|
226
232
|
children: [
|
|
227
|
-
/* @__PURE__ */ jsx(
|
|
228
|
-
(f.options || dynamicOptions[f.id] || []).map((opt) => /* @__PURE__ */ jsx(
|
|
233
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "lg", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("ui.forms.select.emptyOption", "\u2014") }) }),
|
|
234
|
+
/* @__PURE__ */ jsx(SelectContent, { children: (f.options || dynamicOptions[f.id] || []).filter((opt) => opt.value !== "").map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })
|
|
229
235
|
]
|
|
230
236
|
}
|
|
231
237
|
) }),
|
|
@@ -299,20 +305,20 @@ function FilterOverlay({
|
|
|
299
305
|
);
|
|
300
306
|
})(),
|
|
301
307
|
f.type === "checkbox" && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
|
|
302
|
-
|
|
308
|
+
Select,
|
|
303
309
|
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
else if (v === "true") setValue(f.id, true);
|
|
310
|
-
else if (v === "false") setValue(f.id, false);
|
|
310
|
+
value: values[f.id] === true ? "true" : values[f.id] === false ? "false" : void 0,
|
|
311
|
+
onValueChange: (next) => {
|
|
312
|
+
if (!next) setValue(f.id, void 0);
|
|
313
|
+
else if (next === "true") setValue(f.id, true);
|
|
314
|
+
else if (next === "false") setValue(f.id, false);
|
|
311
315
|
},
|
|
312
316
|
children: [
|
|
313
|
-
/* @__PURE__ */ jsx(
|
|
314
|
-
/* @__PURE__ */
|
|
315
|
-
|
|
317
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "lg", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("ui.forms.select.emptyOption", "\u2014") }) }),
|
|
318
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
319
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "true", children: t("common.yes", "Yes") }),
|
|
320
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "false", children: t("common.no", "No") })
|
|
321
|
+
] })
|
|
316
322
|
]
|
|
317
323
|
}
|
|
318
324
|
) })
|
|
@@ -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 { Button } from '../primitives/button'\nimport { Checkbox } from '../primitives/checkbox'\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 <div className=\"grid grid-cols-1 gap-2\">\n <div>\n <div className=\"text-xs text-muted-foreground mb-1\">{t('ui.filters.dateRange.from', 'From')}</div>\n <input\n type=\"date\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id]?.from ?? ''}\n onChange={(e) => setValue(f.id, { ...(values[f.id] ?? {}), from: e.target.value || undefined })}\n />\n </div>\n <div>\n <div className=\"text-xs text-muted-foreground mb-1\">{t('ui.filters.dateRange.to', 'To')}</div>\n <input\n type=\"date\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id]?.to ?? ''}\n onChange={(e) => setValue(f.id, { ...(values[f.id] ?? {}), to: e.target.value || undefined })}\n />\n </div>\n </div>\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 className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id] ?? ''}\n onChange={(e) => setValue(f.id, e.target.value || undefined)}\n >\n <option value=\"\">{t('ui.forms.select.emptyOption', '\u2014')}</option>\n {(f.options || dynamicOptions[f.id] || []).map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\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 className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id] === true ? 'true' : values[f.id] === false ? 'false' : ''}\n onChange={(e) => {\n const v = e.target.value\n if (v === '') setValue(f.id, undefined)\n else if (v === 'true') setValue(f.id, true)\n else if (v === 'false') setValue(f.id, false)\n }}\n >\n <option value=\"\">{t('ui.forms.select.emptyOption', '\u2014')}</option>\n <option value=\"true\">{t('common.yes', 'Yes')}</option>\n <option value=\"false\">{t('common.no', 'No')}</option>\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 { Button } from '../primitives/button'\nimport { Checkbox } from '../primitives/checkbox'\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 <div className=\"grid grid-cols-1 gap-2\">\n <div>\n <div className=\"text-xs text-muted-foreground mb-1\">{t('ui.filters.dateRange.from', 'From')}</div>\n <input\n type=\"date\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id]?.from ?? ''}\n onChange={(e) => setValue(f.id, { ...(values[f.id] ?? {}), from: e.target.value || undefined })}\n />\n </div>\n <div>\n <div className=\"text-xs text-muted-foreground mb-1\">{t('ui.filters.dateRange.to', 'To')}</div>\n <input\n type=\"date\"\n className=\"w-full h-11 rounded border px-2 text-sm\"\n value={values[f.id]?.to ?? ''}\n onChange={(e) => setValue(f.id, { ...(values[f.id] ?? {}), to: e.target.value || undefined })}\n />\n </div>\n </div>\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
|
}
|
|
@@ -3,6 +3,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Button } from "../primitives/button.js";
|
|
5
5
|
import { IconButton } from "../primitives/icon-button.js";
|
|
6
|
+
import { Input } from "../primitives/input.js";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from "../primitives/select.js";
|
|
6
14
|
import { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from "lucide-react";
|
|
7
15
|
function cn(...classes) {
|
|
8
16
|
return classes.filter(Boolean).join(" ");
|
|
@@ -204,50 +212,56 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }) {
|
|
|
204
212
|
] }),
|
|
205
213
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex gap-2 items-center flex-wrap", children: [
|
|
206
214
|
!readOnly && /* @__PURE__ */ jsxs(
|
|
207
|
-
|
|
215
|
+
Select,
|
|
208
216
|
{
|
|
209
217
|
value: type,
|
|
210
|
-
|
|
211
|
-
className: "text-xs border rounded px-1 py-0.5 bg-muted text-foreground focus-visible:ring-1 focus-visible:ring-ring",
|
|
218
|
+
onValueChange: (next) => handleTypeChange(next),
|
|
212
219
|
children: [
|
|
213
|
-
/* @__PURE__ */ jsx(
|
|
214
|
-
/* @__PURE__ */
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
220
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "w-auto min-w-[6rem]", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
221
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
222
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "string", children: "String" }),
|
|
223
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "number", children: "Number" }),
|
|
224
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "boolean", children: "Boolean" }),
|
|
225
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "object", children: "Object" }),
|
|
226
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "array", children: "Array" }),
|
|
227
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "null", children: "Null" })
|
|
228
|
+
] })
|
|
219
229
|
]
|
|
220
230
|
}
|
|
221
231
|
),
|
|
222
232
|
type === "string" && /* @__PURE__ */ jsx(
|
|
223
|
-
|
|
233
|
+
Input,
|
|
224
234
|
{
|
|
225
|
-
|
|
235
|
+
size: "sm",
|
|
236
|
+
className: "flex-1 min-w-0 sm:min-w-[120px]",
|
|
226
237
|
value: data,
|
|
227
238
|
onChange: (e) => onChange(e.target.value),
|
|
228
239
|
disabled: readOnly
|
|
229
240
|
}
|
|
230
241
|
),
|
|
231
242
|
type === "number" && /* @__PURE__ */ jsx(
|
|
232
|
-
|
|
243
|
+
Input,
|
|
233
244
|
{
|
|
234
245
|
type: "number",
|
|
235
|
-
|
|
246
|
+
size: "sm",
|
|
247
|
+
className: "flex-1 w-full sm:w-[100px]",
|
|
236
248
|
value: data,
|
|
237
249
|
onChange: (e) => onChange(parseFloat(e.target.value) || 0),
|
|
238
250
|
disabled: readOnly
|
|
239
251
|
}
|
|
240
252
|
),
|
|
241
253
|
type === "boolean" && /* @__PURE__ */ jsxs(
|
|
242
|
-
|
|
254
|
+
Select,
|
|
243
255
|
{
|
|
244
|
-
className: "flex-1 w-full sm:w-[100px] text-sm border rounded px-2 py-0.5 focus-visible:outline-none focus-visible:border-ring",
|
|
245
256
|
value: String(data),
|
|
246
|
-
|
|
257
|
+
onValueChange: (next) => onChange(next === "true"),
|
|
247
258
|
disabled: readOnly,
|
|
248
259
|
children: [
|
|
249
|
-
/* @__PURE__ */ jsx(
|
|
250
|
-
/* @__PURE__ */
|
|
260
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "flex-1 w-full sm:w-[100px]", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
261
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
262
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "true", children: "true" }),
|
|
263
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "false", children: "false" })
|
|
264
|
+
] })
|
|
251
265
|
]
|
|
252
266
|
}
|
|
253
267
|
),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/JsonBuilder.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'\n\nfunction cn(...classes: (string | undefined | null | false)[]) {\n return classes.filter(Boolean).join(' ')\n}\n\nexport type JsonBuilderProps = {\n value: any\n onChange: (value: any) => void\n disabled?: boolean\n error?: string\n}\n\ntype JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'\n\nfunction getJsonType(value: any): JsonNodeType {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonNodeType\n}\n\nexport function JsonBuilder({\n value,\n onChange,\n disabled,\n error\n}: JsonBuilderProps) {\n const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')\n const [rawString, setRawString] = React.useState(() => {\n if (value === null) return '{}'\n return typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '{}')\n })\n const [parseError, setParseError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (value === null) {\n if (!disabled) {\n onChange({})\n }\n setRawString('{}')\n setParseError(null)\n return\n }\n if (typeof value === 'object') {\n setRawString(JSON.stringify(value, null, 2))\n setParseError(null)\n }\n }, [value, disabled, onChange])\n\n const handleRawChange = (str: string) => {\n setRawString(str)\n try {\n if (str.trim() === '') {\n onChange({})\n setParseError(null)\n } else {\n const parsed = JSON.parse(str)\n onChange(parsed)\n setParseError(null)\n }\n } catch (e) {\n onChange(str)\n setParseError(\"Invalid JSON\")\n }\n }\n\n const switchToBuilder = () => {\n try {\n if (typeof value === 'string') {\n JSON.parse(value)\n }\n setMode('builder')\n } catch (e) {\n alert(\"Cannot switch to Builder mode: Invalid JSON\")\n }\n }\n\n return (\n <div className=\"space-y-4 border rounded-md p-4 bg-card\">\n <div className=\"flex items-center space-x-2 border-b pb-2 mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'raw' && \"bg-muted text-foreground\")}\n onClick={() => setMode('raw')}\n >\n <Code className=\"w-4 h-4\" />\n Raw JSON\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'builder' && \"bg-muted text-foreground\")}\n onClick={switchToBuilder}\n >\n <LayoutList className=\"w-4 h-4\" />\n Builder\n </Button>\n </div>\n\n {mode === 'raw' ? (\n <div className=\"space-y-2\">\n <textarea\n value={rawString}\n onChange={(e) => handleRawChange(e.target.value)}\n onBlur={() => {\n try {\n const parsed = JSON.parse(rawString)\n setRawString(JSON.stringify(parsed, null, 2))\n } catch { }\n }}\n placeholder='{\"key\": \"value\"}'\n className=\"w-full rounded border px-3 py-2 min-h-[300px] text-sm font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n disabled={disabled}\n />\n {parseError && (\n <div className=\"text-xs text-red-600\">Invalid JSON format</div>\n )}\n </div>\n ) : (\n <div className=\"min-h-[300px] text-sm overflow-x-auto\">\n {typeof value === 'object' && value !== null ? (\n <JsonNode\n data={value}\n onChange={onChange}\n readOnly={disabled}\n isRoot\n />\n ) : (\n <div className=\"text-muted-foreground italic p-4 text-center\">\n Value is not an object or array. Switch to Raw to edit.\n </div>\n )}\n </div>\n )}\n\n {error && <div className=\"text-xs text-red-600\">{error}</div>}\n </div>\n )\n}\n\ninterface JsonNodeProps {\n data: any\n onChange: (val: any) => void\n onDelete?: () => void\n readOnly?: boolean\n label?: string\n isRoot?: boolean\n}\n\nfunction JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNodeProps) {\n const type = getJsonType(data)\n const isContainer = type === 'object' || type === 'array'\n const [collapsed, setCollapsed] = React.useState(false)\n\n const handleTypeChange = (newType: JsonNodeType) => {\n let newVal: any = ''\n switch (newType) {\n case 'string': newVal = \"\"; break;\n case 'number': newVal = 0; break;\n case 'boolean': newVal = false; break;\n case 'object': newVal = {}; break;\n case 'array': newVal = []; break;\n case 'null': newVal = null; break;\n }\n onChange(newVal)\n }\n\n const handleAddKey = () => {\n if (type === 'object') {\n const newKey = `newKey_${Object.keys(data).length}`\n onChange({ ...data, [newKey]: \"\" })\n } else if (type === 'array') {\n onChange([...data, \"\"])\n }\n }\n\n const handleChildChange = (key: string | number, newVal: any) => {\n if (type === 'object') {\n onChange({ ...data, [key]: newVal })\n } else if (type === 'array') {\n const arr = [...data]\n arr[Number(key)] = newVal\n onChange(arr)\n }\n }\n\n const handleKeyRename = (oldKey: string, newKey: string) => {\n if (oldKey === newKey) return\n const keys = Object.keys(data)\n const newData: any = {}\n keys.forEach(k => {\n if (k === oldKey) {\n newData[newKey] = data[k]\n } else {\n newData[k] = data[k]\n }\n })\n onChange(newData)\n }\n\n const handleChildDelete = (key: string | number) => {\n if (type === 'object') {\n const newData = { ...data }\n delete newData[key as string]\n onChange(newData)\n } else if (type === 'array') {\n onChange(data.filter((_: any, i: number) => i !== key))\n }\n }\n\n return (\n <div className={cn(\"pl-0\", !isRoot && \"pl-4 border-l border-border ml-1\")}>\n <div className=\"flex items-start gap-2 py-1 group\">\n\n {isContainer && (\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" className=\"mt-1 text-muted-foreground hover:text-foreground\" onClick={() => setCollapsed(!collapsed)}>\n {collapsed ? <ChevronRight className=\"w-3 h-3\" /> : <ChevronDown className=\"w-3 h-3\" />}\n </IconButton>\n )}\n {!isContainer && !isRoot && <div className=\"w-3\" />} {/* Spacer */}\n\n {label !== undefined && !isRoot && (\n <div className=\"flex items-center gap-1\">\n <span className=\"text-xs text-muted-foreground font-mono\">\n {label}\n </span>\n <span className=\"text-muted-foreground text-xs\">:</span>\n </div>\n )}\n\n <div className=\"flex-1 flex gap-2 items-center flex-wrap\">\n\n {!readOnly && (\n <select\n value={type}\n onChange={(e) => handleTypeChange(e.target.value as JsonNodeType)}\n className=\"text-xs border rounded px-1 py-0.5 bg-muted text-foreground focus-visible:ring-1 focus-visible:ring-ring\"\n >\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Boolean</option>\n <option value=\"object\">Object</option>\n <option value=\"array\">Array</option>\n <option value=\"null\">Null</option>\n </select>\n )}\n\n {type === 'string' && (\n <input\n className=\"flex-1 min-w-0 sm:min-w-[120px] text-sm border rounded px-2 py-0.5 focus-visible:outline-none focus-visible:border-ring\"\n value={data}\n onChange={e => onChange(e.target.value)}\n disabled={readOnly}\n />\n )}\n {type === 'number' && (\n <input\n type=\"number\"\n className=\"flex-1 w-full sm:w-[100px] text-sm border rounded px-2 py-0.5 focus-visible:outline-none focus-visible:border-ring\"\n value={data}\n onChange={e => onChange(parseFloat(e.target.value) || 0)}\n disabled={readOnly}\n />\n )}\n {type === 'boolean' && (\n <select\n className=\"flex-1 w-full sm:w-[100px] text-sm border rounded px-2 py-0.5 focus-visible:outline-none focus-visible:border-ring\"\n value={String(data)}\n onChange={e => onChange(e.target.value === 'true')}\n disabled={readOnly}\n >\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n )}\n {type === 'null' && <span className=\"text-xs text-muted-foreground\">null</span>}\n {isContainer && (\n <span className=\"text-xs text-muted-foreground\">\n {type === 'object' ? `{ ${Object.keys(data).length} items }` : `[ ${data.length} items ]`}\n </span>\n )}\n\n {onDelete && !readOnly && (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity\"\n title=\"Remove item\"\n onClick={onDelete}\n >\n <Trash2 className=\"w-3.5 h-3.5\" />\n </IconButton>\n )}\n </div>\n </div>\n\n {isContainer && !collapsed && (\n <div className=\"flex flex-col gap-1 w-full pl-2\">\n {type === 'object' && Object.entries(data).map(([key, val], idx) => (\n <div key={idx} className=\"flex\">\n <div className=\"pt-2\">\n {/* Key Renamer */}\n <input\n className=\"w-full sm:w-[100px] text-xs font-mono border-b border-transparent hover:border-gray-300 focus-visible:border-ring bg-transparent focus-visible:outline-none text-right pr-1\"\n value={key}\n onChange={(e) => handleKeyRename(key, e.target.value)}\n disabled={readOnly}\n />\n </div>\n <div className=\"flex-1\">\n <JsonNode\n data={val}\n onChange={(v) => handleChildChange(key, v)}\n onDelete={() => handleChildDelete(key)}\n readOnly={readOnly}\n />\n </div>\n </div>\n ))}\n\n {type === 'array' && (data as any[]).map((val, idx) => (\n <JsonNode\n key={idx}\n label={String(idx)}\n data={val}\n onChange={(v) => handleChildChange(idx, v)}\n onDelete={() => handleChildDelete(idx)}\n readOnly={readOnly}\n />\n ))}\n\n {!readOnly && (\n <div className=\"pl-4 mt-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleAddKey}\n className=\"h-6 text-xs\"\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n Add {type === 'object' ? 'Property' : 'Item'}\n </Button>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Input } from '../primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'\n\nfunction cn(...classes: (string | undefined | null | false)[]) {\n return classes.filter(Boolean).join(' ')\n}\n\nexport type JsonBuilderProps = {\n value: any\n onChange: (value: any) => void\n disabled?: boolean\n error?: string\n}\n\ntype JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'\n\nfunction getJsonType(value: any): JsonNodeType {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonNodeType\n}\n\nexport function JsonBuilder({\n value,\n onChange,\n disabled,\n error\n}: JsonBuilderProps) {\n const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')\n const [rawString, setRawString] = React.useState(() => {\n if (value === null) return '{}'\n return typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '{}')\n })\n const [parseError, setParseError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (value === null) {\n if (!disabled) {\n onChange({})\n }\n setRawString('{}')\n setParseError(null)\n return\n }\n if (typeof value === 'object') {\n setRawString(JSON.stringify(value, null, 2))\n setParseError(null)\n }\n }, [value, disabled, onChange])\n\n const handleRawChange = (str: string) => {\n setRawString(str)\n try {\n if (str.trim() === '') {\n onChange({})\n setParseError(null)\n } else {\n const parsed = JSON.parse(str)\n onChange(parsed)\n setParseError(null)\n }\n } catch (e) {\n onChange(str)\n setParseError(\"Invalid JSON\")\n }\n }\n\n const switchToBuilder = () => {\n try {\n if (typeof value === 'string') {\n JSON.parse(value)\n }\n setMode('builder')\n } catch (e) {\n alert(\"Cannot switch to Builder mode: Invalid JSON\")\n }\n }\n\n return (\n <div className=\"space-y-4 border rounded-md p-4 bg-card\">\n <div className=\"flex items-center space-x-2 border-b pb-2 mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'raw' && \"bg-muted text-foreground\")}\n onClick={() => setMode('raw')}\n >\n <Code className=\"w-4 h-4\" />\n Raw JSON\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'builder' && \"bg-muted text-foreground\")}\n onClick={switchToBuilder}\n >\n <LayoutList className=\"w-4 h-4\" />\n Builder\n </Button>\n </div>\n\n {mode === 'raw' ? (\n <div className=\"space-y-2\">\n <textarea\n value={rawString}\n onChange={(e) => handleRawChange(e.target.value)}\n onBlur={() => {\n try {\n const parsed = JSON.parse(rawString)\n setRawString(JSON.stringify(parsed, null, 2))\n } catch { }\n }}\n placeholder='{\"key\": \"value\"}'\n className=\"w-full rounded border px-3 py-2 min-h-[300px] text-sm font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n disabled={disabled}\n />\n {parseError && (\n <div className=\"text-xs text-red-600\">Invalid JSON format</div>\n )}\n </div>\n ) : (\n <div className=\"min-h-[300px] text-sm overflow-x-auto\">\n {typeof value === 'object' && value !== null ? (\n <JsonNode\n data={value}\n onChange={onChange}\n readOnly={disabled}\n isRoot\n />\n ) : (\n <div className=\"text-muted-foreground italic p-4 text-center\">\n Value is not an object or array. Switch to Raw to edit.\n </div>\n )}\n </div>\n )}\n\n {error && <div className=\"text-xs text-red-600\">{error}</div>}\n </div>\n )\n}\n\ninterface JsonNodeProps {\n data: any\n onChange: (val: any) => void\n onDelete?: () => void\n readOnly?: boolean\n label?: string\n isRoot?: boolean\n}\n\nfunction JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNodeProps) {\n const type = getJsonType(data)\n const isContainer = type === 'object' || type === 'array'\n const [collapsed, setCollapsed] = React.useState(false)\n\n const handleTypeChange = (newType: JsonNodeType) => {\n let newVal: any = ''\n switch (newType) {\n case 'string': newVal = \"\"; break;\n case 'number': newVal = 0; break;\n case 'boolean': newVal = false; break;\n case 'object': newVal = {}; break;\n case 'array': newVal = []; break;\n case 'null': newVal = null; break;\n }\n onChange(newVal)\n }\n\n const handleAddKey = () => {\n if (type === 'object') {\n const newKey = `newKey_${Object.keys(data).length}`\n onChange({ ...data, [newKey]: \"\" })\n } else if (type === 'array') {\n onChange([...data, \"\"])\n }\n }\n\n const handleChildChange = (key: string | number, newVal: any) => {\n if (type === 'object') {\n onChange({ ...data, [key]: newVal })\n } else if (type === 'array') {\n const arr = [...data]\n arr[Number(key)] = newVal\n onChange(arr)\n }\n }\n\n const handleKeyRename = (oldKey: string, newKey: string) => {\n if (oldKey === newKey) return\n const keys = Object.keys(data)\n const newData: any = {}\n keys.forEach(k => {\n if (k === oldKey) {\n newData[newKey] = data[k]\n } else {\n newData[k] = data[k]\n }\n })\n onChange(newData)\n }\n\n const handleChildDelete = (key: string | number) => {\n if (type === 'object') {\n const newData = { ...data }\n delete newData[key as string]\n onChange(newData)\n } else if (type === 'array') {\n onChange(data.filter((_: any, i: number) => i !== key))\n }\n }\n\n return (\n <div className={cn(\"pl-0\", !isRoot && \"pl-4 border-l border-border ml-1\")}>\n <div className=\"flex items-start gap-2 py-1 group\">\n\n {isContainer && (\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" className=\"mt-1 text-muted-foreground hover:text-foreground\" onClick={() => setCollapsed(!collapsed)}>\n {collapsed ? <ChevronRight className=\"w-3 h-3\" /> : <ChevronDown className=\"w-3 h-3\" />}\n </IconButton>\n )}\n {!isContainer && !isRoot && <div className=\"w-3\" />} {/* Spacer */}\n\n {label !== undefined && !isRoot && (\n <div className=\"flex items-center gap-1\">\n <span className=\"text-xs text-muted-foreground font-mono\">\n {label}\n </span>\n <span className=\"text-muted-foreground text-xs\">:</span>\n </div>\n )}\n\n <div className=\"flex-1 flex gap-2 items-center flex-wrap\">\n\n {!readOnly && (\n <Select\n value={type}\n onValueChange={(next) => handleTypeChange(next as JsonNodeType)}\n >\n <SelectTrigger size=\"sm\" className=\"w-auto min-w-[6rem]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"string\">String</SelectItem>\n <SelectItem value=\"number\">Number</SelectItem>\n <SelectItem value=\"boolean\">Boolean</SelectItem>\n <SelectItem value=\"object\">Object</SelectItem>\n <SelectItem value=\"array\">Array</SelectItem>\n <SelectItem value=\"null\">Null</SelectItem>\n </SelectContent>\n </Select>\n )}\n\n {type === 'string' && (\n <Input\n size=\"sm\"\n className=\"flex-1 min-w-0 sm:min-w-[120px]\"\n value={data}\n onChange={e => onChange(e.target.value)}\n disabled={readOnly}\n />\n )}\n {type === 'number' && (\n <Input\n type=\"number\"\n size=\"sm\"\n className=\"flex-1 w-full sm:w-[100px]\"\n value={data}\n onChange={e => onChange(parseFloat(e.target.value) || 0)}\n disabled={readOnly}\n />\n )}\n {type === 'boolean' && (\n <Select\n value={String(data)}\n onValueChange={(next) => onChange(next === 'true')}\n disabled={readOnly}\n >\n <SelectTrigger size=\"sm\" className=\"flex-1 w-full sm:w-[100px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">true</SelectItem>\n <SelectItem value=\"false\">false</SelectItem>\n </SelectContent>\n </Select>\n )}\n {type === 'null' && <span className=\"text-xs text-muted-foreground\">null</span>}\n {isContainer && (\n <span className=\"text-xs text-muted-foreground\">\n {type === 'object' ? `{ ${Object.keys(data).length} items }` : `[ ${data.length} items ]`}\n </span>\n )}\n\n {onDelete && !readOnly && (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity\"\n title=\"Remove item\"\n onClick={onDelete}\n >\n <Trash2 className=\"w-3.5 h-3.5\" />\n </IconButton>\n )}\n </div>\n </div>\n\n {isContainer && !collapsed && (\n <div className=\"flex flex-col gap-1 w-full pl-2\">\n {type === 'object' && Object.entries(data).map(([key, val], idx) => (\n <div key={idx} className=\"flex\">\n <div className=\"pt-2\">\n {/* Key Renamer */}\n <input\n className=\"w-full sm:w-[100px] text-xs font-mono border-b border-transparent hover:border-gray-300 focus-visible:border-ring bg-transparent focus-visible:outline-none text-right pr-1\"\n value={key}\n onChange={(e) => handleKeyRename(key, e.target.value)}\n disabled={readOnly}\n />\n </div>\n <div className=\"flex-1\">\n <JsonNode\n data={val}\n onChange={(v) => handleChildChange(key, v)}\n onDelete={() => handleChildDelete(key)}\n readOnly={readOnly}\n />\n </div>\n </div>\n ))}\n\n {type === 'array' && (data as any[]).map((val, idx) => (\n <JsonNode\n key={idx}\n label={String(idx)}\n data={val}\n onChange={(v) => handleChildChange(idx, v)}\n onDelete={() => handleChildDelete(idx)}\n readOnly={readOnly}\n />\n ))}\n\n {!readOnly && (\n <div className=\"pl-4 mt-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleAddKey}\n className=\"h-6 text-xs\"\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n Add {type === 'object' ? 'Property' : 'Item'}\n </Button>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { Search, 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 { Input } from "../../primitives/input.js";
|
|
8
9
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
9
10
|
import {
|
|
10
11
|
DndContext,
|
|
@@ -128,19 +129,17 @@ function ColumnChooserSection({
|
|
|
128
129
|
}
|
|
129
130
|
}, [selectedColumns, onToggleColumn]);
|
|
130
131
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
131
|
-
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-t", children: /* @__PURE__ */
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
] }) }),
|
|
132
|
+
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-t", children: /* @__PURE__ */ jsx(
|
|
133
|
+
Input,
|
|
134
|
+
{
|
|
135
|
+
type: "text",
|
|
136
|
+
size: "sm",
|
|
137
|
+
leftIcon: /* @__PURE__ */ jsx(Search, {}),
|
|
138
|
+
placeholder: t("ui.columnChooser.search", "Search columns..."),
|
|
139
|
+
value: searchQuery,
|
|
140
|
+
onChange: (e) => setSearchQuery(e.target.value)
|
|
141
|
+
}
|
|
142
|
+
) }),
|
|
144
143
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
145
144
|
selectedColumns.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "px-4 py-3", children: [
|
|
146
145
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", 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 { 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 <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground\" />\n <input\n type=\"text\"\n className=\"w-full rounded border bg-background pl-8 pr-2 py-2 text-sm\"\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n />\n </div>\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 { 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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|