@wheelhouse/ui 0.2.2 → 0.2.3
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/dist/blocks/columns/columns-types.d.ts +40 -0
- package/dist/blocks/columns/columns-types.d.ts.map +1 -0
- package/dist/blocks/columns/columns-types.js +10 -0
- package/dist/blocks/columns/columns-utils.d.ts +13 -0
- package/dist/blocks/columns/columns-utils.d.ts.map +1 -0
- package/dist/blocks/columns/columns-utils.js +85 -0
- package/dist/blocks/columns/columns.d.ts +3 -0
- package/dist/blocks/columns/columns.d.ts.map +1 -0
- package/dist/blocks/columns/columns.js +79 -0
- package/dist/blocks/columns/columns.stories.d.ts +12 -0
- package/dist/blocks/columns/columns.stories.d.ts.map +1 -0
- package/dist/blocks/columns/columns.stories.js +67 -0
- package/dist/blocks/columns/index.d.ts +6 -0
- package/dist/blocks/columns/index.d.ts.map +1 -0
- package/dist/blocks/columns/index.js +3 -0
- package/dist/blocks/date-selector/date-selector-context.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector-default-i18n.d.ts +10 -0
- package/dist/blocks/date-selector/date-selector-default-i18n.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector-default-i18n.js +29 -0
- package/dist/blocks/date-selector/date-selector-i18n-resources.d.ts +11 -0
- package/dist/blocks/date-selector/date-selector-i18n-resources.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector-i18n-resources.js +248 -0
- package/dist/blocks/date-selector/date-selector-i18n.shared.d.ts +12 -0
- package/dist/blocks/date-selector/date-selector-i18n.shared.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector-i18n.shared.js +84 -0
- package/dist/{components → blocks}/date-selector/date-selector-parts.d.ts +9 -0
- package/dist/blocks/date-selector/date-selector-parts.d.ts.map +1 -0
- package/dist/{components → blocks}/date-selector/date-selector-parts.js +21 -6
- package/dist/{components → blocks}/date-selector/date-selector-types.d.ts +70 -0
- package/dist/blocks/date-selector/date-selector-types.d.ts.map +1 -0
- package/dist/{components → blocks}/date-selector/date-selector-types.js +22 -0
- package/dist/blocks/date-selector/date-selector-value.d.ts +81 -0
- package/dist/blocks/date-selector/date-selector-value.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector-value.js +423 -0
- package/dist/{components → blocks}/date-selector/date-selector.d.ts +1 -1
- package/dist/blocks/date-selector/date-selector.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector.js +191 -0
- package/dist/{components → blocks}/date-selector/date-selector.stories.d.ts +14 -0
- package/dist/blocks/date-selector/date-selector.stories.d.ts.map +1 -0
- package/dist/blocks/date-selector/date-selector.stories.js +299 -0
- package/dist/blocks/date-selector/index.d.ts +11 -0
- package/dist/blocks/date-selector/index.d.ts.map +1 -0
- package/dist/blocks/date-selector/index.js +8 -0
- package/dist/{components → blocks}/date-selector/use-date-selector.d.ts +4 -3
- package/dist/blocks/date-selector/use-date-selector.d.ts.map +1 -0
- package/dist/{components → blocks}/date-selector/use-date-selector.js +14 -8
- package/dist/blocks/index.d.ts +4 -0
- package/dist/blocks/index.d.ts.map +1 -0
- package/dist/blocks/index.js +3 -0
- package/dist/blocks/navigation/index.d.ts +5 -0
- package/dist/blocks/navigation/index.d.ts.map +1 -0
- package/dist/blocks/navigation/index.js +2 -0
- package/dist/blocks/navigation/navigation-types.d.ts +60 -0
- package/dist/blocks/navigation/navigation-types.d.ts.map +1 -0
- package/dist/blocks/navigation/navigation-types.js +1 -0
- package/dist/blocks/navigation/navigation.d.ts +9 -0
- package/dist/blocks/navigation/navigation.d.ts.map +1 -0
- package/dist/blocks/navigation/navigation.demo.d.ts +4 -0
- package/dist/blocks/navigation/navigation.demo.d.ts.map +1 -0
- package/dist/blocks/navigation/navigation.demo.js +46 -0
- package/dist/blocks/navigation/navigation.js +98 -0
- package/dist/blocks/navigation/navigation.stories.d.ts +14 -0
- package/dist/blocks/navigation/navigation.stories.d.ts.map +1 -0
- package/dist/blocks/navigation/navigation.stories.js +16 -0
- package/dist/components/accordion/accordion.stories.js +1 -1
- package/dist/components/alert/alert.stories.js +1 -1
- package/dist/components/alert-dialog/alert-dialog.stories.js +1 -1
- package/dist/components/aspect-ratio/aspect-ratio.stories.js +1 -1
- package/dist/components/avatar/avatar.stories.js +1 -1
- package/dist/components/badge/badge.stories.js +1 -1
- package/dist/components/breadcrumb/breadcrumb.stories.js +1 -1
- package/dist/components/button/button.stories.js +1 -1
- package/dist/components/button-group/button-group.d.ts +10 -4
- package/dist/components/button-group/button-group.d.ts.map +1 -1
- package/dist/components/button-group/button-group.js +15 -3
- package/dist/components/button-group/button-group.stories.js +1 -1
- package/dist/components/button-group/index.d.ts +2 -2
- package/dist/components/button-group/index.d.ts.map +1 -1
- package/dist/components/button-group/index.js +1 -1
- package/dist/components/calendar/calendar.stories.js +1 -1
- package/dist/components/card/card.stories.js +1 -1
- package/dist/components/checkbox/checkbox.stories.js +1 -1
- package/dist/components/collapsible/collapsible.stories.js +1 -1
- package/dist/components/combobox/combobox.stories.js +1 -1
- package/dist/components/command/command.stories.js +1 -1
- package/dist/components/context-menu/context-menu.stories.js +1 -1
- package/dist/components/dialog/dialog.stories.js +1 -1
- package/dist/components/direction/direction.stories.js +1 -1
- package/dist/components/drawer/drawer.stories.js +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts +9 -2
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +4 -1
- package/dist/components/dropdown-menu/dropdown-menu.stories.js +1 -1
- package/dist/components/dropdown-menu/index.d.ts +2 -2
- package/dist/components/dropdown-menu/index.d.ts.map +1 -1
- package/dist/components/dropdown-menu/index.js +1 -1
- package/dist/components/empty/empty.stories.js +1 -1
- package/dist/components/field/field.stories.js +1 -1
- package/dist/components/filters/filter-date-metric-value.d.ts +32 -0
- package/dist/components/filters/filter-date-metric-value.d.ts.map +1 -0
- package/dist/components/filters/filter-date-metric-value.js +331 -0
- package/dist/components/filters/filters-defaults.d.ts +4 -0
- package/dist/components/filters/filters-defaults.d.ts.map +1 -1
- package/dist/components/filters/filters-defaults.js +59 -1
- package/dist/components/filters/filters-i18n-resources.d.ts +277 -0
- package/dist/components/filters/filters-i18n-resources.d.ts.map +1 -0
- package/dist/components/filters/filters-i18n-resources.js +276 -0
- package/dist/components/filters/filters-i18n.shared.d.ts +16 -0
- package/dist/components/filters/filters-i18n.shared.d.ts.map +1 -0
- package/dist/components/filters/filters-i18n.shared.js +111 -0
- package/dist/components/filters/filters-types.d.ts +33 -1
- package/dist/components/filters/filters-types.d.ts.map +1 -1
- package/dist/components/filters/filters-utils.d.ts +28 -1
- package/dist/components/filters/filters-utils.d.ts.map +1 -1
- package/dist/components/filters/filters-utils.js +102 -0
- package/dist/components/filters/filters.d.ts +21 -3
- package/dist/components/filters/filters.d.ts.map +1 -1
- package/dist/components/filters/filters.js +493 -290
- package/dist/components/filters/filters.stories.d.ts +107 -2
- package/dist/components/filters/filters.stories.d.ts.map +1 -1
- package/dist/components/filters/filters.stories.js +364 -29
- package/dist/components/filters/index.d.ts +3 -1
- package/dist/components/filters/index.d.ts.map +1 -1
- package/dist/components/filters/index.js +3 -1
- package/dist/components/frame/frame.stories.js +1 -1
- package/dist/components/hover-card/hover-card.stories.js +1 -1
- package/dist/components/index.d.ts +0 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +0 -2
- package/dist/components/input/input.stories.js +1 -1
- package/dist/components/input-group/input-group.stories.js +1 -1
- package/dist/components/item/item.stories.js +1 -1
- package/dist/components/kbd/kbd.stories.js +1 -1
- package/dist/components/label/label.stories.js +1 -1
- package/dist/components/menubar/menubar.stories.js +1 -1
- package/dist/components/native-select/native-select.stories.js +1 -1
- package/dist/components/navigation-menu/navigation-menu.stories.js +1 -1
- package/dist/components/pagination/pagination.stories.js +1 -1
- package/dist/components/popover/popover.stories.js +1 -1
- package/dist/components/progress/progress.stories.js +1 -1
- package/dist/components/radio-group/radio-group.stories.js +1 -1
- package/dist/components/resizable/resizable.stories.js +1 -1
- package/dist/components/scroll-area/scroll-area.stories.js +1 -1
- package/dist/components/select/select.stories.js +1 -1
- package/dist/components/separator/separator.stories.js +1 -1
- package/dist/components/sheet/sheet.stories.js +1 -1
- package/dist/components/slider/slider.stories.js +1 -1
- package/dist/components/sonner/sonner.stories.js +1 -1
- package/dist/components/sortable/sortable.stories.js +1 -1
- package/dist/components/spinner/spinner.stories.js +1 -1
- package/dist/components/status-indicator/status-indicator.stories.js +1 -1
- package/dist/components/switch/switch.stories.js +1 -1
- package/dist/components/tabs/tabs.stories.js +1 -1
- package/dist/components/text/text.stories.js +1 -1
- package/dist/components/textarea/textarea.stories.js +1 -1
- package/dist/components/toggle/toggle.stories.js +1 -1
- package/dist/components/toggle-group/toggle-group.stories.js +1 -1
- package/dist/components/tooltip/tooltip.stories.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/llms.txt +10 -5
- package/package.json +8 -4
- package/dist/components/date-selector/date-selector-context.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector-parts.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector-types.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector-value.d.ts +0 -47
- package/dist/components/date-selector/date-selector-value.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector-value.js +0 -183
- package/dist/components/date-selector/date-selector.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector.js +0 -144
- package/dist/components/date-selector/date-selector.stories.d.ts.map +0 -1
- package/dist/components/date-selector/date-selector.stories.js +0 -144
- package/dist/components/date-selector/index.d.ts +0 -7
- package/dist/components/date-selector/index.d.ts.map +0 -1
- package/dist/components/date-selector/index.js +0 -5
- package/dist/components/date-selector/use-date-selector.d.ts.map +0 -1
- package/dist/components/navigation-pattern-1/index.d.ts +0 -3
- package/dist/components/navigation-pattern-1/index.d.ts.map +0 -1
- package/dist/components/navigation-pattern-1/index.js +0 -1
- package/dist/components/navigation-pattern-1/pattern-1.config.d.ts +0 -47
- package/dist/components/navigation-pattern-1/pattern-1.config.d.ts.map +0 -1
- package/dist/components/navigation-pattern-1/pattern-1.config.js +0 -55
- package/dist/components/navigation-pattern-1/pattern-1.d.ts +0 -7
- package/dist/components/navigation-pattern-1/pattern-1.d.ts.map +0 -1
- package/dist/components/navigation-pattern-1/pattern-1.js +0 -83
- package/dist/components/navigation-pattern-1/pattern-1.stories.d.ts +0 -16
- package/dist/components/navigation-pattern-1/pattern-1.stories.d.ts.map +0 -1
- package/dist/components/navigation-pattern-1/pattern-1.stories.js +0 -20
- /package/dist/{components → blocks}/date-selector/date-selector-context.d.ts +0 -0
- /package/dist/{components → blocks}/date-selector/date-selector-context.js +0 -0
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { createContext, Fragment, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useRender } from '@base-ui/react/use-render';
|
|
5
5
|
import { cva } from 'class-variance-authority';
|
|
6
6
|
import { cn } from '../../lib/utils';
|
|
7
7
|
import { Button } from '../button';
|
|
8
8
|
import { ButtonGroup, ButtonGroupText } from '../button-group';
|
|
9
|
-
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '../dropdown-menu';
|
|
9
|
+
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '../dropdown-menu';
|
|
10
10
|
import { Input } from '../input';
|
|
11
11
|
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText } from '../input-group';
|
|
12
12
|
import { Kbd } from '../kbd';
|
|
13
13
|
import { ScrollArea } from '../scroll-area';
|
|
14
14
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
|
15
|
-
import { AlertCircle, Check, X } from 'lucide-react';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import { AlertCircle, Check, ChevronDown, Plus, X } from 'lucide-react';
|
|
16
|
+
import { FilterDateMetricBarPeriodSegments, FilterDateMetricBarValueSegment } from './filter-date-metric-value';
|
|
17
|
+
import { DEFAULT_I18N } from './filters-defaults';
|
|
18
|
+
import { createDefaultDateMetricPayload, createFilter, getOperatorsForField, mergeFilterUpdate } from './filters-utils';
|
|
19
|
+
/** Default for `Filters` `submenuSearchMinOptions`. */
|
|
20
|
+
export const DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS = 10;
|
|
21
|
+
function shouldShowSubmenuSearchField(field, minOptions) {
|
|
22
|
+
if (field.searchable === false)
|
|
23
|
+
return false;
|
|
24
|
+
const threshold = Number.isFinite(minOptions) && minOptions >= 0 ? minOptions : DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS;
|
|
25
|
+
return (field.options?.length ?? 0) >= threshold;
|
|
26
|
+
}
|
|
18
27
|
const FilterContext = createContext({
|
|
19
|
-
|
|
20
|
-
size: 'default',
|
|
28
|
+
size: 'sm',
|
|
21
29
|
radius: 'default',
|
|
30
|
+
submenuSearchMinOptions: DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS,
|
|
22
31
|
i18n: DEFAULT_I18N,
|
|
23
32
|
className: undefined,
|
|
24
|
-
showSearchInput: true,
|
|
25
33
|
trigger: undefined,
|
|
26
34
|
allowMultiple: true,
|
|
27
35
|
});
|
|
28
36
|
const useFilterContext = () => useContext(FilterContext);
|
|
29
|
-
// Container
|
|
37
|
+
// Container spacing for the filters wrapper (chip wrap gap by `size` only).
|
|
30
38
|
const filtersContainerVariants = cva('flex flex-wrap items-center', {
|
|
31
39
|
variants: {
|
|
32
|
-
variant: {
|
|
33
|
-
solid: 'gap-2',
|
|
34
|
-
default: '',
|
|
35
|
-
},
|
|
36
40
|
size: {
|
|
37
41
|
sm: 'gap-1.5',
|
|
38
42
|
default: 'gap-2.5',
|
|
@@ -40,8 +44,7 @@ const filtersContainerVariants = cva('flex flex-wrap items-center', {
|
|
|
40
44
|
},
|
|
41
45
|
},
|
|
42
46
|
defaultVariants: {
|
|
43
|
-
|
|
44
|
-
size: 'default',
|
|
47
|
+
size: 'sm',
|
|
45
48
|
},
|
|
46
49
|
});
|
|
47
50
|
function FilterInput({ field, onBlur, onKeyDown, className, ...props }) {
|
|
@@ -139,6 +142,31 @@ const flattenFields = (fields) => {
|
|
|
139
142
|
return [...acc, item];
|
|
140
143
|
}, []);
|
|
141
144
|
};
|
|
145
|
+
/** True when the config uses top-level `{ group?, fields }` containers (add-filter menu shows section labels). */
|
|
146
|
+
const hasTopLevelFieldGroups = (config) => {
|
|
147
|
+
return config.some((item) => {
|
|
148
|
+
if (!item || typeof item !== 'object')
|
|
149
|
+
return false;
|
|
150
|
+
return 'fields' in item && Array.isArray(item.fields);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const parseTopLevelSections = (config) => {
|
|
154
|
+
const sections = [];
|
|
155
|
+
for (const raw of config) {
|
|
156
|
+
const item = raw;
|
|
157
|
+
if (item && typeof item === 'object' && 'fields' in item && Array.isArray(item.fields)) {
|
|
158
|
+
const groupLabel = 'group' in item && item.group ? String(item.group) : undefined;
|
|
159
|
+
sections.push({ groupLabel, fields: item.fields });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const f = raw;
|
|
163
|
+
if (f.key && f.type !== 'separator') {
|
|
164
|
+
sections.push({ groupLabel: undefined, fields: [f] });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return sections;
|
|
169
|
+
};
|
|
142
170
|
const getFieldsMap = (fields) => {
|
|
143
171
|
const flatFields = flattenFields(fields);
|
|
144
172
|
return flatFields.reduce((acc, field) => {
|
|
@@ -149,42 +177,34 @@ const getFieldsMap = (fields) => {
|
|
|
149
177
|
return acc;
|
|
150
178
|
}, {});
|
|
151
179
|
};
|
|
152
|
-
// Helper function to get operators for a field
|
|
153
|
-
const getOperatorsForField = (field, values, i18n) => {
|
|
154
|
-
if (field.operators)
|
|
155
|
-
return field.operators;
|
|
156
|
-
const operators = createOperatorsFromI18n(i18n);
|
|
157
|
-
// Determine field type for operator selection
|
|
158
|
-
let fieldType = field.type || 'select';
|
|
159
|
-
// If it's a select field but has multiple values, treat as multiselect
|
|
160
|
-
if (fieldType === 'select' && values.length > 1) {
|
|
161
|
-
fieldType = 'multiselect';
|
|
162
|
-
}
|
|
163
|
-
// If it's a multiselect field or has multiselect operators, use multiselect operators
|
|
164
|
-
if (fieldType === 'multiselect' || field.type === 'multiselect') {
|
|
165
|
-
return operators.multiselect;
|
|
166
|
-
}
|
|
167
|
-
return operators[fieldType] || operators.select;
|
|
168
|
-
};
|
|
169
180
|
function FilterOperatorDropdown({ field, operator, values, onChange }) {
|
|
170
181
|
const context = useFilterContext();
|
|
171
182
|
const operators = getOperatorsForField(field, values, context.i18n);
|
|
172
|
-
|
|
173
|
-
const operatorLabel =
|
|
174
|
-
|
|
183
|
+
const currentOp = operators.find((op) => op.value === operator);
|
|
184
|
+
const operatorLabel = currentOp?.label || context.i18n.helpers.formatOperator(operator);
|
|
185
|
+
const compactTrigger = Boolean(currentOp?.icon);
|
|
186
|
+
const triggerButton = (_jsx(Button, { variant: "outline", size: context.size, className: cn('text-muted-foreground hover:text-foreground', compactTrigger && 'min-w-8 px-2 text-foreground text-muted-foreground [&_svg]:size-3.5 [&_svg]:shrink-0'), "aria-label": compactTrigger ? operatorLabel : undefined, children: compactTrigger ? currentOp?.icon : operatorLabel }));
|
|
187
|
+
return (_jsxs(DropdownMenu, { children: [compactTrigger ? (_jsxs(Tooltip, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(TooltipTrigger, { render: triggerButton }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: operatorLabel })] })) : (_jsx(DropdownMenuTrigger, { render: triggerButton })), _jsx(DropdownMenuContent, { align: "start", className: "w-fit min-w-fit", children: operators.map((op) => (_jsxs(DropdownMenuItem, { onClick: () => onChange(op.value), className: cn('flex items-center justify-between data-highlighted:bg-accent data-highlighted:text-accent-foreground'), children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [op.icon ? _jsx("span", { className: "inline-flex shrink-0 text-muted-foreground [&_svg]:size-3.5", children: op.icon }) : null, _jsx("span", { className: "truncate", children: op.label })] }), _jsx(Check, { className: cn('ms-auto shrink-0 text-primary', op.value === operator ? 'opacity-100' : 'opacity-0') })] }, op.value))) })] }));
|
|
175
188
|
}
|
|
176
189
|
function SelectOptionsPopover({ field, values, onChange, onClose, inline = false }) {
|
|
177
190
|
const [open, setOpen] = useState(false);
|
|
178
191
|
const [searchInput, setSearchInput] = useState('');
|
|
179
192
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
180
193
|
const inputRef = useRef(null);
|
|
194
|
+
const listboxRef = useRef(null);
|
|
181
195
|
const context = useFilterContext();
|
|
182
196
|
const baseId = useId();
|
|
197
|
+
const showSubmenuSearch = shouldShowSubmenuSearchField(field, context.submenuSearchMinOptions);
|
|
183
198
|
useEffect(() => {
|
|
184
|
-
if (open)
|
|
199
|
+
if (!open)
|
|
200
|
+
return;
|
|
201
|
+
if (showSubmenuSearch) {
|
|
185
202
|
inputRef.current?.focus();
|
|
186
203
|
}
|
|
187
|
-
|
|
204
|
+
else {
|
|
205
|
+
listboxRef.current?.focus();
|
|
206
|
+
}
|
|
207
|
+
}, [open, showSubmenuSearch]);
|
|
188
208
|
useEffect(() => {
|
|
189
209
|
if (highlightedIndex >= 0 && open) {
|
|
190
210
|
const element = document.getElementById(`${baseId}-item-${highlightedIndex}`);
|
|
@@ -192,65 +212,70 @@ function SelectOptionsPopover({ field, values, onChange, onClose, inline = false
|
|
|
192
212
|
}
|
|
193
213
|
}, [highlightedIndex, open, baseId]);
|
|
194
214
|
const isMultiSelect = field.type === 'multiselect' || values.length > 1;
|
|
195
|
-
const effectiveValues = (field.value !== undefined ? field.value : values) || [];
|
|
196
|
-
const selectedOptions = field.options?.filter((opt) => effectiveValues.includes(opt.value)) || [];
|
|
215
|
+
const effectiveValues = useMemo(() => (field.value !== undefined ? field.value : values) || [], [field.value, values]);
|
|
216
|
+
const selectedOptions = useMemo(() => field.options?.filter((opt) => effectiveValues.includes(opt.value)) || [], [field.options, effectiveValues]);
|
|
197
217
|
const triggerPreviewIcons = selectedOptions.slice(0, 3).filter((option) => option.icon != null);
|
|
198
|
-
const unselectedOptions = field.options?.filter((opt) => !effectiveValues.includes(opt.value)) || [];
|
|
218
|
+
const unselectedOptions = useMemo(() => field.options?.filter((opt) => !effectiveValues.includes(opt.value)) || [], [field.options, effectiveValues]);
|
|
199
219
|
// Filter options based on search input
|
|
200
220
|
const filteredSelectedOptions = selectedOptions; // Keep all selected visible
|
|
201
|
-
const filteredUnselectedOptions = unselectedOptions.filter((opt) => opt.label.toLowerCase().includes(searchInput.toLowerCase()));
|
|
202
|
-
const allFilteredOptions = [...filteredSelectedOptions, ...filteredUnselectedOptions];
|
|
203
|
-
const handleClose = () => {
|
|
221
|
+
const filteredUnselectedOptions = useMemo(() => unselectedOptions.filter((opt) => opt.label.toLowerCase().includes(searchInput.toLowerCase())), [unselectedOptions, searchInput]);
|
|
222
|
+
const allFilteredOptions = useMemo(() => [...filteredSelectedOptions, ...filteredUnselectedOptions], [filteredSelectedOptions, filteredUnselectedOptions]);
|
|
223
|
+
const handleClose = useCallback(() => {
|
|
204
224
|
setHighlightedIndex(-1);
|
|
205
225
|
setOpen(false);
|
|
206
226
|
onClose?.();
|
|
207
|
-
};
|
|
208
|
-
const
|
|
227
|
+
}, [onClose]);
|
|
228
|
+
const onSelectOptionsListKeyDown = useCallback((e) => {
|
|
229
|
+
if (e.key === 'ArrowDown') {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
if (allFilteredOptions.length > 0) {
|
|
232
|
+
setHighlightedIndex((prev) => (prev < allFilteredOptions.length - 1 ? prev + 1 : 0));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (e.key === 'ArrowUp') {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
if (allFilteredOptions.length > 0) {
|
|
238
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : allFilteredOptions.length - 1));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else if (e.key === 'ArrowLeft') {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
setHighlightedIndex(-1);
|
|
244
|
+
setOpen(false);
|
|
245
|
+
}
|
|
246
|
+
else if (e.key === 'Enter' && highlightedIndex >= 0) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
const option = allFilteredOptions[highlightedIndex];
|
|
249
|
+
if (option) {
|
|
250
|
+
const isSelected = effectiveValues.includes(option.value);
|
|
251
|
+
const next = isSelected
|
|
252
|
+
? effectiveValues.filter((v) => v !== option.value)
|
|
253
|
+
: isMultiSelect
|
|
254
|
+
? [...effectiveValues, option.value]
|
|
255
|
+
: [option.value];
|
|
256
|
+
if (!isSelected && isMultiSelect && field.maxSelections && next.length > field.maxSelections) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (field.onValueChange) {
|
|
260
|
+
field.onValueChange(next);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
onChange(next);
|
|
264
|
+
}
|
|
265
|
+
if (!isMultiSelect)
|
|
266
|
+
handleClose();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
}, [allFilteredOptions, highlightedIndex, effectiveValues, isMultiSelect, field, onChange, handleClose]);
|
|
271
|
+
const renderMenuContent = () => (_jsxs(_Fragment, { children: [showSubmenuSearch && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": highlightedIndex >= 0 ? `${baseId}-item-${highlightedIndex}` : undefined, placeholder: context.i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 border-input bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', open && 'placeholder:text-foreground'), value: searchInput, onChange: (e) => {
|
|
209
272
|
setSearchInput(e.target.value);
|
|
210
273
|
setHighlightedIndex(-1);
|
|
211
|
-
}, onBlur: () => open && inputRef.current?.focus(), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else if (e.key === 'ArrowUp') {
|
|
219
|
-
e.preventDefault();
|
|
220
|
-
if (allFilteredOptions.length > 0) {
|
|
221
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : allFilteredOptions.length - 1));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (e.key === 'ArrowLeft') {
|
|
225
|
-
e.preventDefault();
|
|
226
|
-
setHighlightedIndex(-1);
|
|
227
|
-
setOpen(false);
|
|
228
|
-
}
|
|
229
|
-
else if (e.key === 'Enter' && highlightedIndex >= 0) {
|
|
230
|
-
e.preventDefault();
|
|
231
|
-
const option = allFilteredOptions[highlightedIndex];
|
|
232
|
-
if (option) {
|
|
233
|
-
const isSelected = effectiveValues.includes(option.value);
|
|
234
|
-
const next = isSelected
|
|
235
|
-
? effectiveValues.filter((v) => v !== option.value)
|
|
236
|
-
: isMultiSelect
|
|
237
|
-
? [...effectiveValues, option.value]
|
|
238
|
-
: [option.value];
|
|
239
|
-
if (!isSelected && isMultiSelect && field.maxSelections && next.length > field.maxSelections) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (field.onValueChange) {
|
|
243
|
-
field.onValueChange(next);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
onChange(next);
|
|
247
|
-
}
|
|
248
|
-
if (!isMultiSelect)
|
|
249
|
-
handleClose();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
e.stopPropagation();
|
|
253
|
-
} }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain", role: "listbox", id: `${baseId}-listbox`, children: _jsxs(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: [allFilteredOptions.length === 0 && _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: context.i18n.noResultsFound }), filteredSelectedOptions.length > 0 && (_jsx(DropdownMenuGroup, { className: "px-1", children: filteredSelectedOptions.map((option, index) => {
|
|
274
|
+
}, onBlur: () => open && showSubmenuSearch && inputRef.current?.focus(), onClick: (e) => e.stopPropagation(), onKeyDown: onSelectOptionsListKeyDown }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { ref: listboxRef, className: cn('flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain', !showSubmenuSearch && 'outline-hidden'), role: "listbox", id: `${baseId}-listbox`, tabIndex: !showSubmenuSearch ? 0 : -1, onKeyDown: (e) => {
|
|
275
|
+
if (!showSubmenuSearch) {
|
|
276
|
+
onSelectOptionsListKeyDown(e);
|
|
277
|
+
}
|
|
278
|
+
}, children: _jsxs(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: [allFilteredOptions.length === 0 && _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: context.i18n.noResultsFound }), filteredSelectedOptions.length > 0 && (_jsx(DropdownMenuGroup, { className: "px-1", children: filteredSelectedOptions.map((option, index) => {
|
|
254
279
|
const isHighlighted = highlightedIndex === index;
|
|
255
280
|
const itemId = `${baseId}-item-${index}`;
|
|
256
281
|
return (_jsxs(DropdownMenuCheckboxItem, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: () => setHighlightedIndex(index), checked: true, className: cn('data-highlighted:bg-accent data-highlighted:text-accent-foreground', option.className), onSelect: (e) => {
|
|
@@ -305,11 +330,12 @@ function SelectOptionsPopover({ field, values, onChange, onClose, inline = false
|
|
|
305
330
|
: context.i18n.select] })) }) }) }), _jsx(DropdownMenuContent, { align: "start", className: cn('w-[200px] px-0', field.className), children: renderMenuContent() })] }));
|
|
306
331
|
}
|
|
307
332
|
function FilterValueSelector({ field, values, onChange, operator, autoFocus }) {
|
|
333
|
+
const context = useFilterContext();
|
|
308
334
|
if (operator === 'empty' || operator === 'not_empty') {
|
|
309
335
|
return null;
|
|
310
336
|
}
|
|
311
337
|
if (field.customRenderer) {
|
|
312
|
-
return (_jsx(ButtonGroupText, { className: "bg-background text-start whitespace-nowrap outline-hidden hover:bg-accent aria-expanded:bg-accent dark:bg-input/30", children: field.customRenderer({ field, values, onChange, operator }) }));
|
|
338
|
+
return (_jsx(ButtonGroupText, { size: context.size, className: "bg-background text-start whitespace-nowrap outline-hidden hover:bg-accent aria-expanded:bg-accent dark:bg-input/30", children: field.customRenderer({ field, values, onChange, operator }) }));
|
|
313
339
|
}
|
|
314
340
|
if (field.type === 'text') {
|
|
315
341
|
return (_jsx(FilterInput, { type: "text", value: values[0] || '', onChange: (e) => onChange([e.target.value]), placeholder: field.placeholder, pattern: field.pattern, field: field, className: cn('w-36', field.className), autoFocus: autoFocus }));
|
|
@@ -319,32 +345,30 @@ function FilterValueSelector({ field, values, onChange, operator, autoFocus }) {
|
|
|
319
345
|
}
|
|
320
346
|
return _jsx(SelectOptionsPopover, { field: field, values: values, onChange: onChange });
|
|
321
347
|
}
|
|
322
|
-
|
|
348
|
+
function FilterBarRow({ filter, field, i18n, size, radius, lastAddedFilterId, onUpdateFilter, onRemove, filterLabelClassName, }) {
|
|
349
|
+
return (_jsxs(ButtonGroup, { className: cn(radius === 'full' &&
|
|
350
|
+
'*:data-[slot]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:!rounded-r-full [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0'), children: [_jsxs(ButtonGroupText, { size: size, className: cn(filterLabelClassName, radius === 'full' && '!rounded-l-full !rounded-r-none'), children: [field.icon && field.icon, field.label] }), field.type === 'date_metric' ? (_jsx(FilterDateMetricBarPeriodSegments, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), i18n: i18n, size: size, autoFocus: filter.id === lastAddedFilterId })) : null, _jsx(FilterOperatorDropdown, { field: field, operator: filter.operator, values: filter.values, onChange: (operator) => onUpdateFilter({ operator }) }), field.type === 'date_metric' ? (_jsx(FilterDateMetricBarValueSegment, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), operator: filter.operator, i18n: i18n, size: size, onOperatorChange: (operator) => onUpdateFilter({ operator }) }, `${filter.id}-${filter.operator}`)) : (_jsx(FilterValueSelector, { field: field, values: filter.values, onChange: (values) => onUpdateFilter({ values }), operator: filter.operator, autoFocus: filter.id === lastAddedFilterId })), _jsx(FilterRemoveButton, { onClick: onRemove, className: cn(radius === 'full' && 'shrink-0 !rounded-l-none !rounded-r-full') })] }));
|
|
351
|
+
}
|
|
352
|
+
export const FiltersContent = ({ filters, fields, onChange, lastAddedFilterId = null }) => {
|
|
323
353
|
const context = useFilterContext();
|
|
324
354
|
const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]);
|
|
325
355
|
const updateFilter = useCallback((filterId, updates) => {
|
|
326
356
|
onChange(filters.map((filter) => {
|
|
327
|
-
if (filter.id
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
updatedFilter.values = [];
|
|
331
|
-
}
|
|
332
|
-
return updatedFilter;
|
|
333
|
-
}
|
|
334
|
-
return filter;
|
|
357
|
+
if (filter.id !== filterId)
|
|
358
|
+
return filter;
|
|
359
|
+
return mergeFilterUpdate(filter, updates, fieldsMap[filter.field]);
|
|
335
360
|
}));
|
|
336
|
-
}, [filters, onChange]);
|
|
361
|
+
}, [filters, onChange, fieldsMap]);
|
|
337
362
|
const removeFilter = useCallback((filterId) => {
|
|
338
363
|
onChange(filters.filter((filter) => filter.id !== filterId));
|
|
339
364
|
}, [filters, onChange]);
|
|
340
365
|
return (_jsx("div", { className: cn(filtersContainerVariants({
|
|
341
|
-
variant: context.variant,
|
|
342
366
|
size: context.size,
|
|
343
367
|
}), context.className), children: filters.map((filter) => {
|
|
344
368
|
const field = fieldsMap[filter.field];
|
|
345
369
|
if (!field)
|
|
346
370
|
return null;
|
|
347
|
-
return (
|
|
371
|
+
return (_jsx(FilterBarRow, { filter: filter, field: field, i18n: context.i18n, size: context.size, radius: context.radius, lastAddedFilterId: lastAddedFilterId, onUpdateFilter: (updates) => updateFilter(filter.id, updates), onRemove: () => removeFilter(filter.id) }, filter.id));
|
|
348
372
|
}) }));
|
|
349
373
|
};
|
|
350
374
|
function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i18n, isActive, onActive, onBack, onClose, }) {
|
|
@@ -352,9 +376,11 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
|
|
|
352
376
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
353
377
|
const inputRef = useRef(null);
|
|
354
378
|
const baseId = useId();
|
|
379
|
+
const filterCtx = useFilterContext();
|
|
380
|
+
const showSubmenuSearch = shouldShowSubmenuSearchField(field, filterCtx.submenuSearchMinOptions);
|
|
355
381
|
useEffect(() => {
|
|
356
382
|
if (isActive) {
|
|
357
|
-
if (
|
|
383
|
+
if (showSubmenuSearch) {
|
|
358
384
|
inputRef.current?.focus();
|
|
359
385
|
}
|
|
360
386
|
else {
|
|
@@ -362,7 +388,7 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
|
|
|
362
388
|
listbox?.focus();
|
|
363
389
|
}
|
|
364
390
|
}
|
|
365
|
-
}, [isActive,
|
|
391
|
+
}, [isActive, showSubmenuSearch, baseId]);
|
|
366
392
|
const filteredOptions = useMemo(() => {
|
|
367
393
|
return (field.options?.filter((option) => {
|
|
368
394
|
const isSelected = currentValues.includes(option.value);
|
|
@@ -388,77 +414,48 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
|
|
|
388
414
|
element?.scrollIntoView({ block: 'nearest' });
|
|
389
415
|
}
|
|
390
416
|
}, [resolvedHighlight, isActive, baseId]);
|
|
391
|
-
|
|
417
|
+
const onFilterSubmenuListKeyDown = useCallback((e) => {
|
|
418
|
+
if (e.key === 'ArrowDown') {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
if (filteredOptions.length > 0) {
|
|
421
|
+
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (e.key === 'ArrowUp') {
|
|
425
|
+
e.preventDefault();
|
|
426
|
+
if (filteredOptions.length > 0) {
|
|
427
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (e.key === 'ArrowLeft') {
|
|
431
|
+
e.preventDefault();
|
|
432
|
+
onBack?.();
|
|
433
|
+
}
|
|
434
|
+
else if (e.key === 'Enter' && resolvedHighlight >= 0) {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
const option = filteredOptions[resolvedHighlight];
|
|
437
|
+
if (option) {
|
|
438
|
+
onToggle(option.value, currentValues.includes(option.value));
|
|
439
|
+
if (!isMultiSelect) {
|
|
440
|
+
onBack?.();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (e.key === 'Escape') {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
onClose?.();
|
|
447
|
+
}
|
|
448
|
+
e.stopPropagation();
|
|
449
|
+
}, [filteredOptions, resolvedHighlight, currentValues, isMultiSelect, onToggle, onBack, onClose]);
|
|
450
|
+
return (_jsxs("div", { className: "flex flex-col", onMouseEnter: onActive, children: [showSubmenuSearch && (_jsxs(_Fragment, { children: [_jsx(Input, { ref: inputRef, role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-haspopup": "listbox", "aria-controls": `${baseId}-listbox`, "aria-activedescendant": resolvedHighlight >= 0 ? `${baseId}-item-${resolvedHighlight}` : undefined, placeholder: i18n.placeholders.searchField(field.label || ''), className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', isActive && 'placeholder:text-foreground'), value: searchInput, onBlur: () => isActive && showSubmenuSearch && inputRef.current?.focus(), onChange: (e) => {
|
|
392
451
|
setSearchInput(e.target.value);
|
|
393
452
|
setHighlightedIndex(-1);
|
|
394
453
|
}, onFocus: () => onActive?.(), onMouseEnter: (e) => {
|
|
395
454
|
onActive?.();
|
|
396
455
|
e.stopPropagation();
|
|
397
|
-
}, onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (filteredOptions.length > 0) {
|
|
401
|
-
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
else if (e.key === 'ArrowUp') {
|
|
405
|
-
e.preventDefault();
|
|
406
|
-
if (filteredOptions.length > 0) {
|
|
407
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
else if (e.key === 'ArrowLeft') {
|
|
411
|
-
e.preventDefault();
|
|
412
|
-
onBack?.();
|
|
413
|
-
}
|
|
414
|
-
else if (e.key === 'Enter' && resolvedHighlight >= 0) {
|
|
415
|
-
e.preventDefault();
|
|
416
|
-
const option = filteredOptions[resolvedHighlight];
|
|
417
|
-
if (option) {
|
|
418
|
-
onToggle(option.value, currentValues.includes(option.value));
|
|
419
|
-
if (!isMultiSelect) {
|
|
420
|
-
onBack?.();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
else if (e.key === 'Escape') {
|
|
425
|
-
e.preventDefault();
|
|
426
|
-
onClose?.();
|
|
427
|
-
}
|
|
428
|
-
e.stopPropagation();
|
|
429
|
-
} }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain outline-hidden", role: "listbox", id: `${baseId}-listbox`, tabIndex: field.searchable === false ? 0 : -1, onKeyDown: (e) => {
|
|
430
|
-
if (field.searchable === false) {
|
|
431
|
-
if (e.key === 'ArrowDown') {
|
|
432
|
-
e.preventDefault();
|
|
433
|
-
if (filteredOptions.length > 0) {
|
|
434
|
-
setHighlightedIndex((prev) => (prev < filteredOptions.length - 1 ? prev + 1 : 0));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
else if (e.key === 'ArrowUp') {
|
|
438
|
-
e.preventDefault();
|
|
439
|
-
if (filteredOptions.length > 0) {
|
|
440
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : filteredOptions.length - 1));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
else if (e.key === 'ArrowLeft') {
|
|
444
|
-
e.preventDefault();
|
|
445
|
-
onBack?.();
|
|
446
|
-
}
|
|
447
|
-
else if (e.key === 'Enter' && resolvedHighlight >= 0) {
|
|
448
|
-
e.preventDefault();
|
|
449
|
-
const option = filteredOptions[resolvedHighlight];
|
|
450
|
-
if (option) {
|
|
451
|
-
onToggle(option.value, currentValues.includes(option.value));
|
|
452
|
-
if (!isMultiSelect) {
|
|
453
|
-
onBack?.();
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else if (e.key === 'Escape') {
|
|
458
|
-
e.preventDefault();
|
|
459
|
-
onClose?.();
|
|
460
|
-
}
|
|
461
|
-
e.stopPropagation();
|
|
456
|
+
}, onClick: (e) => e.stopPropagation(), onKeyDown: onFilterSubmenuListKeyDown }), _jsx(DropdownMenuSeparator, {})] })), _jsx("div", { className: "relative flex max-h-full", children: _jsx("div", { className: "flex max-h-[min(var(--available-height),24rem)] w-full scroll-pt-2 scroll-pb-2 flex-col overscroll-contain outline-hidden", role: "listbox", id: `${baseId}-listbox`, tabIndex: !showSubmenuSearch ? 0 : -1, onKeyDown: (e) => {
|
|
457
|
+
if (!showSubmenuSearch) {
|
|
458
|
+
onFilterSubmenuListKeyDown(e);
|
|
462
459
|
}
|
|
463
460
|
}, children: _jsx(ScrollArea, { className: "size-full min-h-0 **:data-[slot=scroll-area-scrollbar]:m-0 [&_[data-slot=scroll-area-viewport]]:h-full [&_[data-slot=scroll-area-viewport]]:overscroll-contain", children: filteredOptions.length === 0 ? (_jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: i18n.noResultsFound })) : (_jsx(DropdownMenuGroup, { children: filteredOptions.map((option, index) => {
|
|
464
461
|
const isSelected = currentValues.includes(option.value);
|
|
@@ -470,14 +467,16 @@ function FilterSubmenuContent({ field, currentValues, isMultiSelect, onToggle, i
|
|
|
470
467
|
}, onCheckedChange: () => onToggle(option.value, isSelected), children: [option.icon && option.icon, _jsx("span", { className: "truncate", children: option.label })] }, String(option.value)));
|
|
471
468
|
}) })) }) }) })] }));
|
|
472
469
|
}
|
|
473
|
-
export function Filters({ filters, fields, onChange, className,
|
|
470
|
+
export function Filters({ filters, fields, onChange, className, size = 'sm', radius = 'default', i18n, showSearchInput = true, submenuSearchMinOptions = DEFAULT_SUBMENU_SEARCH_MIN_OPTIONS, trigger, allowMultiple = true, menuPopupClassName, collapsibleAddFilterGroups = false, defaultAddFilterGroupsCollapsed = false, nestedAddFilterGroups = false, collapseAddButton = false, enableShortcut = false, shortcutKey = 'f', shortcutLabel = 'F', }) {
|
|
474
471
|
const [addFilterOpen, setAddFilterOpen] = useState(false);
|
|
475
472
|
const [menuSearchInput, setMenuSearchInput] = useState('');
|
|
476
473
|
const [activeMenu, setActiveMenu] = useState('root');
|
|
477
474
|
const [openSubMenu, setOpenSubMenu] = useState(null);
|
|
475
|
+
const [openAddFilterSectionIndex, setOpenAddFilterSectionIndex] = useState(null);
|
|
478
476
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
479
477
|
const [lastAddedFilterId, setLastAddedFilterId] = useState(null);
|
|
480
478
|
const rootInputRef = useRef(null);
|
|
479
|
+
const prevAddFilterOpenRef = useRef(false);
|
|
481
480
|
const rootId = useId();
|
|
482
481
|
useEffect(() => {
|
|
483
482
|
if (!enableShortcut)
|
|
@@ -501,6 +500,7 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
|
|
|
501
500
|
// Track which filter instance is being built in the current Add Filter menu session
|
|
502
501
|
// Maps fieldKey -> unique filterId created during this open session
|
|
503
502
|
const [sessionFilterIds, setSessionFilterIds] = useState({});
|
|
503
|
+
const [collapsedAddFilterGroupIndices, setCollapsedAddFilterGroupIndices] = useState(() => new Set());
|
|
504
504
|
useEffect(() => {
|
|
505
505
|
if (lastAddedFilterId) {
|
|
506
506
|
const timer = setTimeout(() => {
|
|
@@ -509,34 +509,30 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
|
|
|
509
509
|
return () => clearTimeout(timer);
|
|
510
510
|
}
|
|
511
511
|
}, [lastAddedFilterId]);
|
|
512
|
-
const mergedI18n = {
|
|
512
|
+
const mergedI18n = useMemo(() => ({
|
|
513
513
|
...DEFAULT_I18N,
|
|
514
514
|
...i18n,
|
|
515
515
|
operators: { ...DEFAULT_I18N.operators, ...i18n?.operators },
|
|
516
516
|
placeholders: { ...DEFAULT_I18N.placeholders, ...i18n?.placeholders },
|
|
517
517
|
validation: { ...DEFAULT_I18N.validation, ...i18n?.validation },
|
|
518
|
-
|
|
518
|
+
helpers: { ...DEFAULT_I18N.helpers, ...i18n?.helpers },
|
|
519
|
+
}), [i18n]);
|
|
519
520
|
const fieldsMap = useMemo(() => getFieldsMap(fields), [fields]);
|
|
520
521
|
const updateFilter = useCallback((filterId, updates) => {
|
|
521
522
|
onChange(filters.map((filter) => {
|
|
522
|
-
if (filter.id
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
updatedFilter.values = [];
|
|
526
|
-
}
|
|
527
|
-
return updatedFilter;
|
|
528
|
-
}
|
|
529
|
-
return filter;
|
|
523
|
+
if (filter.id !== filterId)
|
|
524
|
+
return filter;
|
|
525
|
+
return mergeFilterUpdate(filter, updates, fieldsMap[filter.field]);
|
|
530
526
|
}));
|
|
531
|
-
}, [filters, onChange]);
|
|
527
|
+
}, [filters, onChange, fieldsMap]);
|
|
532
528
|
const removeFilter = useCallback((filterId) => {
|
|
533
529
|
onChange(filters.filter((filter) => filter.id !== filterId));
|
|
534
530
|
}, [filters, onChange]);
|
|
535
531
|
const addFilter = useCallback((fieldKey) => {
|
|
536
532
|
const field = fieldsMap[fieldKey];
|
|
537
533
|
if (field && field.key) {
|
|
538
|
-
const defaultOperator = field.defaultOperator || (field.type === 'multiselect' ? 'is_any_of' : 'is');
|
|
539
|
-
const defaultValues = field.type === 'text' ? [''] : [];
|
|
534
|
+
const defaultOperator = field.defaultOperator || (field.type === 'multiselect' ? 'is_any_of' : field.type === 'date_metric' ? 'value_greater_than' : 'is');
|
|
535
|
+
const defaultValues = field.type === 'text' ? [''] : field.type === 'date_metric' ? [createDefaultDateMetricPayload()] : [];
|
|
540
536
|
const newFilter = createFilter(fieldKey, defaultOperator, defaultValues);
|
|
541
537
|
setLastAddedFilterId(newFilter.id);
|
|
542
538
|
onChange([...filters, newFilter]);
|
|
@@ -544,6 +540,18 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
|
|
|
544
540
|
setMenuSearchInput('');
|
|
545
541
|
}
|
|
546
542
|
}, [fieldsMap, filters, onChange]);
|
|
543
|
+
const toggleAddFilterGroupCollapsed = useCallback((sectionIndex) => {
|
|
544
|
+
setCollapsedAddFilterGroupIndices((prev) => {
|
|
545
|
+
const next = new Set(prev);
|
|
546
|
+
if (next.has(sectionIndex)) {
|
|
547
|
+
next.delete(sectionIndex);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
next.add(sectionIndex);
|
|
551
|
+
}
|
|
552
|
+
return next;
|
|
553
|
+
});
|
|
554
|
+
}, []);
|
|
547
555
|
const selectableFields = useMemo(() => {
|
|
548
556
|
const flatFields = flattenFields(fields);
|
|
549
557
|
return flatFields.filter((field) => {
|
|
@@ -554,96 +562,322 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
|
|
|
554
562
|
return !filters.some((filter) => filter.field === field.key);
|
|
555
563
|
});
|
|
556
564
|
}, [fields, filters, allowMultiple]);
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
565
|
+
const useGroupedAddMenu = useMemo(() => hasTopLevelFieldGroups(fields), [fields]);
|
|
566
|
+
const { filteredFields, filteredSections } = useMemo(() => {
|
|
567
|
+
const matchesSearch = (f, groupLabel) => {
|
|
568
|
+
if (!menuSearchInput)
|
|
569
|
+
return true;
|
|
570
|
+
const ql = menuSearchInput.toLowerCase();
|
|
571
|
+
if (f.label?.toLowerCase().includes(ql))
|
|
572
|
+
return true;
|
|
573
|
+
if (groupLabel?.toLowerCase().includes(ql))
|
|
574
|
+
return true;
|
|
575
|
+
return false;
|
|
576
|
+
};
|
|
577
|
+
const isSelectableLeaf = (f) => {
|
|
578
|
+
if (!f.key || f.type === 'separator')
|
|
579
|
+
return false;
|
|
580
|
+
if (allowMultiple)
|
|
581
|
+
return true;
|
|
582
|
+
return !filters.some((filter) => filter.field === f.key);
|
|
583
|
+
};
|
|
584
|
+
if (!useGroupedAddMenu) {
|
|
585
|
+
return {
|
|
586
|
+
filteredFields: selectableFields.filter((f) => matchesSearch(f, undefined)),
|
|
587
|
+
filteredSections: undefined,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const sections = parseTopLevelSections(fields)
|
|
591
|
+
.map((sec) => ({
|
|
592
|
+
groupLabel: sec.groupLabel,
|
|
593
|
+
fields: sec.fields.filter(isSelectableLeaf).filter((f) => matchesSearch(f, sec.groupLabel)),
|
|
594
|
+
}))
|
|
595
|
+
.filter((sec) => sec.fields.length > 0);
|
|
596
|
+
return {
|
|
597
|
+
filteredFields: sections.flatMap((s) => s.fields),
|
|
598
|
+
filteredSections: sections,
|
|
599
|
+
};
|
|
600
|
+
}, [fields, filters, allowMultiple, menuSearchInput, useGroupedAddMenu, selectableFields]);
|
|
601
|
+
const nestedRootRows = useMemo(() => {
|
|
602
|
+
if (!useGroupedAddMenu || !nestedAddFilterGroups) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
if (!filteredSections?.length) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
return filteredSections.map((section, sectionIndex) => {
|
|
609
|
+
const needsSectionSubmenu = Boolean(section.groupLabel) || section.fields.length > 1;
|
|
610
|
+
if (needsSectionSubmenu) {
|
|
611
|
+
return {
|
|
612
|
+
rowType: 'section',
|
|
613
|
+
sectionIndex,
|
|
614
|
+
label: section.groupLabel ?? mergedI18n.addFilterUngroupedSection,
|
|
615
|
+
fields: section.fields,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
return { rowType: 'inlineField', sectionIndex, field: section.fields[0] };
|
|
619
|
+
});
|
|
620
|
+
}, [useGroupedAddMenu, nestedAddFilterGroups, filteredSections, mergedI18n.addFilterUngroupedSection]);
|
|
621
|
+
const flatAddMenuKeyboardFields = useMemo(() => {
|
|
622
|
+
if (nestedAddFilterGroups && useGroupedAddMenu) {
|
|
623
|
+
return filteredFields;
|
|
624
|
+
}
|
|
625
|
+
if (!useGroupedAddMenu || !collapsibleAddFilterGroups || !filteredSections?.length) {
|
|
626
|
+
return filteredFields;
|
|
627
|
+
}
|
|
628
|
+
return filteredSections.flatMap((sec, si) => (collapsedAddFilterGroupIndices.has(si) ? [] : sec.fields));
|
|
629
|
+
}, [nestedAddFilterGroups, useGroupedAddMenu, collapsibleAddFilterGroups, filteredSections, collapsedAddFilterGroupIndices, filteredFields]);
|
|
630
|
+
const rootMenuLength = nestedRootRows !== null ? nestedRootRows.length : flatAddMenuKeyboardFields.length;
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
const justOpened = addFilterOpen && !prevAddFilterOpenRef.current;
|
|
633
|
+
prevAddFilterOpenRef.current = addFilterOpen;
|
|
634
|
+
if (!justOpened) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (nestedAddFilterGroups && useGroupedAddMenu) {
|
|
638
|
+
setCollapsedAddFilterGroupIndices(new Set());
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (collapsibleAddFilterGroups && defaultAddFilterGroupsCollapsed && filteredSections?.length) {
|
|
642
|
+
setCollapsedAddFilterGroupIndices(new Set(filteredSections.map((_, i) => i)));
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
setCollapsedAddFilterGroupIndices(new Set());
|
|
646
|
+
}
|
|
647
|
+
}, [addFilterOpen, collapsibleAddFilterGroups, defaultAddFilterGroupsCollapsed, filteredSections, nestedAddFilterGroups, useGroupedAddMenu]);
|
|
560
648
|
const rootResolvedHighlight = useMemo(() => {
|
|
561
|
-
if (!addFilterOpen ||
|
|
649
|
+
if (!addFilterOpen || rootMenuLength === 0) {
|
|
562
650
|
return -1;
|
|
563
651
|
}
|
|
564
652
|
if (highlightedIndex < 0) {
|
|
565
653
|
return 0;
|
|
566
654
|
}
|
|
567
|
-
return Math.min(highlightedIndex,
|
|
568
|
-
}, [addFilterOpen,
|
|
655
|
+
return Math.min(highlightedIndex, rootMenuLength - 1);
|
|
656
|
+
}, [addFilterOpen, rootMenuLength, highlightedIndex]);
|
|
569
657
|
useEffect(() => {
|
|
570
658
|
if (rootResolvedHighlight >= 0 && addFilterOpen) {
|
|
571
659
|
const element = document.getElementById(`${rootId}-item-${rootResolvedHighlight}`);
|
|
572
660
|
element?.scrollIntoView({ block: 'nearest' });
|
|
573
661
|
}
|
|
574
662
|
}, [rootResolvedHighlight, addFilterOpen, rootId]);
|
|
663
|
+
const addMenuCollapsed = Boolean(collapseAddButton && filters.length > 0);
|
|
575
664
|
const triggerButton = useRender({
|
|
576
|
-
render: trigger,
|
|
665
|
+
render: (addMenuCollapsed ? (_jsx(Button, { variant: "outline", size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', "aria-label": mergedI18n.addFilterTitle, children: _jsx(Plus, { className: "size-4 shrink-0", "aria-hidden": true }) })) : (trigger)),
|
|
577
666
|
defaultTagName: 'button',
|
|
578
667
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
668
|
+
const filterContextValue = useMemo(() => ({
|
|
669
|
+
size,
|
|
670
|
+
radius,
|
|
671
|
+
submenuSearchMinOptions,
|
|
672
|
+
i18n: mergedI18n,
|
|
673
|
+
className,
|
|
674
|
+
trigger,
|
|
675
|
+
allowMultiple,
|
|
676
|
+
}), [size, radius, submenuSearchMinOptions, mergedI18n, className, trigger, allowMultiple]);
|
|
677
|
+
const renderAddFilterMenuFieldRow = (field, index, rowOpts) => {
|
|
678
|
+
const rootListHi = rowOpts?.rootListHighlightIndex;
|
|
679
|
+
const isHighlighted = rootListHi === null ? false : rootResolvedHighlight === (rootListHi === undefined ? index : rootListHi);
|
|
680
|
+
const itemId = rowOpts?.itemId ?? `${rootId}-item-${index}`;
|
|
681
|
+
const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
|
|
682
|
+
const syncRootListHighlight = () => {
|
|
683
|
+
if (rowOpts?.rootListHighlightIndex === null)
|
|
684
|
+
return;
|
|
685
|
+
const hi = rowOpts?.rootListHighlightIndex ?? index;
|
|
686
|
+
setHighlightedIndex(hi);
|
|
687
|
+
setActiveMenu('root');
|
|
688
|
+
};
|
|
689
|
+
if (hasSubMenu) {
|
|
690
|
+
const isMultiSelect = field.type === 'multiselect';
|
|
691
|
+
const fieldKey = field.key;
|
|
692
|
+
const sessionFilterId = sessionFilterIds[fieldKey];
|
|
693
|
+
const sessionFilter = sessionFilterId ? filters.find((f) => f.id === sessionFilterId) : null;
|
|
694
|
+
const currentValues = sessionFilter?.values || [];
|
|
695
|
+
return (_jsxs(DropdownMenuSub, { open: openSubMenu === fieldKey, onOpenChange: (open) => {
|
|
696
|
+
if (open) {
|
|
697
|
+
setOpenSubMenu(fieldKey);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
if (openSubMenu === fieldKey) {
|
|
701
|
+
setOpenSubMenu(null);
|
|
702
|
+
setActiveMenu('root');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}, children: [_jsxs(DropdownMenuSubTrigger, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: syncRootListHighlight, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }), _jsx(DropdownMenuSubContent, { className: "w-[200px]", side: "right", children: _jsx(FilterSubmenuContent, { field: field, currentValues: currentValues, isMultiSelect: isMultiSelect, i18n: mergedI18n, isActive: activeMenu === fieldKey, onActive: () => {
|
|
706
|
+
if (shouldShowSubmenuSearchField(field, filterContextValue.submenuSearchMinOptions)) {
|
|
707
|
+
setActiveMenu(fieldKey);
|
|
708
|
+
}
|
|
709
|
+
}, onBack: () => {
|
|
710
|
+
setOpenSubMenu(null);
|
|
711
|
+
setActiveMenu('root');
|
|
712
|
+
}, onClose: () => setAddFilterOpen(false), onToggle: (value, isSelected) => {
|
|
713
|
+
if (isMultiSelect) {
|
|
714
|
+
const nextValues = isSelected ? currentValues.filter((v) => v !== value) : [...currentValues, value];
|
|
715
|
+
if (sessionFilter) {
|
|
716
|
+
if (nextValues.length === 0) {
|
|
717
|
+
onChange(filters.filter((f) => f.id !== sessionFilter.id));
|
|
718
|
+
setSessionFilterIds((prev) => ({
|
|
719
|
+
...prev,
|
|
720
|
+
[fieldKey]: '',
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
onChange(filters.map((f) => (f.id === sessionFilter.id ? { ...f, values: nextValues } : f)));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
const newFilter = createFilter(fieldKey, field.defaultOperator || 'is_any_of', nextValues);
|
|
729
|
+
onChange([...filters, newFilter]);
|
|
730
|
+
setSessionFilterIds((prev) => ({
|
|
731
|
+
...prev,
|
|
732
|
+
[fieldKey]: newFilter.id,
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
const newFilter = createFilter(fieldKey, field.defaultOperator || 'is', [value]);
|
|
738
|
+
setLastAddedFilterId(newFilter.id);
|
|
739
|
+
onChange([...filters, newFilter]);
|
|
740
|
+
setAddFilterOpen(false);
|
|
741
|
+
}
|
|
742
|
+
} }) })] }, fieldKey));
|
|
743
|
+
}
|
|
744
|
+
return (_jsxs(DropdownMenuItem, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: syncRootListHighlight, onClick: () => field.key && addFilter(field.key), className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }, field.key));
|
|
745
|
+
};
|
|
746
|
+
return (_jsx(FilterContext.Provider, { value: filterContextValue, children: _jsxs("div", { className: cn(filtersContainerVariants({ size }), className), children: [selectableFields.length > 0 && (_jsxs(DropdownMenu, { open: addFilterOpen, onOpenChange: (open) => {
|
|
588
747
|
setAddFilterOpen(open);
|
|
589
748
|
if (!open) {
|
|
590
749
|
setMenuSearchInput('');
|
|
591
750
|
setSessionFilterIds({});
|
|
592
751
|
setOpenSubMenu(null);
|
|
752
|
+
setOpenAddFilterSectionIndex(null);
|
|
593
753
|
setHighlightedIndex(-1);
|
|
594
754
|
}
|
|
595
755
|
else {
|
|
596
756
|
setActiveMenu('root');
|
|
597
757
|
setHighlightedIndex(-1);
|
|
598
758
|
}
|
|
599
|
-
}, children: [_jsx(DropdownMenuTrigger, { render: triggerButton }), _jsxs(DropdownMenuContent, { className: cn('w-[220px]', menuPopupClassName), align: "start", children: [showSearchInput && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { ref: rootInputRef, role: "combobox", "aria-controls": `${rootId}-listbox`, "aria-activedescendant": rootResolvedHighlight >= 0 ? `${rootId}-item-${rootResolvedHighlight}` : undefined, placeholder: mergedI18n.searchFields, className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', activeMenu === 'root' && 'placeholder:text-foreground'), value: menuSearchInput, onFocus: () => setActiveMenu('root'), onMouseEnter: () => setActiveMenu('root'), onBlur: (
|
|
759
|
+
}, children: [_jsx(DropdownMenuTrigger, { render: triggerButton }), _jsxs(DropdownMenuContent, { className: cn('w-[220px]', menuPopupClassName), align: "start", children: [showSearchInput && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { ref: rootInputRef, role: "combobox", "aria-controls": `${rootId}-listbox`, "aria-activedescendant": rootResolvedHighlight >= 0 ? `${rootId}-item-${rootResolvedHighlight}` : undefined, placeholder: mergedI18n.searchFields, className: cn('h-8 rounded-none border-0 bg-transparent! px-2 text-sm shadow-none', 'focus-visible:border-border focus-visible:ring-0 focus-visible:ring-offset-0', activeMenu === 'root' && 'placeholder:text-foreground'), value: menuSearchInput, onFocus: () => setActiveMenu('root'), onMouseEnter: () => setActiveMenu('root'), onBlur: (e) => {
|
|
760
|
+
const rt = e.relatedTarget;
|
|
761
|
+
const menuSurface = e.currentTarget.closest('[data-slot="dropdown-menu-content"]');
|
|
762
|
+
const focusStaysInsideMenu = Boolean(rt) &&
|
|
763
|
+
(Boolean(menuSurface?.contains(rt)) ||
|
|
764
|
+
Boolean(rt?.closest?.('[data-slot="dropdown-menu-sub-content"]')));
|
|
765
|
+
if (activeMenu === 'root' && !focusStaysInsideMenu) {
|
|
766
|
+
rootInputRef.current?.focus();
|
|
767
|
+
}
|
|
768
|
+
}, onChange: (e) => {
|
|
600
769
|
setMenuSearchInput(e.target.value);
|
|
601
770
|
setHighlightedIndex(-1);
|
|
602
771
|
}, onClick: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
603
772
|
if (e.key === 'ArrowDown') {
|
|
604
773
|
e.preventDefault();
|
|
605
|
-
if (
|
|
606
|
-
setHighlightedIndex((prev) => (prev <
|
|
774
|
+
if (rootMenuLength > 0) {
|
|
775
|
+
setHighlightedIndex((prev) => (prev < rootMenuLength - 1 ? prev + 1 : 0));
|
|
607
776
|
}
|
|
608
777
|
}
|
|
609
778
|
else if (e.key === 'ArrowUp') {
|
|
610
779
|
e.preventDefault();
|
|
611
|
-
if (
|
|
612
|
-
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 :
|
|
780
|
+
if (rootMenuLength > 0) {
|
|
781
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : rootMenuLength - 1));
|
|
613
782
|
}
|
|
614
783
|
}
|
|
615
784
|
else if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && rootResolvedHighlight >= 0) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
785
|
+
if (nestedRootRows) {
|
|
786
|
+
const row = nestedRootRows[rootResolvedHighlight];
|
|
787
|
+
if (e.key === 'ArrowRight' && row) {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
if (row.rowType === 'section') {
|
|
790
|
+
setOpenAddFilterSectionIndex(row.sectionIndex);
|
|
791
|
+
setOpenSubMenu(null);
|
|
792
|
+
setActiveMenu('root');
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
const field = row.field;
|
|
796
|
+
const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
|
|
797
|
+
if (hasSubMenu && field.key) {
|
|
798
|
+
setOpenSubMenu(field.key);
|
|
799
|
+
setActiveMenu(field.key);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else if (e.key === 'ArrowLeft') {
|
|
804
|
+
e.preventDefault();
|
|
805
|
+
if (openSubMenu) {
|
|
806
|
+
setOpenSubMenu(null);
|
|
807
|
+
setActiveMenu('root');
|
|
808
|
+
}
|
|
809
|
+
else if (openAddFilterSectionIndex !== null) {
|
|
810
|
+
setOpenAddFilterSectionIndex(null);
|
|
811
|
+
setActiveMenu('root');
|
|
812
|
+
}
|
|
813
|
+
}
|
|
622
814
|
}
|
|
623
|
-
else
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
815
|
+
else {
|
|
816
|
+
const field = flatAddMenuKeyboardFields[rootResolvedHighlight];
|
|
817
|
+
const hasSubMenu = field && (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
|
|
818
|
+
if (e.key === 'ArrowRight' && hasSubMenu) {
|
|
819
|
+
e.preventDefault();
|
|
820
|
+
setOpenSubMenu(field.key || null);
|
|
821
|
+
setActiveMenu(field.key || 'root');
|
|
822
|
+
}
|
|
823
|
+
else if (e.key === 'ArrowLeft') {
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
if (openSubMenu) {
|
|
826
|
+
setOpenSubMenu(null);
|
|
827
|
+
setActiveMenu('root');
|
|
828
|
+
}
|
|
628
829
|
}
|
|
629
830
|
}
|
|
630
831
|
}
|
|
631
832
|
else if (e.key === 'Enter' && rootResolvedHighlight >= 0) {
|
|
632
833
|
e.preventDefault();
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
834
|
+
if (nestedRootRows) {
|
|
835
|
+
const row = nestedRootRows[rootResolvedHighlight];
|
|
836
|
+
if (row?.rowType === 'section') {
|
|
837
|
+
if (openAddFilterSectionIndex === row.sectionIndex) {
|
|
838
|
+
setOpenAddFilterSectionIndex(null);
|
|
839
|
+
setOpenSubMenu(null);
|
|
840
|
+
setActiveMenu('root');
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
setOpenAddFilterSectionIndex(row.sectionIndex);
|
|
844
|
+
setOpenSubMenu(null);
|
|
845
|
+
setActiveMenu('root');
|
|
846
|
+
}
|
|
638
847
|
}
|
|
639
|
-
else {
|
|
640
|
-
|
|
848
|
+
else if (row?.field.key) {
|
|
849
|
+
const fieldKey = row.field.key;
|
|
850
|
+
const field = row.field;
|
|
851
|
+
const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
|
|
852
|
+
if (!hasSubMenu) {
|
|
853
|
+
addFilter(fieldKey);
|
|
854
|
+
}
|
|
855
|
+
else if (openSubMenu === fieldKey) {
|
|
641
856
|
setOpenSubMenu(null);
|
|
642
857
|
setActiveMenu('root');
|
|
643
858
|
}
|
|
644
859
|
else {
|
|
645
|
-
setOpenSubMenu(
|
|
646
|
-
setActiveMenu(
|
|
860
|
+
setOpenSubMenu(fieldKey);
|
|
861
|
+
setActiveMenu(fieldKey);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
const field = flatAddMenuKeyboardFields[rootResolvedHighlight];
|
|
867
|
+
if (field.key) {
|
|
868
|
+
const hasSubMenu = (field.type === 'select' || field.type === 'multiselect') && field.options?.length;
|
|
869
|
+
if (!hasSubMenu) {
|
|
870
|
+
addFilter(field.key);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
if (openSubMenu === field.key) {
|
|
874
|
+
setOpenSubMenu(null);
|
|
875
|
+
setActiveMenu('root');
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
setOpenSubMenu(field.key);
|
|
879
|
+
setActiveMenu(field.key);
|
|
880
|
+
}
|
|
647
881
|
}
|
|
648
882
|
}
|
|
649
883
|
}
|
|
@@ -656,78 +890,47 @@ export function Filters({ filters, fields, onChange, className, variant = 'defau
|
|
|
656
890
|
if (filteredFields.length === 0) {
|
|
657
891
|
return _jsx("div", { className: "py-2 text-center text-sm text-muted-foreground", children: mergedI18n.noFieldsFound });
|
|
658
892
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
if (openSubMenu === fieldKey) {
|
|
675
|
-
setOpenSubMenu(null);
|
|
676
|
-
setActiveMenu('root');
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}, children: [_jsxs(DropdownMenuSubTrigger, { id: itemId, role: "option", "aria-selected": isHighlighted, "data-highlighted": isHighlighted || undefined, onMouseEnter: () => {
|
|
680
|
-
setHighlightedIndex(index);
|
|
681
|
-
setActiveMenu('root');
|
|
682
|
-
}, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: [field.icon, _jsx("span", { children: field.label })] }), _jsx(DropdownMenuSubContent, { className: "w-[200px]", side: "right", children: _jsx(FilterSubmenuContent, { field: field, currentValues: currentValues, isMultiSelect: isMultiSelect, i18n: mergedI18n, isActive: activeMenu === fieldKey, onActive: () => {
|
|
683
|
-
if (field.searchable !== false) {
|
|
684
|
-
setActiveMenu(fieldKey);
|
|
685
|
-
}
|
|
686
|
-
}, onBack: () => {
|
|
893
|
+
if (useGroupedAddMenu && filteredSections?.length) {
|
|
894
|
+
if (nestedRootRows !== null) {
|
|
895
|
+
return nestedRootRows.map((row, ri) => (_jsx(Fragment, { children: row.rowType === 'inlineField' ? (_jsx(DropdownMenuGroup, { children: renderAddFilterMenuFieldRow(row.field, ri, {
|
|
896
|
+
itemId: `${rootId}-item-${ri}`,
|
|
897
|
+
rootListHighlightIndex: ri,
|
|
898
|
+
}) })) : (_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuSub, { open: openAddFilterSectionIndex === row.sectionIndex, onOpenChange: (open) => {
|
|
899
|
+
if (open) {
|
|
900
|
+
setOpenAddFilterSectionIndex(row.sectionIndex);
|
|
901
|
+
setOpenSubMenu(null);
|
|
902
|
+
setActiveMenu('root');
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
setOpenAddFilterSectionIndex((prev) => prev === row.sectionIndex ? null : prev);
|
|
687
906
|
setOpenSubMenu(null);
|
|
688
907
|
setActiveMenu('root');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
setSessionFilterIds((prev) => ({
|
|
698
|
-
...prev,
|
|
699
|
-
[fieldKey]: '',
|
|
700
|
-
}));
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
onChange(filters.map((f) => f.id === sessionFilter.id ? { ...f, values: nextValues } : f));
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
const newFilter = createFilter(fieldKey, field.defaultOperator || 'is_any_of', nextValues);
|
|
708
|
-
onChange([...filters, newFilter]);
|
|
709
|
-
setSessionFilterIds((prev) => ({
|
|
710
|
-
...prev,
|
|
711
|
-
[fieldKey]: newFilter.id,
|
|
712
|
-
}));
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
const newFilter = createFilter(fieldKey, field.defaultOperator || 'is', [
|
|
717
|
-
value,
|
|
718
|
-
]);
|
|
719
|
-
setLastAddedFilterId(newFilter.id);
|
|
720
|
-
onChange([...filters, newFilter]);
|
|
721
|
-
setAddFilterOpen(false);
|
|
722
|
-
}
|
|
723
|
-
} }) })] }, fieldKey));
|
|
908
|
+
}
|
|
909
|
+
}, children: [_jsx(DropdownMenuSubTrigger, { id: `${rootId}-item-${ri}`, role: "option", "aria-selected": rootResolvedHighlight === ri, "data-highlighted": rootResolvedHighlight === ri || undefined, onMouseEnter: () => {
|
|
910
|
+
setHighlightedIndex(ri);
|
|
911
|
+
setActiveMenu('root');
|
|
912
|
+
}, className: "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground", children: _jsx("span", { className: "min-w-0 flex-1 truncate", children: row.label }) }), _jsx(DropdownMenuSubContent, { className: cn('w-[220px]', menuPopupClassName), side: "right", align: "start", children: _jsx(ScrollArea, { className: "max-h-[min(var(--available-height),22rem)] **:data-[slot=scroll-area-scrollbar]:m-0", children: _jsx(DropdownMenuGroup, { children: row.fields.map((field, fi) => renderAddFilterMenuFieldRow(field, fi, {
|
|
913
|
+
itemId: `${rootId}-s${row.sectionIndex}-i${fi}`,
|
|
914
|
+
rootListHighlightIndex: null,
|
|
915
|
+
})) }) }) })] }) })) }, `add-filter-root-${row.rowType}-${row.sectionIndex}`)));
|
|
724
916
|
}
|
|
725
|
-
|
|
726
|
-
|
|
917
|
+
let running = 0;
|
|
918
|
+
return filteredSections.map((section, si) => {
|
|
919
|
+
const showCollapsibleHeader = collapsibleAddFilterGroups && (!!section.groupLabel || section.fields.length > 1);
|
|
920
|
+
const isCollapsed = collapsibleAddFilterGroups && showCollapsibleHeader && collapsedAddFilterGroupIndices.has(si);
|
|
921
|
+
return (_jsxs(Fragment, { children: [_jsxs(DropdownMenuGroup, { children: [collapsibleAddFilterGroups && showCollapsibleHeader ? (_jsxs("button", { type: "button", "aria-expanded": !isCollapsed, className: cn('flex w-full items-center gap-1 rounded-md px-1.5 py-1.5 text-left text-xs font-medium text-muted-foreground outline-none select-none', 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring'), onClick: (e) => {
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
e.stopPropagation();
|
|
924
|
+
toggleAddFilterGroupCollapsed(si);
|
|
925
|
+
}, children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: section.groupLabel ?? mergedI18n.addFilterUngroupedSection }), _jsx(ChevronDown, { className: cn('size-4 shrink-0 opacity-70 transition-transform', isCollapsed && '-rotate-90'), "aria-hidden": true })] })) : section.groupLabel ? (_jsx(DropdownMenuLabel, { children: section.groupLabel })) : null, (!collapsibleAddFilterGroups || !showCollapsibleHeader || !isCollapsed) &&
|
|
926
|
+
section.fields.map((field) => renderAddFilterMenuFieldRow(field, running++))] }), collapsibleAddFilterGroups && filteredSections.length > 1 && si < filteredSections.length - 1 ? (_jsx(DropdownMenuSeparator, {})) : null] }, `add-filter-section-${si}`));
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return flatAddMenuKeyboardFields.map((field, index) => renderAddFilterMenuFieldRow(field, index));
|
|
727
930
|
})() }) }) })] })] })), filters.map((filter) => {
|
|
728
931
|
const field = fieldsMap[filter.field];
|
|
729
932
|
if (!field)
|
|
730
933
|
return null;
|
|
731
|
-
return (
|
|
934
|
+
return (_jsx(FilterBarRow, { filter: filter, field: field, i18n: mergedI18n, size: size, radius: radius, lastAddedFilterId: lastAddedFilterId, onUpdateFilter: (updates) => updateFilter(filter.id, updates), onRemove: () => removeFilter(filter.id), filterLabelClassName: "bg-background dark:bg-input/30" }, filter.id));
|
|
732
935
|
})] }) }));
|
|
733
936
|
}
|