Widgets
FilterBar
A filter bar assembled from a description of the fields.
A filter bar assembled from a description of the fields. The bar does not know what to filter — the fields array does. So a new condition is added with one entry rather than edits across five files.
Installation
npx @toimetdev/pathlogs-ui add filter-barExample
- UI-12БагИмпорт досок из Trello падает на больших проектахМТ
- UI-14ФичаЖивые обновления доски по SSEАСДК
- UI-15РефакторингВынести разбор Markdown в отдельный модульДК
- UI-16ФичаWIP-лимиты у колонокМТ
- UI-17ФичаКритический путь на диаграмме ГантаАС
- UI-9БагТултипы обрезались в колонках доскиДК
const [filter, setFilter] = useState(emptyFilter(taskFilterFields));
const matched = tasks.filter((t) => matchesFilter(taskFilterFields, filter, t));
<FilterBar
fields={taskFilterFields}
value={filter}
onChange={setFilter}
matchedCount={matched.length}
totalCount={tasks.length}
savedFilters={presets}
onSaveFilter={(name, query) => saveFilterAction(projectId, name, query)}
onDeleteFilter={deleteFilterAction}
/>Describing the fields
A field is a label, a control type and a «does this item match» function. The ready-made matchers cover almost everything:
import {
textMatcher, equalsMatcher, includesMatcher, type FilterField,
} from "@/components/ui/filter-bar/filterModel";
export const taskFilterFields: FilterField<Task>[] = [
{
key: "q", label: "Search", kind: "text", placeholder: "title or number",
matches: textMatcher((t) => [t.title, t.number]),
},
{
key: "status", label: "Status", kind: "select", anyLabel: "Any",
options: statusOptions,
matches: equalsMatcher((t) => t.status),
},
{
key: "assignee", label: "Assignee", kind: "select", anyLabel: "Any",
options: members.map((m) => ({ value: m.id, label: m.name })),
matches: includesMatcher((t) => t.assignees),
},
];| Prop | Type | Default | Description |
|---|---|---|---|
textMatcher | (pick: (item) => (string | number | null)[]) => Matcher | — | A case-insensitive substring across several properties at once: «12» finds task №12, «pay» finds «Payment page». |
equalsMatcher | (pick: (item) => string | null) => Matcher | — | An exact match on one property. |
includesMatcher | (pick: (item) => { id: string }[]) => Matcher | — | The value is among the related entities: assignees, labels. |
textMatcher(pick: (item) => (string | number | null)[]) => MatcherA case-insensitive substring across several properties at once: «12» finds task №12, «pay» finds «Payment page».
equalsMatcher(pick: (item) => string | null) => MatcherAn exact match on one property.
includesMatcher(pick: (item) => { id: string }[]) => MatcherThe value is among the related entities: assignees, labels.
A matcher of your own is an ordinary (item, value) => booleanfunction: a date range, «overdue», «unassigned».
The query string
The state serialises to an ordinary query string. Saved presets and the address bar hold it in exactly that form:
serializeFilter(fields, filter); // "status=TODO&assignee=u1"
parseFilter(fields, query); // back into stateSo a link to a filtered list opens as precisely what it was, and a saved filter is just a string in the database.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fields* | FilterField<T>[] | — | The field descriptions. |
value* | FilterState | — | The current state. |
onChange* | (next: FilterState) => void | — | A change. |
savedFilters | { id, name, query }[] | — | Presets. Without handlers they can only be applied. |
onSaveFilter | (name, query) => void | Promise | — | Saving a preset. Without it there is no «save filter» button. |
onDeleteFilter | (id: string) => void | Promise | — | Deleting a preset. |
matchedCount / totalCount | number | — | How many passed the filter and how many there are — the caption under the bar. |
compact | boolean | false | A dense layout: the bar sits inside a header rather than standing on its own. |
labels | FilterBarLabels | — | Captions. English by default. |
fields*FilterField<T>[]The field descriptions.
value*FilterStateThe current state.
onChange*(next: FilterState) => voidA change.
savedFilters{ id, name, query }[]Presets. Without handlers they can only be applied.
onSaveFilter(name, query) => void | PromiseSaving a preset. Without it there is no «save filter» button.
onDeleteFilter(id: string) => void | PromiseDeleting a preset.
matchedCount / totalCountnumberHow many passed the filter and how many there are — the caption under the bar.
compactbooleandefaults to: false
A dense layout: the bar sits inside a header rather than standing on its own.
labelsFilterBarLabelsCaptions. English by default.