diff --git a/docs/content/docs/2.components/filter.md b/docs/content/docs/2.components/filter.md new file mode 100644 index 000000000..0d1ded5c7 --- /dev/null +++ b/docs/content/docs/2.components/filter.md @@ -0,0 +1,253 @@ +--- +title: Filter :badge{label="Soon" class="align-text-top"} +description: A composite filter component with search, presets, and a draggable field editor — modelled after the native Bitrix24 filter. +category: form +links: + - label: GitHub + iconName: GitHubIcon + to: https://github.com/bitrix24/b24ui/blob/main/src/runtime/components/Filter.vue +--- + +## Usage + +The `Filter` is a *view-only* component: it never performs network requests. The consumer is responsible for loading data, persisting presets, and reacting to the `apply` event. + +It is composed of: + +- `FilterBar` — the collapsed strip with search, chips, and trailing buttons (search / clear / settings). +- `FilterPanel` — the popover (desktop) or drawer (mobile) that opens from the bar. +- `FilterPresets` — the left column with saved filters. +- `FilterFieldsEditor` — the right column with the active field list. +- `FilterField` — a single field row (label + control + `...` menu + drag handle). + +You usually only mount `` — the subcomponents are internal. + +### Minimal example + +```vue + + + +``` + +### With presets + +```vue + + + +``` + +### Field types + +The core supports the following types out of the box: + +| Type | Control | +|----------------|------------------------------------------| +| `string` | `Input` | +| `number` | Operator `Select` + `InputNumber` × 1–2 | +| `money` | Same as `number` (currency in value) | +| `date` | `Select` with Bitrix-style date presets | +| `time` | Native `time` input | +| `select` | `Select` | +| `multiselect` | `SelectMenu` (multiple) | +| `boolean` | `RadioGroup` (Да / Нет / Не важно) | +| `custom` | Slot `field-{customMeta.kind}` | + +### Custom field types + +Use `type: 'custom'` + the matching `customMeta.kind` to render anything via a slot: + +```vue + + + +``` + +```ts +const fields: FilterFieldConfig[] = [ + { id: 'assignee', label: 'Ответственный', type: 'custom', customMeta: { kind: 'user' } } +] +``` + +The slot receives the field config, the current condition (or `null`), and an `update(condition)` function. Pass `null` to clear the value. + +### Adaptive layout + +The panel switches between `Popover` (desktop) and `Drawer` (mobile) via `useDevice`. No prop required — the breakpoint is `md`. + +### Operators and value shape + +Each condition is a discriminated union by `operator`: + +```ts +type FilterFieldCondition + = | { operator: 'filled' } + | { operator: 'empty' } + | { operator: 'between', value: [unknown, unknown] } + | { operator: 'in', value: unknown[] } + | { operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'startsWith', value: unknown } +``` + +`filled` / `empty` are **operators**, not UI modes — they describe the data state in the record. When chosen, the value input is hidden. + +### Date values + +For `type: 'date'`, the condition's value is a `FilterDateValue`: + +```ts +type FilterDateValue + = | { kind: 'any' } + | { kind: 'preset', preset: FilterDatePreset } // today, yesterday, this-week, ... + | { kind: 'exact', date: string /* ISO 8601 */ } + | { kind: 'range', from: string, to: string } + | { kind: 'relative', days: number, direction: 'past' | 'future' } +``` + +### Drag & drop + +Fields and presets can be reordered by drag. Internally uses `useSortable` from `@vueuse/integrations`. A keyboard alternative is available via the `...` menu on each row (`↑ Вверх` / `↓ Вниз`). + +### Locale + +Default strings are in Russian. Override partially via the `locale` prop: + +```vue + +``` + +### Imperative API + +`` exposes a few methods through `defineExpose`: + +- `applyPreset(id: string): void` +- `apply(): void` +- `reset(): void` +- `openPanel(): void` +- `closePanel(): void` + +```vue + + + +``` + +## API + +### Props + +- `fields: FilterFieldConfig[]`{lang="ts-type"} — required, all available fields. +- `presets?: FilterPreset[]`{lang="ts-type"} — saved filters. +- `modelValue: FilterValue`{lang="ts-type"} — v-model. Map of `fieldId → condition`. +- `activeFields: string[]`{lang="ts-type"} — `v-model:activeFields`. Order matters. +- `activePresetId: string | null`{lang="ts-type"} — `v-model:activePresetId`. +- `searchQuery: string`{lang="ts-type"} — `v-model:searchQuery`. +- `defaultFields?: string[]`{lang="ts-type"} — used by "Вернуть поля по умолчанию". +- `visibleTagsCount?: number`{lang="ts-type"} — chips visible in bar before "+N" (default `3`). +- `searchDebounce?: number`{lang="ts-type"} — search debounce in ms (default `300`). +- `loading?: boolean`{lang="ts-type"}, `disabled?: boolean`{lang="ts-type"} +- `allowSavePresets?: boolean`{lang="ts-type"} (default `true`) +- `allowEditPresets?: boolean`{lang="ts-type"} (default `true`) +- `allowReorderFields?: boolean`{lang="ts-type"} (default `true`) +- `allowReorderPresets?: boolean`{lang="ts-type"} (default `true`) +- `locale?: Partial`{lang="ts-type"} + +### Emits + +- `apply` — `(payload: { values, query, presetId }) => void`. Triggered by `Найти` or Enter in search. +- `reset` — `() => void`. Clears values, keeps the active field set. +- `fieldsReset` — `() => void`. Restores the default field set. +- `presetSave` — `(payload: { name, preset }) => void`. +- `presetUpdate` — `(payload: { id, patch }) => void`. +- `presetDelete` — `(id: string) => void`. +- v-model: `update:modelValue`, `update:activeFields`, `update:activePresetId`, `update:searchQuery`. + +### Slots + +- `field-{type}` — override the control for a specific type. For `type: 'custom'`, the slot is `field-{customMeta.kind}` (falls back to `field-custom`). +- `preset-actions` — extra menu items in a preset's `DropdownMenu`. +- `empty-fields` — placeholder when no fields are active. +- `bar` — fully custom FilterBar. + +## Security + +- Internal `Сделки в работе +
+ + + + + +" +`; + +exports[`Filter > renders with activeFields and values correctly 1`] = ` +"
+
+
Сумма > 1000 +
+
+
+ + + +
" +`; + +exports[`Filter > renders with class correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with disabled correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with fields correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with loading correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with searchQuery correctly 1`] = ` +"
+
+
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders without fields correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; diff --git a/test/components/__snapshots__/Filter.spec.ts.snap b/test/components/__snapshots__/Filter.spec.ts.snap new file mode 100644 index 000000000..0a718a30e --- /dev/null +++ b/test/components/__snapshots__/Filter.spec.ts.snap @@ -0,0 +1,163 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Filter > renders with active preset correctly 1`] = ` +"
+
+
Сделки в работе +
+
+
+ + + +
" +`; + +exports[`Filter > renders with activeFields and values correctly 1`] = ` +"
+
+
Сумма > 1000 +
+
+
+ + + +
" +`; + +exports[`Filter > renders with class correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with disabled correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with fields correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with loading correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders with searchQuery correctly 1`] = ` +"
+
+
+ +
+
+
+ + + +
" +`; + +exports[`Filter > renders without fields correctly 1`] = ` +"
+
+
+ +
+ +
+
+
+ + + +
" +`;