@parallelworks/ui 0.76.0 → 0.78.0
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/README.md +30 -5
- package/dist/access/AccessDrawer.d.ts +28 -7
- package/dist/access/AccessDrawer.js +216 -35
- package/dist/access/AccessList.d.ts +19 -1
- package/dist/access/AccessList.js +220 -9
- package/dist/access/GrantView.d.ts +3 -1
- package/dist/access/GrantView.js +17 -4
- package/dist/access/PrincipalPicker.js +5 -5
- package/dist/access/focus.js +1 -1
- package/dist/access/index.d.ts +2 -2
- package/dist/access/index.js +2 -2
- package/dist/access/strings.d.ts +18 -0
- package/dist/access/strings.js +20 -2
- package/dist/access/types.d.ts +26 -0
- package/dist/ai/adapter/types.d.ts +1 -0
- package/dist/ai/components/AllocationSelector.js +1 -1
- package/dist/ai/components/AttachmentManager.js +5 -5
- package/dist/ai/components/AttachmentUpload.js +2 -2
- package/dist/ai/components/BranchNavigator.js +1 -1
- package/dist/ai/components/ChatActivity.d.ts +2 -0
- package/dist/ai/components/ChatActivity.js +31 -0
- package/dist/ai/components/ChatEmptyState.js +3 -3
- package/dist/ai/components/ChatInput.js +3 -5
- package/dist/ai/components/ChatLayout.js +1 -1
- package/dist/ai/components/ChatMessage.js +1 -1
- package/dist/ai/components/ChatMessageList.js +2 -2
- package/dist/ai/components/ChatSidebar.js +3 -3
- package/dist/ai/components/ChatThread.js +4 -4
- package/dist/ai/components/ComposerAddMenu.js +9 -20
- package/dist/ai/components/ModelSelector.js +3 -3
- package/dist/ai/components/PastedText.js +5 -5
- package/dist/ai/components/SessionView.js +5 -2
- package/dist/ai/components/ShareDialog.js +3 -3
- package/dist/ai/components/SlashMenu.d.ts +31 -4
- package/dist/ai/components/SlashMenu.js +36 -63
- package/dist/ai/components/agent/ActivityLine.js +1 -1
- package/dist/ai/components/agent/AgentMessageParts.d.ts +1 -1
- package/dist/ai/components/agent/AgentMessageParts.js +44 -18
- package/dist/ai/components/conversationActions.js +3 -3
- package/dist/ai/core/ChatProvider.js +11 -2
- package/dist/ai/core/config.d.ts +10 -1
- package/dist/ai/core/config.js +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/strings.d.ts +2 -0
- package/dist/ai/strings.js +2 -1
- package/dist/ai/types.d.ts +3 -0
- package/dist/ai/ui/ConversationSidebar.js +3 -3
- package/dist/ai/ui/sidebar.js +3 -3
- package/dist/assets/yaml.worker-DOBV2lND.js +884 -0
- package/dist/components/ActionMenu.d.ts +38 -9
- package/dist/components/ActionMenu.js +48 -22
- package/dist/components/Avatar.d.ts +66 -6
- package/dist/components/Avatar.js +130 -18
- package/dist/components/Breadcrumbs.d.ts +4 -0
- package/dist/components/Breadcrumbs.js +26 -10
- package/dist/components/Button.d.ts +3 -2
- package/dist/components/Button.js +11 -1
- package/dist/components/Callout.d.ts +3 -2
- package/dist/components/Callout.js +3 -2
- package/dist/components/Card.d.ts +33 -9
- package/dist/components/Card.js +24 -8
- package/dist/components/Checkbox.d.ts +3 -1
- package/dist/components/Checkbox.js +4 -2
- package/dist/components/ColorInput.js +1 -1
- package/dist/components/Combobox.d.ts +23 -0
- package/dist/components/Combobox.js +252 -86
- package/dist/components/ConfirmModal.d.ts +26 -2
- package/dist/components/ConfirmModal.js +81 -15
- package/dist/components/ContextMenu.d.ts +29 -2
- package/dist/components/ContextMenu.js +60 -8
- package/dist/components/CopyButton.d.ts +21 -8
- package/dist/components/CopyButton.js +29 -9
- package/dist/components/CreateModal.d.ts +16 -4
- package/dist/components/CreateModal.js +27 -16
- package/dist/components/DatePicker.d.ts +31 -1
- package/dist/components/DatePicker.js +178 -47
- package/dist/components/DescriptionList.d.ts +13 -3
- package/dist/components/DescriptionList.js +43 -11
- package/dist/components/Disclosure.d.ts +10 -2
- package/dist/components/Disclosure.js +18 -7
- package/dist/components/Drawer.js +5 -5
- package/dist/components/EmptyState.d.ts +12 -4
- package/dist/components/EmptyState.js +18 -10
- package/dist/components/ErrorPage.d.ts +40 -0
- package/dist/components/ErrorPage.js +101 -0
- package/dist/components/ExternalLink.d.ts +4 -1
- package/dist/components/ExternalLink.js +4 -5
- package/dist/components/Field.d.ts +21 -6
- package/dist/components/Field.js +32 -16
- package/dist/components/FileButton.d.ts +19 -0
- package/dist/components/FileButton.js +32 -0
- package/dist/components/HoverCard.d.ts +26 -6
- package/dist/components/HoverCard.js +11 -5
- package/dist/components/IconButton.js +4 -2
- package/dist/components/InfoTip.js +3 -3
- package/dist/components/Input.d.ts +33 -3
- package/dist/components/Input.js +149 -14
- package/dist/components/Kbd.d.ts +40 -0
- package/dist/components/Kbd.js +168 -0
- package/dist/components/Listbox.d.ts +103 -0
- package/dist/components/Listbox.js +190 -0
- package/dist/components/Loader.d.ts +8 -2
- package/dist/components/Loader.js +16 -1
- package/dist/components/Markdown.js +1 -1
- package/dist/components/MarkdownImpl.js +1 -1
- package/dist/components/Menu.d.ts +20 -2
- package/dist/components/Menu.js +48 -19
- package/dist/components/MenuEntries.d.ts +38 -2
- package/dist/components/MenuEntries.js +266 -39
- package/dist/components/Meter.d.ts +50 -11
- package/dist/components/Meter.js +71 -21
- package/dist/components/Modal.d.ts +9 -3
- package/dist/components/Modal.js +68 -29
- package/dist/components/OneTimeCodeInput.js +1 -1
- package/dist/components/PageHeader.js +111 -7
- package/dist/components/PageHeaderAction.d.ts +8 -3
- package/dist/components/PageHeaderAction.js +122 -34
- package/dist/components/Pagination.d.ts +13 -1
- package/dist/components/Pagination.js +90 -19
- package/dist/components/Popover.d.ts +13 -2
- package/dist/components/Popover.js +20 -21
- package/dist/components/Provider.d.ts +140 -3
- package/dist/components/Provider.js +142 -12
- package/dist/components/RefreshButton.d.ts +13 -6
- package/dist/components/RefreshButton.js +17 -9
- package/dist/components/RunStatusIcon.d.ts +16 -7
- package/dist/components/RunStatusIcon.js +21 -8
- package/dist/components/Section.d.ts +11 -4
- package/dist/components/Section.js +33 -22
- package/dist/components/SegmentedControl.d.ts +16 -3
- package/dist/components/SegmentedControl.js +7 -3
- package/dist/components/Select.d.ts +19 -3
- package/dist/components/Select.js +114 -68
- package/dist/components/SelectionCopy.d.ts +10 -0
- package/dist/components/SelectionCopy.js +105 -0
- package/dist/components/SettingModeOption.js +1 -1
- package/dist/components/SettingsCard.js +1 -1
- package/dist/components/Skeleton.js +1 -1
- package/dist/components/StatusBadge.d.ts +55 -10
- package/dist/components/StatusBadge.js +91 -15
- package/dist/components/StickyBar.js +1 -1
- package/dist/components/Table.d.ts +22 -4
- package/dist/components/Table.js +46 -24
- package/dist/components/Tabs.d.ts +9 -4
- package/dist/components/Tabs.js +9 -4
- package/dist/components/Tag.d.ts +49 -0
- package/dist/components/Tag.js +199 -0
- package/dist/components/ThemeEditor.js +1 -1
- package/dist/components/Toast.js +1 -1
- package/dist/components/ToggleGroup.d.ts +4 -1
- package/dist/components/ToggleGroup.js +30 -13
- package/dist/components/Tooltip.d.ts +9 -3
- package/dist/components/Tooltip.js +55 -39
- package/dist/components/buttonClasses.d.ts +5 -2
- package/dist/components/buttonClasses.js +11 -10
- package/dist/components/chart.d.ts +69 -0
- package/dist/components/chart.js +145 -0
- package/dist/components/comboboxItems.d.ts +7 -1
- package/dist/components/comboboxItems.js +6 -2
- package/dist/components/dismiss.d.ts +5 -1
- package/dist/components/dismiss.js +6 -2
- package/dist/components/floating.d.ts +4 -4
- package/dist/components/floating.js +5 -5
- package/dist/components/headerActionsFit.d.ts +37 -0
- package/dist/components/headerActionsFit.js +81 -0
- package/dist/components/pageHeaderStore.d.ts +3 -0
- package/dist/components/pageHeaderStore.js +3 -0
- package/dist/components/pathBreaks.js +2 -2
- package/dist/components/popupContainer.d.ts +17 -0
- package/dist/components/popupContainer.js +69 -0
- package/dist/components/relativeTime.d.ts +21 -10
- package/dist/components/relativeTime.js +50 -22
- package/dist/components/segmented.d.ts +18 -4
- package/dist/components/segmented.js +48 -6
- package/dist/components/typedDate.d.ts +5 -0
- package/dist/components/typedDate.js +50 -0
- package/dist/components/useClosingValue.d.ts +4 -0
- package/dist/components/useClosingValue.js +12 -0
- package/dist/components/useCopy.d.ts +6 -0
- package/dist/components/useCopy.js +3 -3
- package/dist/components/useNarrow.d.ts +2 -0
- package/dist/components/useNarrow.js +16 -0
- package/dist/editor/Editor.js +1 -1
- package/dist/editor/FormCodePanel.js +1 -1
- package/dist/editor/Monaco.js +1 -1
- package/dist/editor/workers.js +1 -1
- package/dist/file-explorer/FileExplorer.js +16 -15
- package/dist/file-explorer/FileExplorerContext.js +2 -2
- package/dist/file-explorer/FilePreview/Viewer.js +3 -3
- package/dist/file-explorer/FilePreview/index.js +2 -2
- package/dist/file-explorer/FileRowsWindow.js +3 -3
- package/dist/file-explorer/FileTable.js +1 -1
- package/dist/file-explorer/LoadMoreRow.js +1 -1
- package/dist/file-explorer/TreeView.js +5 -5
- package/dist/file-explorer/helpers.js +2 -2
- package/dist/file-explorer/toasts.js +2 -2
- package/dist/form/Fields/CheckboxGroupField.js +1 -1
- package/dist/form/Fields/DropdownField.js +1 -1
- package/dist/form/Fields/DurationField.js +2 -2
- package/dist/form/Fields/MultiDropdownField.js +1 -1
- package/dist/form/Fields/NumberField.js +1 -1
- package/dist/form/Fields/ObjectField.js +1 -1
- package/dist/form/Fields/RangeField.js +1 -1
- package/dist/form/Fields/StringField.js +1 -1
- package/dist/form/Form.js +3 -3
- package/dist/form/FormLayout.js +1 -1
- package/dist/form/Wizard/WizardContainer.js +1 -1
- package/dist/form/Wizard/WizardNavigation.js +1 -1
- package/dist/form/Wizard/WizardStepContent.js +1 -1
- package/dist/form/fieldRegistry.js +1 -1
- package/dist/form/formEditing.js +1 -1
- package/dist/format.d.ts +17 -7
- package/dist/format.js +39 -16
- package/dist/graph/AnnotationBanner.js +2 -2
- package/dist/graph/Connectors.js +5 -5
- package/dist/graph/DependencyGraph.js +10 -9
- package/dist/graph/GraphEditor.js +4 -4
- package/dist/graph/GraphEditorDialogs.js +13 -12
- package/dist/graph/GraphNode.js +1 -1
- package/dist/graph/InputDialog.js +5 -5
- package/dist/graph/InputsEditor.js +7 -7
- package/dist/graph/MatrixGroup.js +2 -2
- package/dist/graph/ProblemsButton.js +3 -3
- package/dist/graph/Reveal.js +1 -1
- package/dist/graph/RunWarningsList.js +1 -1
- package/dist/graph/ShortcutsButton.js +25 -8
- package/dist/graph/StepSummaries.js +1 -1
- package/dist/graph/TreeView.js +2 -2
- package/dist/graph/WorkflowSettingsDialog.js +3 -3
- package/dist/graph/editorFields.js +3 -3
- package/dist/graph/editorPrimitives.js +1 -1
- package/dist/graph/editorRows.js +5 -5
- package/dist/graph/fieldProblems.js +2 -2
- package/dist/graph/inputRefs.js +7 -6
- package/dist/graph/stepWith.js +2 -2
- package/dist/index.d.ts +30 -21
- package/dist/index.js +20 -12
- package/dist/list/ActionToolbar.d.ts +4 -0
- package/dist/list/ActionToolbar.js +35 -12
- package/dist/list/ListFilterBar.d.ts +42 -0
- package/dist/list/ListFilterBar.js +789 -0
- package/dist/list/ListPage.d.ts +42 -4
- package/dist/list/ListPage.js +56 -22
- package/dist/list/ListTable.d.ts +27 -3
- package/dist/list/ListTable.js +48 -23
- package/dist/list/ListToolbar.d.ts +31 -0
- package/dist/list/ListToolbar.js +43 -0
- package/dist/list/ListViewControls.d.ts +8 -4
- package/dist/list/ListViewControls.js +22 -14
- package/dist/list/UserHoverCard.d.ts +8 -2
- package/dist/list/UserHoverCard.js +26 -13
- package/dist/list/filterModel.d.ts +122 -0
- package/dist/list/filterModel.js +308 -0
- package/dist/list/index.d.ts +6 -2
- package/dist/list/index.js +6 -2
- package/dist/list/listSearch.d.ts +1 -1
- package/dist/list/listSearch.js +8 -4
- package/dist/list/listView.d.ts +24 -2
- package/dist/list/listView.js +47 -45
- package/dist/list/rowMenu.d.ts +10 -5
- package/dist/list/rowMenu.js +17 -7
- package/dist/list/useListFilters.d.ts +68 -0
- package/dist/list/useListFilters.js +252 -0
- package/dist/logviewer/LogViewer.js +4 -4
- package/dist/styles/theme.css +94 -0
- package/dist/styles.css +1 -1
- package/package.json +11 -11
- package/dist/assets/yaml.worker-k9AdOqzf.js +0 -1441
- package/dist/components/fullscreen.d.ts +0 -4
- package/dist/components/fullscreen.js +0 -18
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
|
+
import { CheckIcon } from "../icons.js";
|
|
2
3
|
import { useLink, useStrings } from "../components/Provider.js";
|
|
4
|
+
import { useCopy } from "../components/useCopy.js";
|
|
3
5
|
import { Avatar } from "../components/Avatar.js";
|
|
4
6
|
import { LoadingRegion, Skeleton } from "../components/Skeleton.js";
|
|
5
7
|
import { CopyButton } from "../components/CopyButton.js";
|
|
@@ -68,21 +70,32 @@ function UserHoverCard({ username, name, avatarSrc, badge, href, linkComponent,
|
|
|
68
70
|
var EMAIL = /^[^\s@]+@[^\s@]+$/;
|
|
69
71
|
var rowClasses = "flex gap-2 text-(--theme-muted-text-color)";
|
|
70
72
|
/** One detail line of a hover card: a leading icon and its content. With
|
|
71
|
-
* `onClick` the line is a button
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
* `onClick` the line is a button; with `copy` it is a button that copies a value,
|
|
74
|
+
* such as the email address it shows, its icon turning into a check for a moment. */
|
|
75
|
+
function HoverCardRow({ icon, children, title, align = "center", truncate = false, onClick, copy }) {
|
|
76
|
+
const { common: t } = useStrings();
|
|
77
|
+
const { copy: write, copied } = useCopy();
|
|
78
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
79
|
+
/* @__PURE__ */ jsx("span", {
|
|
80
|
+
"aria-hidden": "true",
|
|
81
|
+
className: cx("flex shrink-0 items-center [&>svg]:h-3.5 [&>svg]:w-3.5", align === "start" && "mt-0.5"),
|
|
82
|
+
children: copied ? /* @__PURE__ */ jsx(CheckIcon, { className: "text-(--theme-success)" }) : icon
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: cx("min-w-0", truncate && "truncate"),
|
|
86
|
+
children
|
|
87
|
+
}),
|
|
88
|
+
copy && /* @__PURE__ */ jsxs("span", {
|
|
89
|
+
className: "sr-only",
|
|
90
|
+
children: [". ", t.copyThing(copy.label)]
|
|
91
|
+
})
|
|
92
|
+
] });
|
|
81
93
|
const alignClass = align === "start" ? "items-start" : "items-center";
|
|
82
|
-
|
|
94
|
+
const press = copy ? () => void write(copy.text, { label: copy.label }) : onClick;
|
|
95
|
+
if (press) return /* @__PURE__ */ jsx("button", {
|
|
83
96
|
type: "button",
|
|
84
|
-
title,
|
|
85
|
-
onClick,
|
|
97
|
+
title: title ?? (copy ? t.copyThing(copy.label) : void 0),
|
|
98
|
+
onClick: press,
|
|
86
99
|
className: cx(rowClasses, alignClass, "-mx-1 max-w-full rounded px-1 py-0.5 text-left transition-colors hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app)"),
|
|
87
100
|
children: content
|
|
88
101
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** What a filter's value is, which decides its picker and its conditions. */
|
|
3
|
+
export type FilterKind = 'enum' | 'people' | 'date' | 'text' | 'boolean';
|
|
4
|
+
/** How a filter compares its values with a row's. */
|
|
5
|
+
export type FilterOperator =
|
|
6
|
+
/** enum and people: "is A" or "is any of A, B"; date: a preset or a day ("is Last 7 days"). */
|
|
7
|
+
'is'
|
|
8
|
+
/** enum and people: "is not A" or "is none of A, B". */
|
|
9
|
+
| 'isNot'
|
|
10
|
+
/** enum on a field holding many values, such as labels: "includes all of A, B". */
|
|
11
|
+
| 'includesAll'
|
|
12
|
+
/** date: on or after the day. */
|
|
13
|
+
| 'after'
|
|
14
|
+
/** date: on or before the day. */
|
|
15
|
+
| 'before'
|
|
16
|
+
/** date: two days, both included. */
|
|
17
|
+
| 'between' | 'contains' | 'notContains'
|
|
18
|
+
/** Any kind: the row has a value at all ("has a due date"), with no values of its own. */
|
|
19
|
+
| 'isSet' | 'isNotSet';
|
|
20
|
+
export interface FilterOption {
|
|
21
|
+
value: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** A status dot, a label's color, an avatar. */
|
|
24
|
+
icon?: ReactNode;
|
|
25
|
+
/** A second line in the picker, such as a cycle's dates. */
|
|
26
|
+
description?: string | undefined;
|
|
27
|
+
/** How many rows hold it, shown at the end of its row in the picker. */
|
|
28
|
+
count?: number | undefined;
|
|
29
|
+
/** A heading the option sits under in the picker, such as People or Agents. */
|
|
30
|
+
group?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** A value a row holds for a field, for the matching a field does on its own. A date is a
|
|
33
|
+
* `Date`, an ISO string or epoch milliseconds. */
|
|
34
|
+
export type FilterRowValue = string | number | boolean | Date | readonly string[] | null | undefined;
|
|
35
|
+
export interface FilterField<T = never> {
|
|
36
|
+
/** The URL and storage key: stable, never renamed. */
|
|
37
|
+
key: string;
|
|
38
|
+
/** The chip's field part, such as "Status". */
|
|
39
|
+
label: string;
|
|
40
|
+
icon?: ReactNode;
|
|
41
|
+
kind: FilterKind;
|
|
42
|
+
/** The conditions offered, in order; the first is the one a new filter starts with. Defaults:
|
|
43
|
+
* enum and people `is`, `isNot`; date `is`, `after`, `before`, `between`; text `contains`,
|
|
44
|
+
* `notContains`; boolean `is`. Add `includesAll` or `isSet` here. */
|
|
45
|
+
operators?: readonly FilterOperator[] | undefined;
|
|
46
|
+
/** enum and people: the choices. */
|
|
47
|
+
options?: readonly FilterOption[] | undefined;
|
|
48
|
+
/** enum and people: searches on the server as the reader types. Its results show as given. */
|
|
49
|
+
loadOptions?: ((query: string, signal: AbortSignal) => Promise<readonly FilterOption[]>) | undefined;
|
|
50
|
+
/** Names a value no option holds any more, such as a deleted label in a saved link. */
|
|
51
|
+
valueLabel?: ((value: string) => string | undefined) | undefined;
|
|
52
|
+
/** enum and people: whether more than one value can be chosen (default true). */
|
|
53
|
+
multiple?: boolean | undefined;
|
|
54
|
+
/** Whether the picker has a search field. Default: more than eight options, or `loadOptions`. */
|
|
55
|
+
searchable?: boolean | undefined;
|
|
56
|
+
/** Always shows its chip, with no remove button, reading "Any" while unset; Clear leaves it. */
|
|
57
|
+
required?: boolean | undefined;
|
|
58
|
+
/** Always shows its chip, reading "Any" while unset; its remove button only empties it. */
|
|
59
|
+
pinned?: boolean | undefined;
|
|
60
|
+
/** One value across every page sharing the list's `sharedFiltersKey`, such as a namespace. */
|
|
61
|
+
shared?: boolean | undefined;
|
|
62
|
+
/** The row's value, so the list can match the filter itself for every condition. */
|
|
63
|
+
value?: ((row: T) => FilterRowValue) | undefined;
|
|
64
|
+
/** Whether a row passes, for matching `value` can't express. Omit both for a list filtered
|
|
65
|
+
* on the server. */
|
|
66
|
+
matches?: ((row: T, filter: ActiveFilter) => boolean) | undefined;
|
|
67
|
+
}
|
|
68
|
+
/** One filter in effect: plain data, so it can go in a URL, storage or a saved view. */
|
|
69
|
+
export interface ActiveFilter {
|
|
70
|
+
field: string;
|
|
71
|
+
op: FilterOperator;
|
|
72
|
+
/** Dates are `yyyy-MM-dd` days or relative presets (`today`, `-7d`, `month`), which stay
|
|
73
|
+
* relative wherever they are saved. A boolean is `true` or `false`. */
|
|
74
|
+
values: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
/** The relative date presets the date picker offers, as a filter stores them. */
|
|
77
|
+
export declare const DATE_PRESETS: readonly ['today', '-7d', '-30d', '-90d', 'month'];
|
|
78
|
+
export type DatePreset = (typeof DATE_PRESETS)[number];
|
|
79
|
+
type AnyField = FilterField<any>;
|
|
80
|
+
export declare function fieldOperators(field: AnyField): readonly FilterOperator[];
|
|
81
|
+
export declare function isMultiple(field: AnyField): boolean;
|
|
82
|
+
/** Whether a filter narrows anything yet: a value chosen, or a condition that needs none. */
|
|
83
|
+
export declare function isComplete(filter: ActiveFilter): boolean;
|
|
84
|
+
/** Whether two sets of filters say the same, ignoring order, as Save or Update buttons ask. */
|
|
85
|
+
export declare function sameFilters(a: readonly ActiveFilter[], b: readonly ActiveFilter[]): boolean;
|
|
86
|
+
/** What storage holds per field: a plain list for `is`, the shape every earlier version wrote
|
|
87
|
+
* and still reads, or the condition beside its values. Key order keeps the order added. */
|
|
88
|
+
export type StoredFilters = Record<string, readonly string[] | {
|
|
89
|
+
op: FilterOperator;
|
|
90
|
+
values: readonly string[];
|
|
91
|
+
}>;
|
|
92
|
+
export declare function fromStored(stored: unknown): ActiveFilter[];
|
|
93
|
+
export declare function toStored(filters: readonly ActiveFilter[]): StoredFilters;
|
|
94
|
+
/** The URL params a set of filters reads as: `status=started,done&label!=bug&due>=2026-10-01`.
|
|
95
|
+
* Values are not URL-encoded here; the router or `URLSearchParams` does that. */
|
|
96
|
+
export declare function encodeFilters(filters: readonly ActiveFilter[]): Record<string, string>;
|
|
97
|
+
type SearchInput = URLSearchParams | Record<string, unknown>;
|
|
98
|
+
/** The filters a URL's params name, for the given fields; `null` when it names none, so the
|
|
99
|
+
* remembered filters apply. A plain `key=value` is `is`, so links made before conditions
|
|
100
|
+
* existed keep working. Params of no field are left for the host. */
|
|
101
|
+
export declare function decodeFilters(search: SearchInput, fields: readonly AnyField[]): ActiveFilter[] | null;
|
|
102
|
+
/** Every param a field could use, so a host can drop stale ones before writing new. */
|
|
103
|
+
export declare function filterParamKeys(fields: readonly AnyField[]): string[];
|
|
104
|
+
/** A URL's params with the fields' params replaced by these filters, other params kept. */
|
|
105
|
+
export declare function withFilterParams<S extends Record<string, unknown>>(search: S, filters: readonly ActiveFilter[], fields: readonly AnyField[]): Record<string, unknown>;
|
|
106
|
+
/** A local day as `yyyy-MM-dd`. */
|
|
107
|
+
export declare const toDay: (date: Date) => string;
|
|
108
|
+
/** The local day a `yyyy-MM-dd` value names. */
|
|
109
|
+
export declare function parseDay(value: string | undefined): Date | undefined;
|
|
110
|
+
export declare function isDatePreset(value: string | undefined): value is DatePreset;
|
|
111
|
+
/** The span a date filter covers right now: `from` included, `to` excluded (the start of the
|
|
112
|
+
* day after the last), either open. Relative presets resolve against `now`. */
|
|
113
|
+
export declare function dateFilterRange(filter: ActiveFilter, now?: Date): {
|
|
114
|
+
from?: Date;
|
|
115
|
+
to?: Date;
|
|
116
|
+
} | undefined;
|
|
117
|
+
/** Whether a row passes one filter, through the field's `matches` or its `value`. A field
|
|
118
|
+
* with neither passes every row: the host filters it on the server. */
|
|
119
|
+
export declare function matchFilter<T>(field: FilterField<T>, filter: ActiveFilter, row: T, now?: Date): boolean;
|
|
120
|
+
/** Rows passing every complete filter. */
|
|
121
|
+
export declare function applyFilterList<T>(rows: readonly T[], filters: readonly ActiveFilter[], fields: readonly FilterField<T>[], now?: Date): T[];
|
|
122
|
+
export {};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
//#region src/list/filterModel.ts
|
|
2
|
+
/** The relative date presets the date picker offers, as a filter stores them. */
|
|
3
|
+
var DATE_PRESETS = [
|
|
4
|
+
"today",
|
|
5
|
+
"-7d",
|
|
6
|
+
"-30d",
|
|
7
|
+
"-90d",
|
|
8
|
+
"month"
|
|
9
|
+
];
|
|
10
|
+
var DEFAULT_OPERATORS = {
|
|
11
|
+
enum: ["is", "isNot"],
|
|
12
|
+
people: ["is", "isNot"],
|
|
13
|
+
date: [
|
|
14
|
+
"is",
|
|
15
|
+
"after",
|
|
16
|
+
"before",
|
|
17
|
+
"between"
|
|
18
|
+
],
|
|
19
|
+
text: ["contains", "notContains"],
|
|
20
|
+
boolean: ["is"]
|
|
21
|
+
};
|
|
22
|
+
function fieldOperators(field) {
|
|
23
|
+
return field.operators?.length ? field.operators : DEFAULT_OPERATORS[field.kind];
|
|
24
|
+
}
|
|
25
|
+
function isMultiple(field) {
|
|
26
|
+
return (field.kind === "enum" || field.kind === "people") && field.multiple !== false;
|
|
27
|
+
}
|
|
28
|
+
/** Whether a filter narrows anything yet: a value chosen, or a condition that needs none. */
|
|
29
|
+
function isComplete(filter) {
|
|
30
|
+
if (filter.op === "isSet" || filter.op === "isNotSet") return true;
|
|
31
|
+
if (filter.op === "between") return filter.values.length === 2 && filter.values.every(Boolean);
|
|
32
|
+
return filter.values.some(Boolean);
|
|
33
|
+
}
|
|
34
|
+
/** Whether two sets of filters say the same, ignoring order, as Save or Update buttons ask. */
|
|
35
|
+
function sameFilters(a, b) {
|
|
36
|
+
const norm = (list) => list.filter(isComplete).map((f) => `${f.field}\u0000${f.op}\u0000${[...f.values].sort().join("")}`).sort().join("");
|
|
37
|
+
return norm(a) === norm(b);
|
|
38
|
+
}
|
|
39
|
+
function fromStored(stored) {
|
|
40
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return [];
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const [field, entry] of Object.entries(stored)) if (Array.isArray(entry)) {
|
|
43
|
+
const values = entry.filter((v) => typeof v === "string");
|
|
44
|
+
if (values.length) out.push({
|
|
45
|
+
field,
|
|
46
|
+
op: "is",
|
|
47
|
+
values
|
|
48
|
+
});
|
|
49
|
+
} else if (typeof entry === "object" && entry !== null && typeof entry.op === "string") {
|
|
50
|
+
const values = Array.isArray(entry.values) ? entry.values.filter((v) => typeof v === "string") : [];
|
|
51
|
+
out.push({
|
|
52
|
+
field,
|
|
53
|
+
op: entry.op,
|
|
54
|
+
values
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function toStored(filters) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const f of filters) out[f.field] = f.op === "is" ? [...f.values] : {
|
|
62
|
+
op: f.op,
|
|
63
|
+
values: [...f.values]
|
|
64
|
+
};
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
var SUFFIX = {
|
|
68
|
+
is: "",
|
|
69
|
+
isNot: "!",
|
|
70
|
+
includesAll: "*",
|
|
71
|
+
after: ">",
|
|
72
|
+
before: "<",
|
|
73
|
+
between: "",
|
|
74
|
+
contains: "~",
|
|
75
|
+
notContains: "!~",
|
|
76
|
+
isSet: "",
|
|
77
|
+
isNotSet: "!"
|
|
78
|
+
};
|
|
79
|
+
var SET = "_set";
|
|
80
|
+
var escapeValue = (v) => v.replaceAll("%", "%25").replaceAll(",", "%2C");
|
|
81
|
+
var unescapeValue = (v) => v.replaceAll("%2C", ",").replaceAll("%25", "%");
|
|
82
|
+
var joinValues = (values) => values.map(escapeValue).join(",");
|
|
83
|
+
var splitValues = (raw) => raw === "" ? [] : raw.split(",").map(unescapeValue).filter(Boolean);
|
|
84
|
+
/** The URL params a set of filters reads as: `status=started,done&label!=bug&due>=2026-10-01`.
|
|
85
|
+
* Values are not URL-encoded here; the router or `URLSearchParams` does that. */
|
|
86
|
+
function encodeFilters(filters) {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const f of filters) {
|
|
89
|
+
if (!isComplete(f)) continue;
|
|
90
|
+
if (f.op === "isSet" || f.op === "isNotSet") out[`${f.field}${SUFFIX[f.op]}`] = SET;
|
|
91
|
+
else if (f.op === "between") {
|
|
92
|
+
out[`${f.field}>`] = escapeValue(f.values[0] ?? "");
|
|
93
|
+
out[`${f.field}<`] = escapeValue(f.values[1] ?? "");
|
|
94
|
+
} else out[`${f.field}${SUFFIX[f.op]}`] = joinValues(f.values);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function entriesOf(search) {
|
|
99
|
+
if (search instanceof URLSearchParams) return [...search.entries()];
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const [key, value] of Object.entries(search)) if (typeof value === "string") out.push([key, value]);
|
|
102
|
+
else if (typeof value === "number" || typeof value === "boolean") out.push([key, String(value)]);
|
|
103
|
+
else if (Array.isArray(value)) out.push([key, value.map((v) => escapeValue(String(v))).join(",")]);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** The filters a URL's params name, for the given fields; `null` when it names none, so the
|
|
107
|
+
* remembered filters apply. A plain `key=value` is `is`, so links made before conditions
|
|
108
|
+
* existed keep working. Params of no field are left for the host. */
|
|
109
|
+
function decodeFilters(search, fields) {
|
|
110
|
+
const byKey = new Map(fields.map((f) => [f.key, f]));
|
|
111
|
+
const out = [];
|
|
112
|
+
const dates = /* @__PURE__ */ new Map();
|
|
113
|
+
let found = false;
|
|
114
|
+
for (const [rawKey, raw] of entriesOf(search)) {
|
|
115
|
+
const suffix = [
|
|
116
|
+
"!~",
|
|
117
|
+
"!",
|
|
118
|
+
"*",
|
|
119
|
+
">",
|
|
120
|
+
"<",
|
|
121
|
+
"~"
|
|
122
|
+
].find((s) => rawKey.endsWith(s) && byKey.has(rawKey.slice(0, -s.length)));
|
|
123
|
+
const key = suffix ? rawKey.slice(0, -suffix.length) : rawKey;
|
|
124
|
+
const field = byKey.get(key);
|
|
125
|
+
if (!field) continue;
|
|
126
|
+
found = true;
|
|
127
|
+
if (raw === SET && (suffix === void 0 || suffix === "!")) {
|
|
128
|
+
out.push({
|
|
129
|
+
field: key,
|
|
130
|
+
op: suffix ? "isNotSet" : "isSet",
|
|
131
|
+
values: []
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (suffix === ">" || suffix === "<") {
|
|
136
|
+
const entry = dates.get(key) ?? {};
|
|
137
|
+
entry[suffix === ">" ? "after" : "before"] = unescapeValue(raw);
|
|
138
|
+
if (!dates.has(key)) {
|
|
139
|
+
dates.set(key, entry);
|
|
140
|
+
out.push({
|
|
141
|
+
field: key,
|
|
142
|
+
op: "after",
|
|
143
|
+
values: []
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const op = suffix === "!" ? "isNot" : suffix === "*" ? "includesAll" : suffix === "~" ? "contains" : suffix === "!~" ? "notContains" : field.kind === "text" ? "contains" : "is";
|
|
149
|
+
out.push({
|
|
150
|
+
field: key,
|
|
151
|
+
op,
|
|
152
|
+
values: splitValues(raw)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const resolved = out.map((f) => {
|
|
156
|
+
const range = dates.get(f.field);
|
|
157
|
+
if (!range || f.op !== "after" || f.values.length) return f;
|
|
158
|
+
if (range.after && range.before) return {
|
|
159
|
+
field: f.field,
|
|
160
|
+
op: "between",
|
|
161
|
+
values: [range.after, range.before]
|
|
162
|
+
};
|
|
163
|
+
return range.after ? {
|
|
164
|
+
field: f.field,
|
|
165
|
+
op: "after",
|
|
166
|
+
values: [range.after]
|
|
167
|
+
} : {
|
|
168
|
+
field: f.field,
|
|
169
|
+
op: "before",
|
|
170
|
+
values: [range.before ?? ""]
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
return found ? resolved : null;
|
|
174
|
+
}
|
|
175
|
+
/** Every param a field could use, so a host can drop stale ones before writing new. */
|
|
176
|
+
function filterParamKeys(fields) {
|
|
177
|
+
return fields.flatMap((f) => [
|
|
178
|
+
"",
|
|
179
|
+
"!",
|
|
180
|
+
"*",
|
|
181
|
+
">",
|
|
182
|
+
"<",
|
|
183
|
+
"~",
|
|
184
|
+
"!~"
|
|
185
|
+
].map((s) => `${f.key}${s}`));
|
|
186
|
+
}
|
|
187
|
+
/** A URL's params with the fields' params replaced by these filters, other params kept. */
|
|
188
|
+
function withFilterParams(search, filters, fields) {
|
|
189
|
+
const next = { ...search };
|
|
190
|
+
for (const key of filterParamKeys(fields)) delete next[key];
|
|
191
|
+
return {
|
|
192
|
+
...next,
|
|
193
|
+
...encodeFilters(filters)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
197
|
+
/** A local day as `yyyy-MM-dd`. */
|
|
198
|
+
var toDay = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
199
|
+
/** The local day a `yyyy-MM-dd` value names. */
|
|
200
|
+
function parseDay(value) {
|
|
201
|
+
const match = value?.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
202
|
+
if (!match) return;
|
|
203
|
+
const [, y, m, d] = match.map(Number);
|
|
204
|
+
const date = new Date(y, m - 1, d);
|
|
205
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
206
|
+
}
|
|
207
|
+
var startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
208
|
+
var addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
209
|
+
function isDatePreset(value) {
|
|
210
|
+
return DATE_PRESETS.includes(value ?? "") || /^-\d+d$/.test(value ?? "");
|
|
211
|
+
}
|
|
212
|
+
/** The days a preset or a day covers, as `[first, last]` local days, both included. */
|
|
213
|
+
function presetDays(value, now) {
|
|
214
|
+
const today = startOfDay(now);
|
|
215
|
+
if (value === "today") return [today, today];
|
|
216
|
+
if (value === "month") return [new Date(today.getFullYear(), today.getMonth(), 1), today];
|
|
217
|
+
const days = value.match(/^-(\d+)d$/);
|
|
218
|
+
if (days) return [addDays(today, 1 - Number(days[1])), today];
|
|
219
|
+
const day = parseDay(value);
|
|
220
|
+
return day ? [day, day] : void 0;
|
|
221
|
+
}
|
|
222
|
+
/** The span a date filter covers right now: `from` included, `to` excluded (the start of the
|
|
223
|
+
* day after the last), either open. Relative presets resolve against `now`. */
|
|
224
|
+
function dateFilterRange(filter, now = /* @__PURE__ */ new Date()) {
|
|
225
|
+
const [first, second] = filter.values;
|
|
226
|
+
const days = (v) => v ? presetDays(v, now) : void 0;
|
|
227
|
+
switch (filter.op) {
|
|
228
|
+
case "is": {
|
|
229
|
+
const span = days(first);
|
|
230
|
+
return span && {
|
|
231
|
+
from: span[0],
|
|
232
|
+
to: addDays(span[1], 1)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
case "after": {
|
|
236
|
+
const span = days(first);
|
|
237
|
+
return span && { from: span[0] };
|
|
238
|
+
}
|
|
239
|
+
case "before": {
|
|
240
|
+
const span = days(first);
|
|
241
|
+
return span && { to: addDays(span[1], 1) };
|
|
242
|
+
}
|
|
243
|
+
case "between": {
|
|
244
|
+
const a = days(first);
|
|
245
|
+
const b = days(second);
|
|
246
|
+
return a && b ? {
|
|
247
|
+
from: a[0],
|
|
248
|
+
to: addDays(b[1], 1)
|
|
249
|
+
} : void 0;
|
|
250
|
+
}
|
|
251
|
+
default: return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
var isEmpty = (v) => v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0;
|
|
255
|
+
var asList = (v) => isEmpty(v) ? [] : Array.isArray(v) ? [...v] : [String(v)];
|
|
256
|
+
function asTime(v) {
|
|
257
|
+
if (v instanceof Date) return v.getTime();
|
|
258
|
+
if (typeof v === "number") return v;
|
|
259
|
+
if (typeof v === "string" && v) {
|
|
260
|
+
const day = parseDay(v);
|
|
261
|
+
const t = day ? day.getTime() : Date.parse(v);
|
|
262
|
+
return Number.isNaN(t) ? void 0 : t;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Whether a row passes one filter, through the field's `matches` or its `value`. A field
|
|
266
|
+
* with neither passes every row: the host filters it on the server. */
|
|
267
|
+
function matchFilter(field, filter, row, now) {
|
|
268
|
+
if (field.matches) return field.matches(row, filter);
|
|
269
|
+
if (!field.value) return true;
|
|
270
|
+
const v = field.value(row);
|
|
271
|
+
switch (filter.op) {
|
|
272
|
+
case "isSet": return !isEmpty(v);
|
|
273
|
+
case "isNotSet": return isEmpty(v);
|
|
274
|
+
case "contains":
|
|
275
|
+
case "notContains": {
|
|
276
|
+
const needle = (filter.values[0] ?? "").toLocaleLowerCase();
|
|
277
|
+
const has = asList(v).some((s) => s.toLocaleLowerCase().includes(needle));
|
|
278
|
+
return filter.op === "contains" ? has : !has;
|
|
279
|
+
}
|
|
280
|
+
case "includesAll": {
|
|
281
|
+
const held = asList(v);
|
|
282
|
+
return filter.values.every((x) => held.includes(x));
|
|
283
|
+
}
|
|
284
|
+
case "isNot": return !asList(v).some((x) => filter.values.includes(x));
|
|
285
|
+
}
|
|
286
|
+
if (field.kind === "date") {
|
|
287
|
+
const range = dateFilterRange(filter, now);
|
|
288
|
+
const t = asTime(v);
|
|
289
|
+
if (!range || t === void 0) return range === void 0;
|
|
290
|
+
return (range.from === void 0 || t >= range.from.getTime()) && (range.to === void 0 || t < range.to.getTime());
|
|
291
|
+
}
|
|
292
|
+
if (field.kind === "boolean") return String(Boolean(v)) === (filter.values[0] ?? "true");
|
|
293
|
+
return asList(v).some((x) => filter.values.includes(x));
|
|
294
|
+
}
|
|
295
|
+
/** Rows passing every complete filter. */
|
|
296
|
+
function applyFilterList(rows, filters, fields, now = /* @__PURE__ */ new Date()) {
|
|
297
|
+
const checks = filters.flatMap((filter) => {
|
|
298
|
+
const field = fields.find((f) => f.key === filter.field);
|
|
299
|
+
return field && isComplete(filter) ? [{
|
|
300
|
+
field,
|
|
301
|
+
filter
|
|
302
|
+
}] : [];
|
|
303
|
+
});
|
|
304
|
+
if (checks.length === 0) return [...rows];
|
|
305
|
+
return rows.filter((row) => checks.every(({ field, filter }) => matchFilter(field, filter, row, now)));
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
export { DATE_PRESETS, applyFilterList, dateFilterRange, decodeFilters, encodeFilters, fieldOperators, filterParamKeys, fromStored, isComplete, isDatePreset, isMultiple, matchFilter, parseDay, sameFilters, toDay, toStored, withFilterParams };
|
package/dist/list/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export { ActionToolbar, type ToolbarAction, ToolbarButton, type ToolbarItem, } from './ActionToolbar.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { type ActiveFilter, applyFilterList, DATE_PRESETS, type DatePreset, dateFilterRange, decodeFilters, encodeFilters, type FilterField, type FilterKind, type FilterOperator, type FilterOption, type FilterRowValue, filterParamKeys, matchFilter, sameFilters, withFilterParams, } from './filterModel.js';
|
|
3
|
+
export { AddFilterMenu, type AddFilterMenuProps, FilterChip, ListFilterBar, type ListFilterBarProps, } from './ListFilterBar.js';
|
|
4
|
+
export { ListColumns, ListPage, type ListPageEmpty, type ListPageProps, ListRow, ListRowActionsProvider, useListNavigate, } from './ListPage.js';
|
|
5
|
+
export { actionsColumnWidth, GroupRow, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, listTableFixedProps, listTableProps, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, } from './ListTable.js';
|
|
6
|
+
export { ListToolbar, type ListToolbarProps } from './ListToolbar.js';
|
|
4
7
|
export { ListDisplayMenu, ListFilterMenu } from './ListViewControls.js';
|
|
5
8
|
export { ListCount, type ListSearch, ListSearchControl, useListSearch, } from './listSearch.js';
|
|
6
9
|
export { type ActionDef, type ColumnDef, DEFAULT_ORDER, FAVORITE_ORDER, type FilterFacet, type ListView, type ListViewChrome, type ListViewConfig, type OrderByOption, useListView, } from './listView.js';
|
|
7
10
|
export { assembleRowMenu, type CopyFields, type CopySubmenu, MoreButton, type RowAction, rowContextMenuProps, useCopySubmenu, useRowMenuActive, } from './rowMenu.js';
|
|
8
11
|
export { HoverCardRow, UserHoverCard, type UserHoverCardProps, } from './UserHoverCard.js';
|
|
12
|
+
export { type FilterStore, type ListFilters, type ListFiltersConfig, useListFilters, } from './useListFilters.js';
|
package/dist/list/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { ActionToolbar, ToolbarButton } from "./ActionToolbar.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DATE_PRESETS, applyFilterList, dateFilterRange, decodeFilters, encodeFilters, filterParamKeys, matchFilter, sameFilters, withFilterParams } from "./filterModel.js";
|
|
3
|
+
import { AddFilterMenu, FilterChip, ListFilterBar } from "./ListFilterBar.js";
|
|
4
|
+
import { GroupRow, ListActionsHeader, ListCellText, ListError, ListSelectAllHeader, ListSkeleton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, actionsColumnWidth, listTableFixedProps, listTableProps } from "./ListTable.js";
|
|
3
5
|
import { MoreButton, assembleRowMenu, rowContextMenuProps, useCopySubmenu, useRowMenuActive } from "./rowMenu.js";
|
|
4
6
|
import { ListColumns, ListPage, ListRow, ListRowActionsProvider, useListNavigate } from "./ListPage.js";
|
|
5
7
|
import { ListDisplayMenu, ListFilterMenu } from "./ListViewControls.js";
|
|
6
8
|
import { ListCount, ListSearchControl, useListSearch } from "./listSearch.js";
|
|
9
|
+
import { ListToolbar } from "./ListToolbar.js";
|
|
10
|
+
import { useListFilters } from "./useListFilters.js";
|
|
7
11
|
import { DEFAULT_ORDER, FAVORITE_ORDER, useListView } from "./listView.js";
|
|
8
12
|
import { HoverCardRow, UserHoverCard } from "./UserHoverCard.js";
|
|
9
|
-
export { ActionToolbar, DEFAULT_ORDER, FAVORITE_ORDER, GroupRow, HoverCardRow, ListActionsHeader, ListColumns, ListCount, ListDisplayMenu, ListError, ListFilterMenu, ListPage, ListRow, ListRowActionsProvider, ListSearchControl, ListSelectAllHeader, ListSkeleton, MoreButton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, ToolbarButton, UserHoverCard, actionsColumnWidth, assembleRowMenu, listTableFixedProps, listTableProps, rowContextMenuProps, useCopySubmenu, useListNavigate, useListSearch, useListView, useRowMenuActive };
|
|
13
|
+
export { ActionToolbar, AddFilterMenu, DATE_PRESETS, DEFAULT_ORDER, FAVORITE_ORDER, FilterChip, GroupRow, HoverCardRow, ListActionsHeader, ListCellText, ListColumns, ListCount, ListDisplayMenu, ListError, ListFilterBar, ListFilterMenu, ListPage, ListRow, ListRowActionsProvider, ListSearchControl, ListSelectAllHeader, ListSkeleton, ListToolbar, MoreButton, NAME_MIN_WIDTH_PX, NameCell, RowSelectCheckbox, ToolbarButton, UserHoverCard, actionsColumnWidth, applyFilterList, assembleRowMenu, dateFilterRange, decodeFilters, encodeFilters, filterParamKeys, listTableFixedProps, listTableProps, matchFilter, rowContextMenuProps, sameFilters, useCopySubmenu, useListFilters, useListNavigate, useListSearch, useListView, useRowMenuActive, withFilterParams };
|
|
@@ -16,7 +16,7 @@ export type ListSearch = ReturnType<typeof useListSearch>;
|
|
|
16
16
|
export declare function ListSearchControl({ search, placeholder, label: labelProp, }: {
|
|
17
17
|
search: ListSearch;
|
|
18
18
|
placeholder: string;
|
|
19
|
-
label?: string;
|
|
19
|
+
label?: string | undefined;
|
|
20
20
|
}): import("react").JSX.Element;
|
|
21
21
|
export declare function ListCount({ total, matches, filterActive, noun, nounPlural, }: {
|
|
22
22
|
total: number;
|
package/dist/list/listSearch.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { buttonClasses } from "../components/buttonClasses.js";
|
|
2
2
|
import { CloseIcon, RemoveIcon, SearchIcon } from "../icons.js";
|
|
3
|
+
import { ariaKeyShortcuts } from "../components/Kbd.js";
|
|
3
4
|
import { Tooltip } from "../components/Tooltip.js";
|
|
4
|
-
import { usePersistedState, useStrings } from "../components/Provider.js";
|
|
5
|
+
import { useLocale, usePersistedState, useStrings } from "../components/Provider.js";
|
|
5
6
|
import { isTextEntryContext } from "../components/shortcutKey.js";
|
|
6
|
-
import {
|
|
7
|
+
import { formatInteger } from "../format.js";
|
|
7
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
10
|
//#region src/list/listSearch.tsx
|
|
9
11
|
var NO_TERMS = [];
|
|
10
|
-
var COUNT = new Intl.NumberFormat();
|
|
11
12
|
/** Pass `enabled: false` to suspend the global ⌘F/`/` hijack (e.g. when search isn't shown and browser Find should stay active). */
|
|
12
13
|
function useListSearch(enabled = true, storageKey) {
|
|
13
14
|
const [filter, setFilter] = useState("");
|
|
@@ -122,6 +123,7 @@ function ListSearchControl({ search, placeholder, label: labelProp }) {
|
|
|
122
123
|
type: "button",
|
|
123
124
|
onClick: search.openSearch,
|
|
124
125
|
"aria-label": label,
|
|
126
|
+
"aria-keyshortcuts": `/ ${ariaKeyShortcuts("Mod+F")}`,
|
|
125
127
|
className: buttonClasses({
|
|
126
128
|
variant: "outline",
|
|
127
129
|
size: "sm",
|
|
@@ -133,12 +135,14 @@ function ListSearchControl({ search, placeholder, label: labelProp }) {
|
|
|
133
135
|
}
|
|
134
136
|
function ListCount({ total, matches, filterActive, noun, nounPlural }) {
|
|
135
137
|
const { list: t } = useStrings();
|
|
138
|
+
const locale = useLocale();
|
|
139
|
+
const count = (n) => formatInteger(n, locale);
|
|
136
140
|
return /* @__PURE__ */ jsx("div", {
|
|
137
141
|
className: "flex items-center h-8",
|
|
138
142
|
children: /* @__PURE__ */ jsx("span", {
|
|
139
143
|
role: "status",
|
|
140
144
|
className: "text-[13px] text-(--theme-muted-text-color)",
|
|
141
|
-
children: filterActive ? t.matchCount(
|
|
145
|
+
children: filterActive ? t.matchCount(count(matches), count(total), nounPlural) : t.count(count(total), total === 1 ? noun : nounPlural)
|
|
142
146
|
})
|
|
143
147
|
});
|
|
144
148
|
}
|
package/dist/list/listView.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ActiveFilter, FilterField } from './filterModel.js';
|
|
3
|
+
import { type FilterStore, type ListFilters } from './useListFilters.js';
|
|
2
4
|
/** A toggleable column. Rendering stays in the page's row component, gated by
|
|
3
5
|
* {@link ListView.isVisible}; this is just the header metadata + visibility. */
|
|
4
6
|
export interface ColumnDef {
|
|
@@ -23,6 +25,9 @@ export interface ColumnDef {
|
|
|
23
25
|
* then sorts the list: it shows, and sets, the same order as Display
|
|
24
26
|
* options' Ordering. */
|
|
25
27
|
orderBy?: string;
|
|
28
|
+
/** `end` sets the header and the cells against the column's far edge, for
|
|
29
|
+
* figures, sizes and times that compare best lined up on their last digit. */
|
|
30
|
+
align?: 'start' | 'end';
|
|
26
31
|
}
|
|
27
32
|
/** Column responsive importance; `low` drops before `medium`. See {@link ColumnDef.priority}. */
|
|
28
33
|
type ColumnPriority = 'low' | 'medium';
|
|
@@ -37,7 +42,9 @@ export interface OrderByOption<T> {
|
|
|
37
42
|
compare: (a: T, b: T) => number;
|
|
38
43
|
}
|
|
39
44
|
type OrderDir = 'asc' | 'desc';
|
|
40
|
-
/**
|
|
45
|
+
/** A multi-choice filter: shorthand for an `enum` {@link FilterField} offering "is" and "is not".
|
|
46
|
+
* `matches` decides whether a row passes given the values chosen ("is not" passes the rows it
|
|
47
|
+
* turns away). */
|
|
41
48
|
export interface FilterFacet<T> {
|
|
42
49
|
key: string;
|
|
43
50
|
label: string;
|
|
@@ -66,6 +73,16 @@ export interface ListViewConfig<T> {
|
|
|
66
73
|
orderBys?: OrderByOption<T>[];
|
|
67
74
|
defaultOrderBy?: string;
|
|
68
75
|
facets?: FilterFacet<T>[];
|
|
76
|
+
/** Filters beyond facets: people, dates, text, yes-or-no, with conditions. They follow the
|
|
77
|
+
* facets in the Filter menu. */
|
|
78
|
+
filterFields?: FilterField<T>[] | undefined;
|
|
79
|
+
/** What the page shows before the reader changes anything; Reset returns to them. */
|
|
80
|
+
defaultFilters?: ActiveFilter[] | undefined;
|
|
81
|
+
/** Filters held outside the list too, such as the URL: its filters win while it has any, and
|
|
82
|
+
* changes go to both. See `decodeFilters` and `encodeFilters`. */
|
|
83
|
+
filterStore?: FilterStore | undefined;
|
|
84
|
+
/** False leaves remembering the filters to `filterStore` alone. */
|
|
85
|
+
rememberFilters?: boolean | undefined;
|
|
69
86
|
actions?: ActionDef[];
|
|
70
87
|
/** Explicit pinned-button count when not using actions+isPinned; sizes the actions column. */
|
|
71
88
|
actionCount?: number;
|
|
@@ -96,6 +113,8 @@ export interface ListViewChrome {
|
|
|
96
113
|
/** Per visible column, whether it stacks under the first on a narrow
|
|
97
114
|
* container ({@link ColumnDef.stack}); `ListRow` reads entry `i`. */
|
|
98
115
|
columnStacks: boolean[];
|
|
116
|
+
/** The list's filters, which `ListPage` shows as a row of chips under its toolbar. */
|
|
117
|
+
filterState?: ListFilters | undefined;
|
|
99
118
|
}
|
|
100
119
|
export interface ListView<T> extends ListViewChrome {
|
|
101
120
|
columns: ColumnDef[];
|
|
@@ -122,8 +141,11 @@ export interface ListView<T> extends ListViewChrome {
|
|
|
122
141
|
sortBy: (value: string) => void;
|
|
123
142
|
applyOrder: (rows: T[]) => T[];
|
|
124
143
|
facets: FilterFacet<T>[];
|
|
144
|
+
filterState: ListFilters;
|
|
145
|
+
/** The values of each filter that reads "is"; `filterState.active` has every filter. */
|
|
125
146
|
filters: Record<string, string[]>;
|
|
126
147
|
setFacetValues: (key: string, values: string[]) => void;
|
|
148
|
+
/** Empties every filter but the required ones. */
|
|
127
149
|
clearFilters: () => void;
|
|
128
150
|
activeFilterCount: number;
|
|
129
151
|
applyFilters: (rows: T[]) => T[];
|
|
@@ -145,5 +167,5 @@ export interface ListView<T> extends ListViewChrome {
|
|
|
145
167
|
}
|
|
146
168
|
/** Persisted grouping + column-visibility + field-filter state for a list page,
|
|
147
169
|
* driving the shared Display/Filter menus. */
|
|
148
|
-
export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
|
|
170
|
+
export declare function useListView<T>({ storageKey, sharedFiltersKey, columns, groupBys, defaultGroupBy, orderBys, defaultOrderBy, facets, filterFields, defaultFilters, filterStore, rememberFilters, actions, actionCount, defaultShowColumnHeaders, defaultShowActions, isFavorite, isOn, createdAt, }: ListViewConfig<T>): ListView<T>;
|
|
149
171
|
export {};
|