@open-mercato/ui 0.6.1-develop.3278.1.a4d26475e4 → 0.6.1-develop.3291.1.6fad645fd0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +23 -14
- package/dist/backend/AppShell.js +8 -32
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +18 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/FilterBar.js +11 -14
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -6
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +11 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventPatternInput.js +5 -2
- package/dist/backend/inputs/EventPatternInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +10 -4
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +51 -47
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +196 -45
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +2 -2
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/utils/serverErrors.js +6 -2
- package/dist/backend/utils/serverErrors.js.map +2 -2
- package/dist/portal/utils/nav.js +17 -0
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/amount-input.js +128 -0
- package/dist/primitives/amount-input.js.map +7 -0
- package/dist/primitives/button-input.js +47 -0
- package/dist/primitives/button-input.js.map +7 -0
- package/dist/primitives/card-input.js +133 -0
- package/dist/primitives/card-input.js.map +7 -0
- package/dist/primitives/email-input.js +29 -0
- package/dist/primitives/email-input.js.map +7 -0
- package/dist/primitives/password-input.js +64 -0
- package/dist/primitives/password-input.js.map +7 -0
- package/dist/primitives/search-input.js +57 -0
- package/dist/primitives/search-input.js.map +7 -0
- package/dist/primitives/select.js +43 -5
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/website-input.js +56 -0
- package/dist/primitives/website-input.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -30
- package/src/backend/CrudForm.tsx +17 -0
- package/src/backend/FilterBar.tsx +5 -6
- package/src/backend/__tests__/serverErrors.test.ts +79 -1
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -5
- package/src/backend/inputs/ComboboxInput.tsx +32 -26
- package/src/backend/inputs/EventPatternInput.tsx +11 -2
- package/src/backend/inputs/EventSelect.tsx +13 -4
- package/src/backend/inputs/LookupSelect.tsx +75 -53
- package/src/backend/inputs/PhoneNumberField.tsx +237 -47
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +30 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +4 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +4 -0
- package/src/backend/messages/message-compose-form-groups.tsx +2 -2
- package/src/backend/utils/serverErrors.ts +15 -8
- package/src/portal/utils/__tests__/nav.test.ts +80 -0
- package/src/portal/utils/nav.ts +23 -1
- package/src/primitives/__tests__/password-input.test.tsx +93 -0
- package/src/primitives/__tests__/search-input.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +4 -0
- package/src/primitives/amount-input.tsx +184 -0
- package/src/primitives/button-input.tsx +68 -0
- package/src/primitives/card-input.tsx +193 -0
- package/src/primitives/email-input.tsx +36 -0
- package/src/primitives/password-input.tsx +100 -0
- package/src/primitives/search-input.tsx +87 -0
- package/src/primitives/select.tsx +98 -11
- package/src/primitives/website-input.tsx +72 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { ListFilter
|
|
4
|
+
import { ListFilter } from "lucide-react";
|
|
5
5
|
import { Button } from "../primitives/button.js";
|
|
6
|
+
import { SearchInput } from "../primitives/search-input.js";
|
|
6
7
|
import { FilterOverlay } from "./FilterOverlay.js";
|
|
7
8
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
9
|
function FilterBar({
|
|
@@ -54,19 +55,15 @@ function FilterBar({
|
|
|
54
55
|
}, [values]);
|
|
55
56
|
const containerClass = `flex flex-col ${layout === "inline" ? "gap-1 sm:gap-2" : "gap-2"} w-full`;
|
|
56
57
|
const searchBlock = onSearchChange ? /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-2 ${searchAlign === "right" ? "sm:ml-auto" : ""}`, children: [
|
|
57
|
-
/* @__PURE__ */
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
),
|
|
68
|
-
/* @__PURE__ */ jsx(Search, { "aria-hidden": "true", className: "absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" })
|
|
69
|
-
] }),
|
|
58
|
+
/* @__PURE__ */ jsx("div", { className: "w-full sm:w-72 lg:w-80", children: /* @__PURE__ */ jsx(
|
|
59
|
+
SearchInput,
|
|
60
|
+
{
|
|
61
|
+
value: searchDraft,
|
|
62
|
+
onChange: setSearchDraft,
|
|
63
|
+
placeholder: resolvedSearchPlaceholder,
|
|
64
|
+
suppressHydrationWarning: true
|
|
65
|
+
}
|
|
66
|
+
) }),
|
|
70
67
|
searchTrailing ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: searchTrailing }) : null
|
|
71
68
|
] }) : null;
|
|
72
69
|
const controls = /* @__PURE__ */ jsxs("div", { className: `flex flex-wrap items-center gap-2 ${searchAlign === "left" && searchBlock ? "sm:ml-auto" : ""}`, children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/FilterBar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { ListFilter
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { ListFilter } from 'lucide-react'\nimport { Button } from '../primitives/button'\nimport { SearchInput } from '../primitives/search-input'\nimport { FilterDef, FilterOverlay, FilterValues } from './FilterOverlay'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type FilterBarProps = {\n searchValue?: string\n onSearchChange?: (v: string) => void\n searchPlaceholder?: string\n searchAlign?: 'left' | 'right'\n filters?: FilterDef[]\n values?: FilterValues\n onApply?: (values: FilterValues) => void\n onClear?: () => void\n className?: string\n leadingItems?: React.ReactNode\n trailingItems?: React.ReactNode\n /**\n * Items rendered immediately after the search input on the same row.\n * Intended for compact, icon-sized triggers (AI assistants, saved view\n * shortcuts). Stays adjacent to the search input regardless of\n * `searchAlign` and is suppressed when no search input is rendered.\n */\n searchTrailing?: React.ReactNode\n layout?: 'stacked' | 'inline'\n filtersExtraContent?: React.ReactNode\n}\n\nexport function FilterBar({\n searchValue,\n onSearchChange,\n searchPlaceholder,\n searchAlign = 'left',\n filters = [],\n values = {},\n onApply,\n onClear,\n className,\n leadingItems,\n trailingItems,\n searchTrailing,\n layout = 'stacked',\n filtersExtraContent,\n}: FilterBarProps) {\n const t = useT()\n const resolvedSearchPlaceholder = searchPlaceholder ?? t('ui.filterBar.searchPlaceholder', 'Search')\n const [open, setOpen] = React.useState(false)\n const [searchDraft, setSearchDraft] = React.useState(searchValue ?? '')\n const lastAppliedSearchRef = React.useRef(searchValue ?? '')\n\n React.useEffect(() => {\n const next = searchValue ?? ''\n lastAppliedSearchRef.current = next\n setSearchDraft((prev) => (prev === next ? prev : next))\n }, [searchValue])\n\n React.useEffect(() => {\n if (!onSearchChange) return\n const handle = window.setTimeout(() => {\n if (lastAppliedSearchRef.current === searchDraft) return\n lastAppliedSearchRef.current = searchDraft\n onSearchChange(searchDraft)\n }, 1000)\n return () => {\n window.clearTimeout(handle)\n }\n }, [searchDraft, onSearchChange])\n\n const activeCount = React.useMemo(() => {\n const isActive = (v: any) => {\n if (v == null) return false\n if (typeof v === 'string') return v.trim() !== ''\n if (Array.isArray(v)) return v.length > 0\n if (typeof v === 'object') return Object.values(v).some((x) => x != null && x !== '')\n return Boolean(v)\n }\n return Object.values(values).filter(isActive).length\n }, [values])\n\n const containerClass = `flex flex-col ${layout === 'inline' ? 'gap-1 sm:gap-2' : 'gap-2'} w-full`\n const searchBlock = onSearchChange ? (\n <div className={`flex items-center gap-2 ${searchAlign === 'right' ? 'sm:ml-auto' : ''}`}>\n <div className=\"w-full sm:w-72 lg:w-80\">\n <SearchInput\n value={searchDraft}\n onChange={setSearchDraft}\n placeholder={resolvedSearchPlaceholder}\n suppressHydrationWarning\n />\n </div>\n {searchTrailing ? (\n <div className=\"flex items-center gap-1\">{searchTrailing}</div>\n ) : null}\n </div>\n ) : null\n const controls = (\n <div className={`flex flex-wrap items-center gap-2 ${searchAlign === 'left' && searchBlock ? 'sm:ml-auto' : ''}`}>\n {filters.length > 0 && (\n <Button variant=\"outline\" onClick={() => setOpen(true)}>\n <ListFilter aria-hidden=\"true\" className=\"size-4 opacity-80\" />\n {activeCount\n ? t('ui.filterBar.filtersWithCount', 'Filters {count}', { count: activeCount })\n : t('ui.filterBar.filters', 'Filters')\n }\n </Button>\n )}\n {leadingItems}\n {trailingItems}\n </div>\n )\n\n return (\n <div className={`${containerClass} ${className ?? ''}`}>\n <div className=\"flex flex-wrap items-center gap-2 w-full\">\n {searchAlign === 'left' ? searchBlock : null}\n {controls}\n {searchAlign === 'right' ? searchBlock : null}\n </div>\n {/* Active filter chips */}\n {filters.length > 0 && activeCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1\">\n {filters.map((f) => {\n const v = (values as any)[f.id]\n if (v == null || v === '' || (Array.isArray(v) && v.length === 0)) return null\n const toLabel = (val: any) => {\n if (typeof f.formatValue === 'function' && (typeof val === 'string' || typeof val === 'number')) {\n const formatted = f.formatValue(String(val))\n if (formatted) return formatted\n }\n if (f.type === 'select' && f.options) {\n const o = f.options.find((o) => o.value === val)\n return o ? o.label : String(val)\n }\n if (typeof val === 'object' && val.from == null && val.to == null) return null\n if (typeof val === 'object') {\n const from = val.from ?? ''\n const to = val.to ? ` \u2192 ${val.to}` : ''\n return `${from}${to}`.trim()\n }\n if (val === true) return t('common.yes', 'Yes')\n if (val === false) return t('common.no', 'No')\n return String(val)\n }\n const removeValue = (val?: any) => {\n const next = { ...(values || {}) }\n if (Array.isArray(v) && val !== undefined) next[f.id] = v.filter((x: any) => x !== val)\n else delete (next as any)[f.id]\n onApply?.(next)\n }\n if (Array.isArray(v)) {\n return v.map((item) => (\n <Button key={`${f.id}:${item}`} size=\"sm\" variant=\"outline\" className=\"max-w-[calc(100vw-4rem)] truncate\" onClick={() => removeValue(item)}>\n {f.label}: {toLabel(item)} \u00D7\n </Button>\n ))\n }\n const label = toLabel(v)\n if (!label) return null\n return (\n <Button key={f.id} size=\"sm\" variant=\"outline\" className=\"max-w-[calc(100vw-4rem)] truncate\" onClick={() => removeValue()}>\n {f.label}: {label} \u00D7\n </Button>\n )\n })}\n </div>\n )}\n <FilterOverlay\n title={t('ui.filterOverlay.title', 'Filters')}\n filters={filters}\n initialValues={values}\n open={open}\n onOpenChange={setOpen}\n onApply={(v) => onApply?.(v)}\n onClear={onClear}\n extraContent={filtersExtraContent}\n />\n </div>\n )\n}\n\nexport type { FilterDef, FilterValues } from './FilterOverlay'\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["o"]
|
|
7
7
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { GripVertical, X, ChevronRight } from "lucide-react";
|
|
5
5
|
import { Button } from "../../primitives/button.js";
|
|
6
6
|
import { IconButton } from "../../primitives/icon-button.js";
|
|
7
7
|
import { Switch } from "../../primitives/switch.js";
|
|
8
|
-
import {
|
|
8
|
+
import { SearchInput } from "../../primitives/search-input.js";
|
|
9
9
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
10
10
|
import {
|
|
11
11
|
DndContext,
|
|
@@ -130,14 +130,12 @@ function ColumnChooserSection({
|
|
|
130
130
|
}, [selectedColumns, onToggleColumn]);
|
|
131
131
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
132
132
|
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-t", children: /* @__PURE__ */ jsx(
|
|
133
|
-
|
|
133
|
+
SearchInput,
|
|
134
134
|
{
|
|
135
|
-
type: "text",
|
|
136
135
|
size: "sm",
|
|
137
|
-
leftIcon: /* @__PURE__ */ jsx(Search, {}),
|
|
138
136
|
placeholder: t("ui.columnChooser.search", "Search columns..."),
|
|
139
137
|
value: searchQuery,
|
|
140
|
-
onChange:
|
|
138
|
+
onChange: setSearchQuery
|
|
141
139
|
}
|
|
142
140
|
) }),
|
|
143
141
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/columns/ColumnChooserPanel.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Search, GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || t('ui.columnChooser.ungrouped', 'Other')\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet, t])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <Input\n type=\"text\"\n size=\"sm\"\n leftIcon={<Search />}\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{group}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { GripVertical, X, ChevronRight } from 'lucide-react'\nimport { Button } from '../../primitives/button'\nimport { IconButton } from '../../primitives/icon-button'\nimport { Switch } from '../../primitives/switch'\nimport { Input } from '../../primitives/input'\nimport { SearchInput } from '../../primitives/search-input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n DndContext,\n closestCenter,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from '@dnd-kit/core'\nimport {\n SortableContext,\n sortableKeyboardCoordinates,\n verticalListSortingStrategy,\n useSortable,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\n\nexport type ColumnChooserField = {\n key: string\n label: string\n group: string\n defaultVisible?: boolean\n alwaysVisible?: boolean\n}\n\nexport type ColumnChooserSectionProps = {\n availableColumns: ColumnChooserField[]\n visibleColumnKeys: string[]\n columnOrder: string[]\n onToggleColumn: (key: string) => void\n onReorderColumns: (newOrder: string[]) => void\n dndContextId?: string\n}\n\nexport type ColumnChooserPanelProps = ColumnChooserSectionProps & {\n open: boolean\n onOpenChange: (open: boolean) => void\n}\n\nfunction SortableColumnItem({\n column,\n onToggle,\n}: {\n column: ColumnChooserField\n onToggle: (key: string) => void\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: column.key })\n\n const style: React.CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n }\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n className=\"flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50\"\n >\n <span className=\"cursor-grab text-muted-foreground\" {...attributes} {...listeners}>\n <GripVertical className=\"size-4\" />\n </span>\n <span className=\"truncate flex-1 min-w-0\">{column.label}</span>\n <Switch\n checked\n disabled={column.alwaysVisible}\n onCheckedChange={() => onToggle(column.key)}\n className=\"shrink-0 scale-90\"\n />\n </div>\n )\n}\n\nexport function ColumnChooserSection({\n availableColumns,\n visibleColumnKeys,\n columnOrder,\n onToggleColumn,\n onReorderColumns,\n dndContextId = 'column-chooser',\n}: ColumnChooserSectionProps) {\n const t = useT()\n const [searchQuery, setSearchQuery] = React.useState('')\n const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set())\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n )\n\n const visibleSet = React.useMemo(() => new Set(visibleColumnKeys), [visibleColumnKeys])\n\n const selectedColumns = React.useMemo(() => {\n const ordered: ColumnChooserField[] = []\n for (const key of columnOrder) {\n const col = availableColumns.find((c) => c.key === key)\n if (col && visibleSet.has(key)) ordered.push(col)\n }\n for (const col of availableColumns) {\n if (visibleSet.has(col.key) && !ordered.some((o) => o.key === col.key)) {\n ordered.push(col)\n }\n }\n if (!searchQuery) return ordered\n const lowerQuery = searchQuery.toLowerCase()\n return ordered.filter((c) => c.label.toLowerCase().includes(lowerQuery))\n }, [availableColumns, visibleSet, columnOrder, searchQuery])\n\n const groupedAvailable = React.useMemo(() => {\n const lowerQuery = searchQuery.toLowerCase()\n const filtered = availableColumns\n .filter((c) => !visibleSet.has(c.key))\n .filter((c) => !searchQuery || c.label.toLowerCase().includes(lowerQuery))\n\n const groups = new Map<string, ColumnChooserField[]>()\n for (const col of filtered) {\n const group = col.group || t('ui.columnChooser.ungrouped', 'Other')\n const list = groups.get(group) ?? []\n list.push(col)\n groups.set(group, list)\n }\n return groups\n }, [availableColumns, searchQuery, visibleSet, t])\n\n const toggleGroup = React.useCallback((group: string) => {\n setExpandedGroups((prev) => {\n const next = new Set(prev)\n if (next.has(group)) next.delete(group)\n else next.add(group)\n return next\n })\n }, [])\n\n const handleDragEnd = React.useCallback((event: DragEndEvent) => {\n const { active, over } = event\n if (!over || active.id === over.id) return\n const oldIndex = selectedColumns.findIndex((c) => c.key === active.id)\n const newIndex = selectedColumns.findIndex((c) => c.key === over.id)\n if (oldIndex === -1 || newIndex === -1) return\n const reordered = [...selectedColumns]\n const [moved] = reordered.splice(oldIndex, 1)\n reordered.splice(newIndex, 0, moved)\n onReorderColumns(reordered.map((c) => c.key))\n }, [selectedColumns, onReorderColumns])\n\n const handleHideAll = React.useCallback(() => {\n for (const col of selectedColumns) {\n if (!col.alwaysVisible) onToggleColumn(col.key)\n }\n }, [selectedColumns, onToggleColumn])\n\n return (\n <div className=\"flex flex-col\">\n <div className=\"px-4 py-3 border-t\">\n <SearchInput\n size=\"sm\"\n placeholder={t('ui.columnChooser.search', 'Search columns...')}\n value={searchQuery}\n onChange={setSearchQuery}\n />\n </div>\n\n <div>\n {selectedColumns.length > 0 ? (\n <div className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-baseline gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('ui.columnChooser.shown', 'Shown')}\n </div>\n <span className=\"text-xs text-muted-foreground\">\n {selectedColumns.length}/{availableColumns.length}\n </span>\n </div>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto px-0 text-sm text-muted-foreground hover:text-foreground\"\n onClick={handleHideAll}\n >\n {t('ui.columnChooser.hideAll', 'Hide all')}\n </Button>\n </div>\n <DndContext id={dndContextId} sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <SortableContext items={selectedColumns.map((c) => c.key)} strategy={verticalListSortingStrategy}>\n <div className=\"space-y-2\">\n {selectedColumns.map((col) => (\n <SortableColumnItem key={col.key} column={col} onToggle={onToggleColumn} />\n ))}\n </div>\n </SortableContext>\n </DndContext>\n </div>\n ) : null}\n\n <div className=\"border-t px-4 py-3 mt-4 mb-4\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground mb-3\">\n {t('ui.columnChooser.available', 'Available columns')}\n </div>\n {Array.from(groupedAvailable.entries()).map(([group, columns]) => {\n const isCollapsed = !searchQuery && !expandedGroups.has(group)\n return (\n <div key={group} className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start gap-2 h-auto px-1 py-2 text-xs font-medium uppercase text-muted-foreground\"\n onClick={() => toggleGroup(group)}\n >\n <ChevronRight className={`size-4 transition-transform ${isCollapsed ? '' : 'rotate-90'}`} />\n <span>{group}</span>\n </Button>\n {!isCollapsed ? (\n <div className=\"space-y-2 mt-2\">\n {columns.map((col) => (\n <div\n key={col.key}\n className=\"flex items-center gap-2 rounded pl-7 pr-2 py-1.5 text-sm hover:bg-muted/50 cursor-pointer\"\n onClick={() => { if (!col.alwaysVisible) onToggleColumn(col.key) }}\n >\n <span className=\"truncate flex-1 min-w-0\">{col.label}</span>\n <Switch\n checked={false}\n disabled={col.alwaysVisible}\n onCheckedChange={() => onToggleColumn(col.key)}\n onClick={(e) => e.stopPropagation()}\n className=\"shrink-0 scale-90\"\n />\n </div>\n ))}\n </div>\n ) : null}\n </div>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n\nexport function ColumnChooserPanel({\n open,\n onOpenChange,\n ...sectionProps\n}: ColumnChooserPanelProps) {\n const t = useT()\n React.useEffect(() => {\n if (!open) return\n if (typeof document === 'undefined') return\n document.body.dataset.columnChooserOpen = 'true'\n return () => {\n delete document.body.dataset.columnChooserOpen\n }\n }, [open])\n if (!open) return null\n return (\n <div className=\"fixed inset-y-0 right-0 z-modal w-80 border-l bg-background shadow-lg flex flex-col\">\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"font-semibold text-sm\">\n {t('ui.columnChooser.title', 'Columns')}\n </h3>\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => onOpenChange(false)} aria-label={t('ui.columnChooser.close', 'Close')}>\n <X className=\"size-4\" />\n </IconButton>\n </div>\n <div className=\"flex-1 overflow-auto\">\n <ColumnChooserSection {...sectionProps} />\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { Button } from "../../primitives/button.js";
|
|
5
6
|
function normalizeOptions(input) {
|
|
6
7
|
if (!Array.isArray(input)) return [];
|
|
@@ -32,6 +33,9 @@ function ComboboxInput({
|
|
|
32
33
|
disabled = false,
|
|
33
34
|
allowCustomValues = true
|
|
34
35
|
}) {
|
|
36
|
+
const t = useT();
|
|
37
|
+
const resolvedPlaceholder = placeholder ?? t("ui.inputs.comboboxInput.placeholder", "Type to search...");
|
|
38
|
+
const loadingLabel = t("ui.inputs.comboboxInput.loading", "Loading suggestions\u2026");
|
|
35
39
|
const [input, setInput] = React.useState("");
|
|
36
40
|
const [asyncOptions, setAsyncOptions] = React.useState([]);
|
|
37
41
|
const [resolvedOptions, setResolvedOptions] = React.useState([]);
|
|
@@ -230,9 +234,9 @@ function ComboboxInput({
|
|
|
230
234
|
{
|
|
231
235
|
ref: inputRef,
|
|
232
236
|
type: "text",
|
|
233
|
-
className: "w-full h-9 rounded border px-
|
|
237
|
+
className: "w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed",
|
|
234
238
|
value: input,
|
|
235
|
-
placeholder:
|
|
239
|
+
placeholder: resolvedPlaceholder,
|
|
236
240
|
autoFocus,
|
|
237
241
|
"data-crud-focus-target": "",
|
|
238
242
|
disabled,
|
|
@@ -259,26 +263,26 @@ function ComboboxInput({
|
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
),
|
|
262
|
-
showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && /* @__PURE__ */ jsx("div", { className: "absolute z-popover w-full mt-1 rounded border bg-popover shadow-
|
|
266
|
+
showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && /* @__PURE__ */ jsx("div", { className: "absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto", children: loading && touched ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: loadingLabel }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: filteredSuggestions.map((option, index) => /* @__PURE__ */ jsxs(
|
|
263
267
|
Button,
|
|
264
268
|
{
|
|
265
269
|
type: "button",
|
|
266
270
|
variant: "ghost",
|
|
267
271
|
size: "sm",
|
|
268
272
|
className: [
|
|
269
|
-
"w-full h-auto justify-start font-normal text-left flex flex-col items-start
|
|
270
|
-
index === selectedIndex ? "bg-
|
|
273
|
+
"w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2",
|
|
274
|
+
index === selectedIndex ? "bg-muted" : ""
|
|
271
275
|
].filter(Boolean).join(" "),
|
|
272
276
|
onMouseDown: (event) => event.preventDefault(),
|
|
273
277
|
onClick: () => selectValue(option.value),
|
|
274
278
|
onMouseEnter: () => setSelectedIndex(index),
|
|
275
279
|
children: [
|
|
276
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: option.label }),
|
|
280
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: option.label }),
|
|
277
281
|
option.description ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: option.description }) : null
|
|
278
282
|
]
|
|
279
283
|
},
|
|
280
284
|
option.value
|
|
281
|
-
)) })
|
|
285
|
+
)) }) })
|
|
282
286
|
] });
|
|
283
287
|
}
|
|
284
288
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/ComboboxInput.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '../../primitives/button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n}: ComboboxInputProps) {\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Options whose label is genuinely known (not a self-mapping `value === label`\n // placeholder). Used to decide whether eager resolution still needs to run.\n const knownLabelValues = React.useMemo(() => {\n const set = new Set<string>()\n for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n if (option.label && option.label !== option.value) set.add(option.value)\n }\n return set\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n const sync = typeof resolveLabel === 'function' ? resolveLabel(value) : undefined\n map.set(value, {\n value,\n label: typeof sync === 'string' && sync ? sync : value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, resolveLabel, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(async () => {\n setLoading(true)\n try {\n const items = await loadSuggestions(query)\n if (!cancelled) {\n setAsyncOptions(normalizeOptions(items))\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n if (knownLabelValues.has(value)) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions(normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, disabled, knownLabelValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className=\"w-full h-9 rounded border px-2 text-sm disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={input}\n placeholder={placeholder || 'Type to search...'}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay to allow click on suggestions\n setTimeout(() => {\n if (disabled) return\n confirmSelection(input)\n }, 200)\n }}\n />\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded border bg-popover shadow-lg max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-3 py-2 text-xs text-muted-foreground\">Loading suggestions\u2026</div>\n ) : (\n filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start px-3 py-2',\n index === selectedIndex ? 'bg-accent' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => selectValue(option.value)}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../../primitives/button'\n\nexport type ComboboxOption = {\n value: string\n label: string\n description?: string | null\n}\n\nexport type ComboboxInputProps = {\n value: string\n onChange: (next: string) => void\n placeholder?: string\n suggestions?: Array<string | ComboboxOption>\n // Options to hydrate the option map up front (typically the linked entity's\n // display fields, already present in a record-detail payload). Merged with\n // `suggestions` so a pre-selected value renders its label without interaction.\n seedOptions?: ComboboxOption[]\n loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>\n // Eagerly resolve a pre-selected `value` to a human label when it is not\n // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once\n // per value, before any user interaction. May be sync or async.\n resolveLabel?: (value: string) => string | Promise<string>\n resolveDescription?: (value: string) => string | null | undefined\n autoFocus?: boolean\n disabled?: boolean\n allowCustomValues?: boolean\n}\n\nfunction normalizeOptions(input?: Array<string | ComboboxOption>): ComboboxOption[] {\n if (!Array.isArray(input)) return []\n return input\n .map((option) => {\n if (typeof option === 'string') {\n const trimmed = option.trim()\n if (!trimmed) return null\n return { value: trimmed, label: trimmed }\n }\n const value = typeof option.value === 'string' ? option.value.trim() : ''\n if (!value) return null\n return {\n value,\n label: option.label?.trim() || value,\n description: option.description ?? null,\n }\n })\n .filter((option): option is ComboboxOption => !!option)\n}\n\nexport function ComboboxInput({\n value,\n onChange,\n placeholder,\n suggestions,\n seedOptions,\n loadSuggestions,\n resolveLabel,\n resolveDescription,\n autoFocus,\n disabled = false,\n allowCustomValues = true,\n}: ComboboxInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')\n const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions\u2026')\n const [input, setInput] = React.useState('')\n const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])\n const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])\n const [loading, setLoading] = React.useState(false)\n const [touched, setTouched] = React.useState(false)\n const [showSuggestions, setShowSuggestions] = React.useState(false)\n const [selectedIndex, setSelectedIndex] = React.useState(-1)\n const inputRef = React.useRef<HTMLInputElement>(null)\n const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))\n\n const staticOptions = React.useMemo(\n () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),\n [seedOptions, suggestions]\n )\n\n // Options whose label is genuinely known (not a self-mapping `value === label`\n // placeholder). Used to decide whether eager resolution still needs to run.\n const knownLabelValues = React.useMemo(() => {\n const set = new Set<string>()\n for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {\n if (option.label && option.label !== option.value) set.add(option.value)\n }\n return set\n }, [staticOptions, asyncOptions, resolvedOptions])\n\n const optionMap = React.useMemo(() => {\n const map = new Map<string, ComboboxOption>()\n const register = (option: ComboboxOption) => {\n const existing = map.get(option.value)\n // Prefer an entry that carries a real label over a self-mapping placeholder.\n if (!existing || (existing.label === existing.value && option.label !== option.value)) {\n map.set(option.value, option)\n }\n }\n staticOptions.forEach(register)\n asyncOptions.forEach(register)\n resolvedOptions.forEach(register)\n if (value) {\n const existing = map.get(value)\n if (!existing) {\n const sync = typeof resolveLabel === 'function' ? resolveLabel(value) : undefined\n map.set(value, {\n value,\n label: typeof sync === 'string' && sync ? sync : value,\n description: resolveDescription?.(value) ?? null,\n })\n }\n }\n return map\n }, [asyncOptions, resolvedOptions, resolveDescription, resolveLabel, staticOptions, value])\n\n const availableOptions = React.useMemo(() => {\n return Array.from(optionMap.values())\n }, [optionMap])\n\n const filteredSuggestions = React.useMemo(() => {\n const query = input.toLowerCase().trim()\n if (!query) return availableOptions\n return availableOptions.filter((option) => {\n const labelMatch = option.label.toLowerCase().includes(query)\n const descMatch = option.description?.toLowerCase().includes(query)\n return labelMatch || Boolean(descMatch)\n })\n }, [availableOptions, input])\n\n React.useEffect(() => {\n if (!loadSuggestions || !touched || disabled) return\n const query = input.trim()\n let cancelled = false\n const handle = window.setTimeout(async () => {\n setLoading(true)\n try {\n const items = await loadSuggestions(query)\n if (!cancelled) {\n setAsyncOptions(normalizeOptions(items))\n }\n } finally {\n if (!cancelled) setLoading(false)\n }\n }, 200)\n return () => {\n cancelled = true\n window.clearTimeout(handle)\n }\n }, [disabled, input, loadSuggestions, touched])\n\n // Eagerly resolve a pre-selected value to its label without requiring the user\n // to focus the field. Runs once per value when it is not already covered.\n const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined\n React.useEffect(() => {\n if (!value || disabled) return\n if (knownLabelValues.has(value)) return\n let cancelled = false\n const apply = (label?: string | null, description?: string | null) => {\n const clean = typeof label === 'string' ? label.trim() : ''\n if (cancelled || !clean || clean === value) return\n setResolvedOptions((prev) => {\n if (prev.some((option) => option.value === value && option.label === clean)) return prev\n return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]\n })\n }\n if (eagerResolveLabel) {\n Promise.resolve()\n .then(() => eagerResolveLabel(value))\n .then((label) => apply(label, resolveDescription?.(value)))\n .catch(() => {})\n return () => { cancelled = true }\n }\n // Fallback: pull the first page of async suggestions so a remount that lost\n // its option cache can still recover the label without user interaction.\n if (loadSuggestions) {\n setLoading(true)\n Promise.resolve()\n .then(() => loadSuggestions())\n .then((items) => {\n if (cancelled) return\n const normalized = normalizeOptions(items)\n setAsyncOptions(normalized)\n })\n .catch(() => {})\n .finally(() => { if (!cancelled) setLoading(false) })\n }\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value, disabled, knownLabelValues, eagerResolveLabel, loadSuggestions])\n\n // Sync input with value when value changes externally and input is not focused.\n React.useEffect(() => {\n if (document.activeElement !== inputRef.current) {\n const option = optionMap.get(value)\n setInput(option?.label ?? value ?? '')\n }\n }, [value, optionMap])\n\n const selectValue = React.useCallback(\n (nextValue: string) => {\n if (disabled) return\n const trimmed = nextValue.trim()\n onChange(trimmed)\n const option = optionMap.get(trimmed)\n setInput(option?.label ?? trimmed)\n setShowSuggestions(false)\n setSelectedIndex(-1)\n },\n [disabled, onChange, optionMap]\n )\n\n const findOptionForInput = React.useCallback(\n (raw: string): ComboboxOption | null => {\n const query = raw.trim().toLowerCase()\n if (!query) return null\n for (const option of optionMap.values()) {\n if (option.value === raw.trim()) return option\n if (option.label.toLowerCase() === query) return option\n }\n return null\n },\n [optionMap]\n )\n\n const confirmSelection = React.useCallback(\n (raw: string) => {\n if (disabled) return\n const option = findOptionForInput(raw)\n if (option) {\n selectValue(option.value)\n return\n }\n if (!allowCustomValues) {\n // Revert to the current value's label \u2014 but only if we actually know it.\n // Baking the raw value back in while eager resolution is still pending\n // would freeze a placeholder (e.g. a UUID) into the visible input.\n setShowSuggestions(false)\n const currentOption = optionMap.get(value)\n if (currentOption && currentOption.label !== currentOption.value) {\n setInput(currentOption.label)\n } else if (!value) {\n setInput('')\n }\n return\n }\n selectValue(raw)\n },\n [allowCustomValues, disabled, findOptionForInput, optionMap, selectValue, value]\n )\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLInputElement>) => {\n if (disabled) return\n\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showSuggestions) {\n setShowSuggestions(true)\n setSelectedIndex(0)\n } else {\n setSelectedIndex((prev) => Math.min(prev + 1, filteredSuggestions.length - 1))\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n setSelectedIndex((prev) => Math.max(prev - 1, -1))\n } else if (event.key === 'Enter') {\n event.preventDefault()\n if (selectedIndex >= 0 && filteredSuggestions[selectedIndex]) {\n selectValue(filteredSuggestions[selectedIndex].value)\n } else {\n confirmSelection(input)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n setShowSuggestions(false)\n setSelectedIndex(-1)\n }\n },\n [confirmSelection, disabled, filteredSuggestions, input, selectValue, selectedIndex, showSuggestions]\n )\n\n return (\n <div className=\"relative w-full\">\n {/* Use raw <input> here instead of the DS Input primitive: ComboboxInput's\n focus / suggestions-popup interplay relies on the trigger being a plain\n input element. The DS wrapper introduces a <div> that desyncs autocomplete\n on this specific surface. Keeps the rest of the form on Input primitive. */}\n <input\n ref={inputRef}\n type=\"text\"\n className=\"w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed\"\n value={input}\n placeholder={resolvedPlaceholder}\n autoFocus={autoFocus}\n data-crud-focus-target=\"\"\n disabled={disabled}\n onFocus={() => {\n setTouched(true)\n if (suppressOpenOnFocusRef.current) {\n suppressOpenOnFocusRef.current = false\n return\n }\n setShowSuggestions(true)\n }}\n onChange={(event) => {\n setTouched(true)\n setInput(event.target.value)\n setShowSuggestions(true)\n setSelectedIndex(-1)\n }}\n onKeyDown={handleKeyDown}\n onBlur={() => {\n // Delay to allow click on suggestions\n setTimeout(() => {\n if (disabled) return\n confirmSelection(input)\n }, 200)\n }}\n />\n\n {showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (\n <div className=\"absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto\">\n {loading && touched ? (\n <div className=\"px-2 py-1.5 text-xs text-muted-foreground\">{loadingLabel}</div>\n ) : (\n <div className=\"flex flex-col gap-1\">\n {filteredSuggestions.map((option, index) => (\n <Button\n key={option.value}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={[\n 'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',\n index === selectedIndex ? 'bg-muted' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => selectValue(option.value)}\n onMouseEnter={() => setSelectedIndex(index)}\n >\n <span className=\"font-medium text-foreground\">{option.label}</span>\n {option.description ? (\n <span className=\"text-xs text-muted-foreground\">{option.description}</span>\n ) : null}\n </Button>\n ))}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
5
|
import { ComboboxInput } from "./ComboboxInput.js";
|
|
5
6
|
import { useAvailableEvents } from "./EventSelect.js";
|
|
6
7
|
function EventPatternInput({
|
|
7
8
|
value,
|
|
8
9
|
onChange,
|
|
9
|
-
placeholder
|
|
10
|
+
placeholder,
|
|
10
11
|
disabled,
|
|
11
12
|
categories,
|
|
12
13
|
modules
|
|
13
14
|
}) {
|
|
15
|
+
const t = useT();
|
|
16
|
+
const resolvedPlaceholder = placeholder ?? t("ui.inputs.eventPatternInput.placeholder", "sales.orders.created");
|
|
14
17
|
const { events } = useAvailableEvents({ categories, modules });
|
|
15
18
|
const suggestions = React.useMemo(
|
|
16
19
|
() => events.map((event) => ({
|
|
@@ -25,7 +28,7 @@ function EventPatternInput({
|
|
|
25
28
|
{
|
|
26
29
|
value,
|
|
27
30
|
onChange,
|
|
28
|
-
placeholder,
|
|
31
|
+
placeholder: resolvedPlaceholder,
|
|
29
32
|
suggestions,
|
|
30
33
|
allowCustomValues: true,
|
|
31
34
|
disabled
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/EventPatternInput.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { ComboboxInput, type ComboboxOption } from './ComboboxInput'\nimport { useAvailableEvents } from './EventSelect'\n\nexport interface EventPatternInputProps {\n value: string\n onChange: (pattern: string) => void\n placeholder?: string\n disabled?: boolean\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n modules?: string[]\n}\n\nexport function EventPatternInput({\n value,\n onChange,\n placeholder
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { ComboboxInput, type ComboboxOption } from './ComboboxInput'\nimport { useAvailableEvents } from './EventSelect'\n\nexport interface EventPatternInputProps {\n value: string\n onChange: (pattern: string) => void\n /**\n * Placeholder hint shown when the field is empty. Defaults to a translated\n * `ui.inputs.eventPatternInput.placeholder` key (English fallback\n * `'sales.orders.created'`) \u2014 an example of a valid event id with wildcards.\n * Pass a custom string to override per-surface.\n */\n placeholder?: string\n disabled?: boolean\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n modules?: string[]\n}\n\nexport function EventPatternInput({\n value,\n onChange,\n placeholder,\n disabled,\n categories,\n modules,\n}: EventPatternInputProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventPatternInput.placeholder', 'sales.orders.created')\n const { events } = useAvailableEvents({ categories, modules })\n\n const suggestions = React.useMemo<ComboboxOption[]>(\n () =>\n events.map(event => ({\n value: event.id,\n label: event.label,\n description: event.id,\n })),\n [events]\n )\n\n return (\n <ComboboxInput\n value={value}\n onChange={onChange}\n placeholder={resolvedPlaceholder}\n suggestions={suggestions}\n allowCustomValues={true}\n disabled={disabled}\n />\n )\n}\n"],
|
|
5
|
+
"mappings": ";AA6CI;AA3CJ,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,qBAA0C;AACnD,SAAS,0BAA0B;AAiB5B,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,QAAM,IAAI,KAAK;AACf,QAAM,sBAAsB,eAAe,EAAE,2CAA2C,sBAAsB;AAC9G,QAAM,EAAE,OAAO,IAAI,mBAAmB,EAAE,YAAY,QAAQ,CAAC;AAE7D,QAAM,cAAc,MAAM;AAAA,IACxB,MACE,OAAO,IAAI,YAAU;AAAA,MACnB,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,aAAa,MAAM;AAAA,IACrB,EAAE;AAAA,IACJ,CAAC,MAAM;AAAA,EACT;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb;AAAA,MACA,mBAAmB;AAAA,MACnB;AAAA;AAAA,EACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
6
|
import { apiCall } from "../utils/apiCall.js";
|
|
6
7
|
import {
|
|
7
8
|
Select,
|
|
@@ -15,13 +16,18 @@ import {
|
|
|
15
16
|
function EventSelect({
|
|
16
17
|
value,
|
|
17
18
|
onChange,
|
|
18
|
-
placeholder
|
|
19
|
+
placeholder,
|
|
19
20
|
className,
|
|
20
21
|
disabled,
|
|
21
22
|
categories,
|
|
22
23
|
modules,
|
|
23
|
-
excludeTriggerExcluded = true
|
|
24
|
+
excludeTriggerExcluded = true,
|
|
25
|
+
size = "default"
|
|
24
26
|
}) {
|
|
27
|
+
const t = useT();
|
|
28
|
+
const resolvedPlaceholder = placeholder ?? t("ui.inputs.eventSelect.placeholder", "Select an event...");
|
|
29
|
+
const loadingPlaceholder = t("ui.inputs.eventSelect.loading", "Loading...");
|
|
30
|
+
const emptyPlaceholder = t("ui.inputs.eventSelect.empty", "No events available");
|
|
25
31
|
const { data: allEvents = [], isLoading } = useQuery({
|
|
26
32
|
queryKey: ["declared-events", excludeTriggerExcluded],
|
|
27
33
|
queryFn: async () => {
|
|
@@ -66,10 +72,10 @@ function EventSelect({
|
|
|
66
72
|
onValueChange: (next) => onChange(next ?? ""),
|
|
67
73
|
disabled: disabled || isLoading,
|
|
68
74
|
children: [
|
|
69
|
-
/* @__PURE__ */ jsx(SelectTrigger, { size
|
|
75
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size, className, children: /* @__PURE__ */ jsx(
|
|
70
76
|
SelectValue,
|
|
71
77
|
{
|
|
72
|
-
placeholder: isLoading ?
|
|
78
|
+
placeholder: isLoading ? loadingPlaceholder : isEmpty ? emptyPlaceholder : resolvedPlaceholder
|
|
73
79
|
}
|
|
74
80
|
) }),
|
|
75
81
|
/* @__PURE__ */ jsx(SelectContent, { children: Object.entries(eventsByModule).map(([module, moduleEvents]) => /* @__PURE__ */ jsxs(SelectGroup, { children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/inputs/EventSelect.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useMemo } from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { apiCall } from '../utils/apiCall'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\n/**\n * Event definition returned by the API\n */\nexport interface EventDefinition {\n id: string\n label: string\n description?: string\n category?: 'crud' | 'lifecycle' | 'system' | 'custom'\n module?: string\n entity?: string\n excludeFromTriggers?: boolean\n}\n\nexport interface EventSelectProps {\n /** Current selected event ID */\n value: string\n /** Called when event is selected */\n onChange: (eventId: string) => void\n /** Placeholder text when no event selected */\n placeholder?: string\n /** Additional CSS classes */\n className?: string\n /** Whether the select is disabled */\n disabled?: boolean\n /** Filter events by category */\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n /** Filter events by module */\n modules?: string[]\n /** Whether to exclude events marked as excludeFromTriggers (default: true) */\n excludeTriggerExcluded?: boolean\n}\n\n/**\n * EventSelect - A reusable select component for choosing declared events\n *\n * Fetches available events from the API and groups them by module.\n */\nexport function EventSelect({\n value,\n onChange,\n placeholder
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useMemo } from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '../utils/apiCall'\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectTrigger,\n SelectValue,\n} from '../../primitives/select'\n\n/**\n * Event definition returned by the API\n */\nexport interface EventDefinition {\n id: string\n label: string\n description?: string\n category?: 'crud' | 'lifecycle' | 'system' | 'custom'\n module?: string\n entity?: string\n excludeFromTriggers?: boolean\n}\n\nexport interface EventSelectProps {\n /** Current selected event ID */\n value: string\n /** Called when event is selected */\n onChange: (eventId: string) => void\n /** Placeholder text when no event selected. Defaults to a translated string. */\n placeholder?: string\n /** Additional CSS classes */\n className?: string\n /** Whether the select is disabled */\n disabled?: boolean\n /** Filter events by category */\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n /** Filter events by module */\n modules?: string[]\n /** Whether to exclude events marked as excludeFromTriggers (default: true) */\n excludeTriggerExcluded?: boolean\n /** Trigger size \u2014 defaults to `'default'` (DS row-height contract). */\n size?: 'sm' | 'default' | 'lg'\n}\n\n/**\n * EventSelect - A reusable select component for choosing declared events\n *\n * Fetches available events from the API and groups them by module.\n */\nexport function EventSelect({\n value,\n onChange,\n placeholder,\n className,\n disabled,\n categories,\n modules,\n excludeTriggerExcluded = true,\n size = 'default',\n}: EventSelectProps) {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventSelect.placeholder', 'Select an event...')\n const loadingPlaceholder = t('ui.inputs.eventSelect.loading', 'Loading...')\n const emptyPlaceholder = t('ui.inputs.eventSelect.empty', 'No events available')\n\n // Fetch events from the API\n const { data: allEvents = [], isLoading } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000, // Cache for 5 minutes\n })\n\n // Filter events based on props\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (categories?.length) {\n events = events.filter(e => e.category && categories.includes(e.category))\n }\n if (modules?.length) {\n events = events.filter(e => e.module && modules.includes(e.module))\n }\n\n return events\n }, [allEvents, categories, modules])\n\n // Group events by module for better UX\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n // Sort modules alphabetically\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n // Format module name for display\n const formatModuleName = (module: string): string => {\n return module.charAt(0).toUpperCase() + module.slice(1).replace(/_/g, ' ')\n }\n\n const isEmpty = !isLoading && filteredEvents.length === 0\n\n return (\n <Select\n value={value || undefined}\n onValueChange={(next) => onChange(next ?? '')}\n disabled={disabled || isLoading}\n >\n <SelectTrigger size={size} className={className}>\n <SelectValue\n placeholder={isLoading ? loadingPlaceholder : isEmpty ? emptyPlaceholder : resolvedPlaceholder}\n />\n </SelectTrigger>\n <SelectContent>\n {Object.entries(eventsByModule).map(([module, moduleEvents]) => (\n <SelectGroup key={module}>\n <SelectLabel>{formatModuleName(module)}</SelectLabel>\n {moduleEvents.map(event => (\n <SelectItem key={event.id} value={event.id}>\n {event.label}\n </SelectItem>\n ))}\n </SelectGroup>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n/**\n * Hook for getting available events\n */\nexport function useAvailableEvents(options?: {\n categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>\n modules?: string[]\n excludeTriggerExcluded?: boolean\n}) {\n const excludeTriggerExcluded = options?.excludeTriggerExcluded !== false\n\n const { data: allEvents = [], isLoading, error, refetch } = useQuery({\n queryKey: ['declared-events', excludeTriggerExcluded],\n queryFn: async () => {\n const result = await apiCall<{ data: EventDefinition[]; total: number }>(\n `/api/events?excludeTriggerExcluded=${excludeTriggerExcluded}`\n )\n if (!result.ok) return []\n return result.result?.data || []\n },\n staleTime: 5 * 60 * 1000,\n })\n\n const filteredEvents = useMemo(() => {\n let events = allEvents\n\n if (options?.categories?.length) {\n events = events.filter(e => e.category && options.categories!.includes(e.category))\n }\n if (options?.modules?.length) {\n events = events.filter(e => e.module && options.modules!.includes(e.module))\n }\n\n return events\n }, [allEvents, options])\n\n // Group by module\n const eventsByModule = useMemo(() => {\n const grouped: Record<string, EventDefinition[]> = {}\n for (const event of filteredEvents) {\n const module = event.module || 'other'\n if (!grouped[module]) grouped[module] = []\n grouped[module].push(event)\n }\n return Object.fromEntries(\n Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))\n )\n }, [filteredEvents])\n\n return {\n events: filteredEvents,\n eventsByModule,\n isLoading,\n error,\n refetch,\n }\n}\n\nexport default EventSelect\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|