@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.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/dist/app/action-form-body.js +10 -2
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/base-path-context.d.ts +9 -0
- package/dist/app/base-path-context.js +13 -0
- package/dist/app/base-path.d.ts +3 -0
- package/dist/app/base-path.js +27 -0
- package/dist/app/config-edit-body.d.ts +1 -1
- package/dist/app/config-edit-body.js +19 -6
- package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
- package/dist/app/config-edit-without-secret-fields.js +28 -0
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +9 -4
- package/dist/app/entity-list-facet-specs.d.ts +3 -0
- package/dist/app/entity-list-facet-specs.js +65 -0
- package/dist/app/entity-list-screen.js +74 -206
- package/dist/app/extension-sections.d.ts +5 -3
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- package/dist/app/kumiko-screen.js +6 -14
- package/dist/app/list-bulk-actions.d.ts +18 -0
- package/dist/app/list-bulk-actions.js +54 -0
- package/dist/app/locked-screen-header.d.ts +9 -0
- package/dist/app/locked-screen-header.js +33 -0
- package/dist/app/projection-list-body.js +51 -46
- package/dist/app/row-actions.d.ts +3 -0
- package/dist/app/row-actions.js +10 -5
- package/dist/app/screen-shared.js +9 -2
- package/dist/app/use-entity-range-facets.d.ts +24 -0
- package/dist/app/use-entity-range-facets.js +64 -0
- package/dist/app/use-projection-date-range.d.ts +12 -0
- package/dist/app/use-projection-date-range.js +21 -0
- package/dist/app/use-screen-list-rows.d.ts +37 -0
- package/dist/app/use-screen-list-rows.js +104 -0
- package/dist/app/use-screen-list-table.d.ts +24 -0
- package/dist/app/use-screen-list-table.js +46 -0
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +1 -1
- package/dist/components/list-group-page-bridges.d.ts +3 -0
- package/dist/components/list-group-page-bridges.js +31 -0
- package/dist/components/list-state-store.d.ts +16 -0
- package/dist/components/list-state-store.js +31 -0
- package/dist/components/related-list-section.js +51 -90
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit.js +11 -8
- package/dist/components/render-field.js +11 -7
- package/dist/components/render-list-toolbar-action.d.ts +27 -0
- package/dist/components/render-list-toolbar-action.js +40 -0
- package/dist/components/render-list.d.ts +13 -29
- package/dist/components/render-list.js +72 -67
- package/dist/current-user.d.ts +2 -0
- package/dist/data-table-aggregate-types.d.ts +47 -0
- package/dist/data-table-aggregate-types.js +1 -0
- package/dist/data-table-bulk-types.d.ts +27 -0
- package/dist/data-table-bulk-types.js +1 -0
- package/dist/data-table-facet-types.d.ts +36 -0
- package/dist/data-table-facet-types.js +1 -0
- package/dist/data-table-view-types.d.ts +73 -0
- package/dist/data-table-view-types.js +1 -0
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/form/form-controller.js +8 -0
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/view-model/edit.js +5 -1
- package/dist/headless/view-model/list.js +184 -149
- package/dist/headless/view-model/types.d.ts +7 -2
- package/dist/hooks/list-column-layout.d.ts +19 -0
- package/dist/hooks/list-column-layout.js +36 -0
- package/dist/hooks/list-group-pages.d.ts +26 -0
- package/dist/hooks/list-group-pages.js +37 -0
- package/dist/hooks/list-reset.d.ts +8 -0
- package/dist/hooks/list-reset.js +15 -0
- package/dist/hooks/list-server-groups.d.ts +39 -0
- package/dist/hooks/list-server-groups.js +84 -0
- package/dist/hooks/list-state-schema.d.ts +28 -0
- package/dist/hooks/list-state-schema.js +76 -0
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/use-bulk-run.d.ts +41 -0
- package/dist/hooks/use-bulk-run.js +65 -0
- package/dist/hooks/use-group-load-state.d.ts +11 -0
- package/dist/hooks/use-group-load-state.js +31 -0
- package/dist/hooks/use-list-aggregates.d.ts +38 -0
- package/dist/hooks/use-list-aggregates.js +107 -0
- package/dist/hooks/use-list-table.d.ts +38 -0
- package/dist/hooks/use-list-table.js +91 -0
- package/dist/hooks/use-list-view-state.d.ts +43 -0
- package/dist/hooks/use-list-view-state.js +161 -0
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +17 -3
- package/dist/hooks/use-related-list-rows.d.ts +27 -0
- package/dist/hooks/use-related-list-rows.js +81 -0
- package/dist/hooks/use-related-list-state.d.ts +15 -0
- package/dist/hooks/use-related-list-state.js +31 -0
- package/dist/hooks/use-remembered-list-params.d.ts +13 -0
- package/dist/hooks/use-remembered-list-params.js +53 -0
- package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
- package/dist/hooks/use-remembered-local-list-state.js +63 -0
- package/dist/i18n-defaults.js +59 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +30 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +15 -7
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/history-leave-guard.d.ts +6 -0
- package/dist/web/app/history-leave-guard.js +115 -0
- package/dist/web/app/list-state-store-boot.d.ts +6 -0
- package/dist/web/app/list-state-store-boot.js +15 -0
- package/dist/web/app/nav.js +7 -54
- package/dist/web/app/session-list-state-store.d.ts +15 -0
- package/dist/web/app/session-list-state-store.js +117 -0
- package/dist/web/index.d.ts +2 -1
- package/dist/web/index.js +2 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/lib/export-xlsx-request.d.ts +11 -0
- package/dist/web/lib/export-xlsx-request.js +46 -0
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.js +1 -1
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
- package/dist/web/primitives/data-table/actions-column.js +34 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
- package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
- package/dist/web/primitives/data-table/card-sort-select.js +40 -0
- package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
- package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
- package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
- package/dist/web/primitives/data-table/column-picker-section.js +35 -0
- package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
- package/dist/web/primitives/data-table/density.d.ts +14 -0
- package/dist/web/primitives/data-table/density.js +20 -0
- package/dist/web/primitives/data-table/export-button.d.ts +5 -0
- package/dist/web/primitives/data-table/export-button.js +37 -0
- package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
- package/dist/web/primitives/data-table/facet-cluster.js +37 -0
- package/dist/web/primitives/data-table/group-band.d.ts +28 -0
- package/dist/web/primitives/data-table/group-band.js +53 -0
- package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
- package/dist/web/primitives/data-table/measure-value.js +28 -0
- package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
- package/dist/web/primitives/data-table/pin-section.js +20 -0
- package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
- package/dist/web/primitives/data-table/reset-all-button.js +8 -0
- package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
- package/dist/web/primitives/data-table/server-group-header.js +51 -0
- package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
- package/dist/web/primitives/data-table/sortable-header.js +41 -0
- package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
- package/dist/web/primitives/data-table/table-chrome.js +30 -0
- package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
- package/dist/web/primitives/data-table/table-footer.js +20 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
- package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
- package/dist/web/primitives/data-table/totals-footer.js +48 -0
- package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
- package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
- package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
- package/dist/web/primitives/data-table/use-column-pins.js +117 -0
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
- package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
- package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
- package/dist/web/primitives/data-table/use-row-selection.js +48 -0
- package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
- package/dist/web/primitives/data-table/use-view-columns.js +17 -0
- package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
- package/dist/web/primitives/data-table/view-menu.js +45 -0
- package/dist/web/primitives/data-table-cell.d.ts +0 -22
- package/dist/web/primitives/data-table-cell.js +1 -58
- package/dist/web/primitives/data-table.d.ts +1 -1
- package/dist/web/primitives/data-table.js +125 -146
- package/dist/web/primitives/facet-filter.js +1 -1
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/file-value-list.d.ts +1 -1
- package/dist/web/primitives/file-value-list.js +5 -5
- package/dist/web/primitives/input.js +6 -1
- package/dist/web/primitives/layout-elements.js +6 -4
- package/dist/web/primitives/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/row-actions.js +3 -3
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +49 -21
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.js +1 -1
- package/dist/web/widgets/detail-list.d.ts +5 -1
- package/dist/web/widgets/detail-list.js +5 -1
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.js +10 -5
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/upload-zone.js +13 -5
- package/package.json +5 -5
- package/src/changes.json +158 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cardColumnRoles } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import {
|
|
4
|
-
import { Fragment, useId, useState
|
|
3
|
+
import { ChevronRight, Search, SlidersHorizontal } from "lucide-react";
|
|
4
|
+
import { Fragment, useId, useState } from "react";
|
|
5
5
|
import { useOptionalLocale, useOptionalTranslation } from "../../index.js";
|
|
6
|
+
import { UNIFORM_BAR_CONTROLS_CLASSES } from "../layout/uniform-bar-controls.js";
|
|
6
7
|
import { cn } from "../lib/cn.js";
|
|
7
8
|
import { Button as UiButton } from "../ui/button.js";
|
|
8
9
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
@@ -10,54 +11,28 @@ import { CardLeadingImage } from "./file-thumbnail.js";
|
|
|
10
11
|
import { screenPaddingClassName } from "./layout.js";
|
|
11
12
|
import { useIsNarrowViewport } from "./use-narrow-viewport.js";
|
|
12
13
|
import "../react-css-custom-properties.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { ActionsBodyCell, ActionsHeaderCell } from "./data-table/actions-column.js";
|
|
15
|
+
import { CardSortSelect } from "./data-table/card-sort-select.js";
|
|
16
|
+
import { isPinningActive, isResizingActive } from "./data-table/column-layout-mode.js";
|
|
17
|
+
import { ColumnResizeHandle, ColumnWidthGroup } from "./data-table/column-resize-handle.js";
|
|
18
|
+
import { densityClasses } from "./data-table/density.js";
|
|
19
|
+
import { buildFacetCluster } from "./data-table/facet-cluster.js";
|
|
20
|
+
import { bandClassName, bandReactKey, bandTestId, buildDataTableItems, GroupBand, } from "./data-table/group-band.js";
|
|
21
|
+
import { SortableHeader } from "./data-table/sortable-header.js";
|
|
22
|
+
import { EXPAND_TOGGLE_CELL_CLASS, EXPANSION_CELL_CLASS, EXPANSION_CONTENT_CLASS, FILL_TABLE_CLASS, FlushTable, RowExpandToggle, } from "./data-table/table-chrome.js";
|
|
23
|
+
import { DataTableFooter, showFullTextOnHover } from "./data-table/table-footer.js";
|
|
24
|
+
import { ToolbarEndCluster, toolbarEndHasContent } from "./data-table/toolbar-end-cluster.js";
|
|
25
|
+
import { TotalsCard, TotalsFooterRow } from "./data-table/totals-footer.js";
|
|
26
|
+
import { useBulkSelection } from "./data-table/use-bulk-selection.js";
|
|
27
|
+
import { EXPAND_COLUMN_KEY, SELECT_COLUMN_KEY, useColumnPins, } from "./data-table/use-column-pins.js";
|
|
28
|
+
import { useColumnSizing } from "./data-table/use-column-sizing.js";
|
|
29
|
+
import { useMeasuredColumnWidths } from "./data-table/use-measured-column-widths.js";
|
|
30
|
+
import { useRowKeyboardNavigation } from "./data-table/use-row-keyboard-navigation.js";
|
|
31
|
+
import { useViewColumns } from "./data-table/use-view-columns.js";
|
|
32
|
+
import { cellLinkProp, cellTitle, DataTableCell, FIT_CONTENT_CELL_CLASS, FirstCellLink, isBadgeColumn, isEmptyCellValue, isUnlabeledFalse, NUMERIC_COLUMN_TYPES, rowToneClass, showsBareCheckMark, TABULAR_COLUMN_TYPES, } from "./data-table-cell.js";
|
|
33
|
+
import { InfiniteSentinel, Pager } from "./pager.js";
|
|
16
34
|
import { RowActionsCell } from "./row-actions.js";
|
|
17
|
-
|
|
18
|
-
// would become the scrollport of a sticky header. A scrollBody table scrolls
|
|
19
|
-
// in its parent instead, so it renders the bare <table>.
|
|
20
|
-
function FlushTable({ className, ...props }) {
|
|
21
|
-
return (_jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }));
|
|
22
|
-
}
|
|
23
|
-
// Header cells stick to the scroll surface; the bottom rule is an inset
|
|
24
|
-
// shadow because a border on a sticky cell scrolls away under border-collapse.
|
|
25
|
-
// Column weight (medium vs. semibold for the sorted one) lives on the th itself
|
|
26
|
-
// in SortableHeader: a `[&_th]:font-*` rule here would outrank it.
|
|
27
|
-
// Child selectors only: an expanded row nests a whole table inside a cell, and
|
|
28
|
-
// descendant selectors would make its header sticky and re-pad its cells.
|
|
29
|
-
const FILL_TABLE_CLASS = cn("[&>thead>tr>th]:sticky [&>thead>tr>th]:top-0 [&>thead>tr>th]:z-10 [&>thead>tr>th]:h-9 [&>thead>tr>th]:bg-muted [&>thead>tr>th]:px-2 [&>thead>tr>th]:text-[13px]", "[&>thead>tr>th]:shadow-[inset_0_-1px_0_var(--color-border)] [&>thead>tr>th_button]:text-[13px] [&>tbody>tr>td]:px-2 [&>tbody>tr>td]:py-0", "[&>*>tr>:first-child]:pl-6 [&>*>tr>:last-child]:pr-6");
|
|
30
|
-
// `!`: beats the first/last-child padding of FILL_TABLE_CLASS, which would
|
|
31
|
-
// otherwise widen the narrow toggle column / pad the full-width expansion cell.
|
|
32
|
-
// The nested table's sticky actions cell (last td) gets the muted surface
|
|
33
|
-
// instead of its own md:bg-background, so it blends into the area.
|
|
34
|
-
const EXPAND_TOGGLE_CELL_CLASS = "w-10 px-1! py-0!";
|
|
35
|
-
const EXPANSION_CELL_CLASS = "whitespace-normal bg-muted p-0! align-top md:[&_td:last-child]:bg-muted";
|
|
36
|
-
const EXPANSION_CONTENT_CLASS = "pt-1 pr-4 pb-3.5 pl-[52px]";
|
|
37
|
-
function RowExpandToggle({ expanded, controlsId, label, onToggle, testId, }) {
|
|
38
|
-
return (_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0 max-md:size-11", "aria-expanded": expanded, ...(expanded && { "aria-controls": controlsId }), "aria-label": label, "data-testid": testId, onClick: (e) => {
|
|
39
|
-
e.stopPropagation();
|
|
40
|
-
onToggle();
|
|
41
|
-
}, onKeyDown: (e) => e.stopPropagation(), children: _jsx(ChevronRight, { className: cn("size-4 transition-transform", expanded && "rotate-90"), "aria-hidden": "true" }) }));
|
|
42
|
-
}
|
|
43
|
-
function DataTableFooter({ count, itemNoun, testId, }) {
|
|
44
|
-
const t = useOptionalTranslation();
|
|
45
|
-
const noun = itemNoun?.(count);
|
|
46
|
-
const label = noun !== undefined
|
|
47
|
-
? (t?.("kumiko.list.count.noun", { count: count.toLocaleString(), noun }) ??
|
|
48
|
-
`${count.toLocaleString()} ${noun}`)
|
|
49
|
-
: count === 1
|
|
50
|
-
? (t?.("kumiko.list.count.one") ?? "1 entry")
|
|
51
|
-
: (t?.("kumiko.list.count.other", { count: count.toLocaleString() }) ??
|
|
52
|
-
`${count.toLocaleString()} entries`);
|
|
53
|
-
return (_jsx("div", { "data-testid": testId, className: TABLE_FOOTER_BAR_CLASS, children: _jsx("span", { "data-testid": `${testId}-count`, children: label }) }));
|
|
54
|
-
}
|
|
55
|
-
// Cell content is a ReactNode, so the tooltip text is read from the rendered DOM.
|
|
56
|
-
function showFullTextOnHover(event) {
|
|
57
|
-
const element = event.currentTarget;
|
|
58
|
-
element.title = element.scrollWidth > element.clientWidth ? (element.textContent ?? "") : "";
|
|
59
|
-
}
|
|
60
|
-
export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, rowGrouping, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, }) {
|
|
35
|
+
export function DefaultDataTable({ columns: schemaColumns, rows, onRowClick, sort, onSortChange, emptyState, toolbarStart, toolbarDescription, toolbarEnd, pager, onReachEnd, loadingMore, hasMore, itemNoun, rowActions, rowActionMode, bulk, rowTone, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, testId, onCellChange, getRowTestId, getCellTestId, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, view, resetAll, exportList, serverGroups, totals, notice, rangeFacets, onRangeChange, }) {
|
|
61
36
|
// One locale/translate subscription per table — not per cell.
|
|
62
37
|
// Optional hooks: a bare DataTable outside LocaleProvider must not crash.
|
|
63
38
|
const tableTranslate = useOptionalTranslation();
|
|
@@ -66,8 +41,9 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
66
41
|
// affordance. Cards replace the table entirely below the breakpoint — same
|
|
67
42
|
// single-mount pattern as EmbeddedListInput/embedded-list-input.tsx.
|
|
68
43
|
const isNarrow = useIsNarrowViewport();
|
|
44
|
+
const viewColumns = useViewColumns(schemaColumns, view, isNarrow);
|
|
45
|
+
const density = densityClasses(view, scrollBody === true);
|
|
69
46
|
const [facetsOpenNarrow, setFacetsOpenNarrow] = useState(false);
|
|
70
|
-
const [toggledGroups, setToggledGroups] = useState(new Set());
|
|
71
47
|
// Toolbar wrapper: shared container for toolbar + table so the two belong
|
|
72
48
|
// together visually. The toolbar is NOT sticky: lists typically scroll with
|
|
73
49
|
// the page container, not internally. Sticky would compete with the topbar.
|
|
@@ -86,10 +62,39 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
86
62
|
const rowClickLabel = rowClickAction?.label;
|
|
87
63
|
const rowClickIsHidden = (row) => rowClickAction?.isVisible !== undefined && !rowClickAction.isVisible(row);
|
|
88
64
|
const hasTableActions = tableActions !== undefined && tableActions.length > 0;
|
|
89
|
-
const isEmpty = rows.length === 0;
|
|
65
|
+
const isEmpty = rows.length === 0 && (serverGroups?.groups.length ?? 0) === 0;
|
|
90
66
|
const expansionIdPrefix = useId();
|
|
91
|
-
const isRowExpandable = renderExpandedRow !== undefined && onToggleRowExpanded !== undefined;
|
|
92
67
|
const rowTestId = (row) => getRowTestId?.(row) ?? `row-${row.id}`;
|
|
68
|
+
const isRowExpandable = renderExpandedRow !== undefined && onToggleRowExpanded !== undefined;
|
|
69
|
+
const fillsHeight = scrollBody === true;
|
|
70
|
+
const selection = useBulkSelection({
|
|
71
|
+
bulk,
|
|
72
|
+
rows,
|
|
73
|
+
titleField: schemaColumns[0]?.field,
|
|
74
|
+
rowTestId,
|
|
75
|
+
flush: fillsHeight,
|
|
76
|
+
});
|
|
77
|
+
const { headerRowRef, measured } = useMeasuredColumnWidths(isPinningActive(view, isNarrow) || isResizingActive(view, isNarrow));
|
|
78
|
+
const pins = useColumnPins({
|
|
79
|
+
columns: viewColumns,
|
|
80
|
+
view,
|
|
81
|
+
isNarrow,
|
|
82
|
+
measured,
|
|
83
|
+
hasActionsColumn: hasTableActions,
|
|
84
|
+
hasExpandColumn: isRowExpandable,
|
|
85
|
+
hasSelectColumn: selection.active,
|
|
86
|
+
fillsHeight,
|
|
87
|
+
});
|
|
88
|
+
const columns = pins.columns;
|
|
89
|
+
const sizing = useColumnSizing({
|
|
90
|
+
columns,
|
|
91
|
+
view,
|
|
92
|
+
isNarrow,
|
|
93
|
+
measured,
|
|
94
|
+
hasExpandColumn: isRowExpandable,
|
|
95
|
+
hasSelectColumn: selection.active,
|
|
96
|
+
});
|
|
97
|
+
const fullColSpan = columns.length + [isRowExpandable, hasTableActions, selection.active].filter(Boolean).length;
|
|
93
98
|
const expansionDomId = (row) => `${expansionIdPrefix}-${row.id}`;
|
|
94
99
|
// The expand toggle of a row, shared by table and card layout. Named after
|
|
95
100
|
// the first data column, the one a user recognizes the row by.
|
|
@@ -111,75 +116,59 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
111
116
|
}
|
|
112
117
|
return (_jsx(TableRow, { id: expansionDomId(row), "data-testid": `${rowTestId(row)}-expansion`, className: "hover:bg-transparent", children: _jsx(TableCell, { colSpan: colSpan, className: EXPANSION_CELL_CLASS, children: _jsx("div", { className: EXPANSION_CONTENT_CLASS, children: content }) }) }));
|
|
113
118
|
}
|
|
114
|
-
const fillsHeight = scrollBody === true;
|
|
115
119
|
const TableRoot = fillsHeight ? FlushTable : Table;
|
|
116
120
|
const emptyBlock = (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-empty` : "render-list-empty", className: cn("flex flex-col items-center justify-center p-12 text-sm text-muted-foreground gap-3", chromeless !== true && !fillsHeight && "rounded-md border border-dashed"), children: emptyState ?? _jsx("span", { children: tableTranslate?.("kumiko.list.no-entries") ?? "No entries." }) }));
|
|
117
|
-
const tableItems =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
const tableItems = buildDataTableItems(rows, serverGroups);
|
|
122
|
+
const keyboardRow = useRowKeyboardNavigation({
|
|
123
|
+
view,
|
|
124
|
+
isNarrow,
|
|
125
|
+
rowIds: tableItems.flatMap((item) => (item.kind === "row" ? [item.row.id] : [])),
|
|
126
|
+
onSelect: selection.onKeyboardSelect,
|
|
127
|
+
});
|
|
128
|
+
const schemaColumnByField = new Map(schemaColumns.map((col) => [col.field, col]));
|
|
129
|
+
const columnOf = (field) => schemaColumnByField.get(field);
|
|
130
|
+
function renderBand(item) {
|
|
131
|
+
return _jsx(GroupBand, { item: item, columnOf: columnOf });
|
|
125
132
|
}
|
|
126
133
|
function tableInner() {
|
|
127
134
|
if (isEmpty)
|
|
128
135
|
return emptyBlock;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
className: "w-px text-right text-muted-foreground md:sticky md:right-0 md:z-10 md:bg-muted", "aria-label": tableTranslate?.("kumiko.list.actions") ?? "Actions" }))] }) }), _jsx(TableBody, { children: tableItems.map((item) => {
|
|
138
|
-
if (item.kind === "group") {
|
|
139
|
-
return (_jsx(TableRow, { "data-testid": `row-group-${item.key}`, children: _jsx(TableCell, { colSpan: columns.length + (isRowExpandable ? 1 : 0) + (hasTableActions ? 1 : 0), className: "bg-muted/40 p-0", children: renderGroupToggle(item) }) }, `group-${item.key}`));
|
|
136
|
+
const expandHeaderPin = pins.headerCell(EXPAND_COLUMN_KEY);
|
|
137
|
+
const expandBodyPin = pins.bodyCell(EXPAND_COLUMN_KEY);
|
|
138
|
+
return (_jsx("div", { className: cn(!fillsHeight && "overflow-hidden", !fillsHeight && chromeless !== true && "rounded-lg border bg-card"), children: _jsxs(TableRoot, { "data-testid": testId, className: fillsHeight ? FILL_TABLE_CLASS : undefined, ...(sizing !== undefined && { style: sizing.tableStyle }), children: [sizing !== undefined && (_jsx(ColumnWidthGroup, { fields: columns.map((col) => col.field), sizing: sizing, hasExpandColumn: isRowExpandable, hasSelectColumn: selection.active, hasActionsColumn: hasTableActions })), _jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { ref: headerRowRef, className: "hover:bg-transparent", children: [selection.headerCell(pins.headerCell(SELECT_COLUMN_KEY)), isRowExpandable && (_jsx(TableHead, { "data-testid": "column-expand", className: cn(EXPAND_TOGGLE_CELL_CLASS, expandHeaderPin.className), style: expandHeaderPin.style })), columns.map((col) => {
|
|
139
|
+
const headerPin = pins.headerCell(col.field);
|
|
140
|
+
return (_jsx(SortableHeader, { field: col.field, label: col.label, sortable: col.sortable === true, highlighted: col.highlighted === true, numeric: NUMERIC_COLUMN_TYPES.has(col.type), fitContent: col.fileDisplay !== undefined, className: cn(density.header, headerPin.className), style: headerPin.style, columnField: headerPin.columnField, trailing: sizing && (_jsx(ColumnResizeHandle, { field: col.field, label: col.label, sizing: sizing })), ...(sort !== undefined && sort !== null && { sort }), ...(onSortChange !== undefined && { onSortChange }) }, col.field));
|
|
141
|
+
}), hasTableActions && (_jsx(ActionsHeaderCell, { label: tableTranslate?.("kumiko.list.actions") ?? "Actions", style: pins.actionsHeaderStyle, absorbsLeftover: sizing !== undefined }))] }) }), _jsx(TableBody, { children: tableItems.map((item) => {
|
|
142
|
+
if (item.kind !== "row") {
|
|
143
|
+
return (_jsx(TableRow, { "data-testid": bandTestId(item), children: _jsx(TableCell, { colSpan: fullColSpan, className: bandClassName(item), children: renderBand(item) }) }, bandReactKey(item)));
|
|
140
144
|
}
|
|
141
145
|
const row = item.row;
|
|
142
146
|
const opensOnClick = rowIsLink && !rowClickIsHidden(row);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// not navigation). We stopPropagation for mouse and
|
|
168
|
-
// keyboard so a11y stays consistent.
|
|
169
|
-
onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(RowActionsCell, { row: row, actions: tableActions, mode: rowActionMode, rowIsLink: rowIsLink }) }))] }), renderExpansion(row, "row", columns.length + (isRowExpandable ? 1 : 0) + (hasTableActions ? 1 : 0))] }, row.id));
|
|
170
|
-
}) })] }) }));
|
|
147
|
+
const keyboard = keyboardRow(row.id, opensOnClick ? () => onRowClick(row) : undefined);
|
|
148
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), onClick: opensOnClick ? () => onRowClick(row) : undefined, className: cn(rowToneClass(rowTone?.(row)), opensOnClick && "cursor-pointer", fillsHeight && "h-10 border-border-row hover:bg-muted", density.row, keyboard.className), ...keyboard.props, children: [selection.bodyCell(row, pins.bodyCell(SELECT_COLUMN_KEY)), isRowExpandable && (_jsx(TableCell, { "data-testid": getCellTestId?.(row, "expand") ?? `cell-${row.id}-expand`, className: cn(EXPAND_TOGGLE_CELL_CLASS, expandBodyPin.className), style: expandBodyPin.style, children: renderExpandToggle(row) })), columns.map((col, colIndex) => {
|
|
149
|
+
const bodyPin = pins.bodyCell(col.field);
|
|
150
|
+
return (_jsx(TableCell, { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, "data-highlighted": col.highlighted === true ? "true" : undefined,
|
|
151
|
+
// Text wraps instead of truncating: nowrap cells keep their
|
|
152
|
+
// full width, so a wide table scrolled sideways and its
|
|
153
|
+
// last column slid under the sticky actions column with
|
|
154
|
+
// nothing visibly cut off. max-w-xs bounds a single column;
|
|
155
|
+
// badges and dates stay nowrap by their own classes.
|
|
156
|
+
className: cn(col.fileDisplay === undefined
|
|
157
|
+
? cn(sizing ? "overflow-hidden" : "max-w-xs", "whitespace-normal break-words")
|
|
158
|
+
: FIT_CONTENT_CELL_CLASS, colIndex === 0
|
|
159
|
+
? "font-medium text-foreground"
|
|
160
|
+
: "text-foreground-secondary", NUMERIC_COLUMN_TYPES.has(col.type) && "text-right", TABULAR_COLUMN_TYPES.has(col.type) && "tabular-nums", col.highlighted === true && "bg-accent/40", density.cell, bodyPin.className), style: bodyPin.style, title: cellTitle(row.values[col.field]), children: _jsx(FirstCellLink, { enabled: colIndex === 0 && opensOnClick && onCellChange === undefined, onOpen: () => onRowClick?.(row), empty: isEmptyCellValue(row.values[col.field]), emptyLabel: rowClickLabel, children: _jsx(DataTableCell, { value: row.values[col.field], row: row.values, field: col.field, type: col.type, renderer: col.renderer, translate: tableTranslate, locale: tableLocale, ...(col.optionLabels !== undefined && {
|
|
161
|
+
optionLabels: col.optionLabels,
|
|
162
|
+
}), ...(col.optionTones !== undefined && {
|
|
163
|
+
optionTones: col.optionTones,
|
|
164
|
+
}), ...(col.grouping !== undefined && { grouping: col.grouping }), ...(col.fileDisplay !== undefined && {
|
|
165
|
+
fileDisplay: col.fileDisplay,
|
|
166
|
+
}), ...(onCellChange !== undefined && {
|
|
167
|
+
onChange: (value) => onCellChange(row.id, col.field, value),
|
|
168
|
+
}), ...cellLinkProp(col, row, !(colIndex === 0 && opensOnClick && onCellChange === undefined)) }) }) }, col.field));
|
|
169
|
+
}), hasTableActions && (_jsx(ActionsBodyCell, { testId: getCellTestId?.(row, "actions") ?? `cell-${row.id}-actions`, fillsHeight: fillsHeight, measureContent: sizing !== undefined, children: _jsx(RowActionsCell, { row: row, actions: tableActions, mode: rowActionMode, rowIsLink: rowIsLink }) }))] }), renderExpansion(row, "row", fullColSpan)] }, row.id));
|
|
170
|
+
}) }), totals !== undefined && (_jsx(TotalsFooterRow, { totals: totals, columns: columns, cellPin: pins.bodyCell, leadingCells: { select: selection.active, expand: isRowExpandable }, actionsCell: hasTableActions ? { fillsHeight } : undefined, cellClassName: density.cell }))] }) }));
|
|
171
171
|
}
|
|
172
|
-
// No column headers below the breakpoint, so the click-to-sort affordance
|
|
173
|
-
// on SortableHeader has nothing to attach to. A single native <select> is
|
|
174
|
-
// the whole fix — no custom widget, no menu — fed straight from the
|
|
175
|
-
// columns marked sortable in the ViewModel. Options carry the resolved
|
|
176
|
-
// {field, dir} directly so onChange can look the pick up by value instead
|
|
177
|
-
// of parsing/casting the option string back apart.
|
|
178
|
-
const sortableColumns = columns.filter((col) => col.sortable);
|
|
179
|
-
const sortOptions = sortableColumns.flatMap((col) => [
|
|
180
|
-
{ value: `${col.field}:asc`, field: col.field, dir: "asc", label: `${col.label} ↑` },
|
|
181
|
-
{ value: `${col.field}:desc`, field: col.field, dir: "desc", label: `${col.label} ↓` },
|
|
182
|
-
]);
|
|
183
172
|
const { image: cardImageColumn, title: cardTitleColumn, status: cardStatusColumn, meta: cardMetaColumns, } = cardColumnRoles(columns);
|
|
184
173
|
function cardCell(row, col) {
|
|
185
174
|
// A bare check mark in the subtitle line says nothing without its column.
|
|
@@ -198,7 +187,7 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
198
187
|
const body = (_jsxs(_Fragment, { children: [cardImageColumn?.fileDisplay !== undefined && (_jsx("div", { "data-testid": `card-image-${row.id}`, className: "shrink-0", children: _jsx(CardLeadingImage, { value: row.values[cardImageColumn.field], display: cardImageColumn.fileDisplay }) })), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-2", children: [cardTitleColumn !== undefined && (_jsx("span", { "data-testid": getCellTestId?.(row, cardTitleColumn.field) ??
|
|
199
188
|
`cell-${row.id}-${cardTitleColumn.field}`, className: "min-w-0 break-words text-base font-semibold text-foreground", children: cardCell(row, cardTitleColumn) })), showStatus && (_jsx("span", { "data-testid": getCellTestId?.(row, cardStatusColumn.field) ??
|
|
200
189
|
`cell-${row.id}-${cardStatusColumn.field}`, className: "shrink-0", children: cardCell(row, cardStatusColumn) }))] }), metaColumns.length > 0 && (_jsx("div", { className: "min-w-0 overflow-x-clip", children: _jsx("div", { "data-testid": `card-meta-${row.id}`, className: "-ms-3 flex min-w-0 flex-wrap items-center text-[13px] leading-5 tabular-nums text-foreground-secondary", children: metaColumns.map((col) => (_jsxs("div", { className: "flex h-5 min-w-0 max-w-full items-center", children: [_jsx("span", { "aria-hidden": "true", className: "w-3 shrink-0 text-center", children: "\u00B7" }), _jsx("div", { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, className: "min-w-0 truncate leading-5", onPointerEnter: showFullTextOnHover, children: isBadgeColumn(col) ? (_jsx("span", { className: "inline-flex align-middle", children: cardCell(row, col) })) : (cardCell(row, col)) })] }, col.field))) }) }))] }), opensOnClick && (_jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }))] }));
|
|
201
|
-
const rowItem = (_jsxs("li", { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), className: cn("flex min-h-[72px] items-center gap-2 border-b border-border-row py-3", rowToneClass(rowTone?.(row)), isRowExpandable ? "pl-2" : "pl-4", hasMenu ? "pr-2" : "pr-4"), ...(opensOnClick && { onClick: () => onRowClick(row) }), children: [renderExpandToggle(row), opensOnClick ? (_jsx("button", { type: "button", className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 bg-transparent p-0 text-left", onKeyDown: (e) => {
|
|
190
|
+
const rowItem = (_jsxs("li", { "data-testid": rowTestId(row), "data-tone": rowTone?.(row), className: cn("flex min-h-[72px] items-center gap-2 border-b border-border-row py-3", rowToneClass(rowTone?.(row)), isRowExpandable || selection.active ? "pl-2" : "pl-4", hasMenu ? "pr-2" : "pr-4", density.card), ...(opensOnClick && { onClick: () => onRowClick(row) }), children: [selection.cardCheckbox(row), renderExpandToggle(row), opensOnClick ? (_jsx("button", { type: "button", className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 bg-transparent p-0 text-left", onKeyDown: (e) => {
|
|
202
191
|
if (e.key !== "Enter")
|
|
203
192
|
return;
|
|
204
193
|
e.preventDefault();
|
|
@@ -214,47 +203,37 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
214
203
|
function cardsInner() {
|
|
215
204
|
if (isEmpty)
|
|
216
205
|
return emptyBlock;
|
|
217
|
-
|
|
218
|
-
? (tableTranslate?.("kumiko.list.sort.by", {
|
|
219
|
-
column: columns.find((col) => col.field === sort.field)?.label ?? sort.field,
|
|
220
|
-
}) ?? `Sorted by ${sort.field}`)
|
|
221
|
-
: (tableTranslate?.("kumiko.list.sort.unsorted") ?? "Unsorted");
|
|
222
|
-
const SortArrow = sort?.dir === "desc" ? ArrowDown : ArrowUp;
|
|
223
|
-
// URL state can carry a sort on a column that is not sortable; without an
|
|
224
|
-
// option for it the select would claim "Unsorted" while the list is sorted.
|
|
225
|
-
const currentSortValue = sort !== undefined && sort !== null ? `${sort.field}:${sort.dir}` : "";
|
|
226
|
-
const currentSortIsUnlisted = sort !== undefined && sort !== null && !sortOptions.some((o) => o.value === currentSortValue);
|
|
227
|
-
return (_jsx("div", { "data-testid": testId, className: "contents", children: _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-cards` : "render-list-cards", className: "flex flex-col", children: [onSortChange !== undefined && sortableColumns.length > 0 && (_jsxs("div", { className: "relative flex h-9 items-center gap-1.5 px-4 text-[13px] text-foreground-secondary", children: [_jsx("span", { "aria-hidden": "true", children: sortLabel }), sort !== undefined && sort !== null && (_jsx(SortArrow, { className: "size-3.5", "aria-hidden": "true" })), _jsxs("select", { "aria-label": tableTranslate?.("kumiko.list.sort.label") ?? "Sort", "data-testid": testId !== undefined ? `${testId}-sort` : "render-list-sort", value: currentSortValue, onChange: (e) => {
|
|
228
|
-
const raw = e.target.value;
|
|
229
|
-
if (raw === "") {
|
|
230
|
-
onSortChange(null);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const picked = sortOptions.find((o) => o.value === raw);
|
|
234
|
-
if (picked === undefined)
|
|
235
|
-
return;
|
|
236
|
-
onSortChange({ field: picked.field, dir: picked.dir });
|
|
237
|
-
}, className: "absolute inset-0 size-full cursor-pointer opacity-0", children: [_jsx("option", { value: "", children: tableTranslate?.("kumiko.list.sort.unsorted") ?? "Unsorted" }), currentSortIsUnlisted && (_jsx("option", { value: currentSortValue, children: `${columns.find((col) => col.field === sort.field)?.label ?? sort.field} ${sort.dir === "desc" ? "↓" : "↑"}` })), sortOptions.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value)))] })] })), _jsx("ul", { className: "m-0 list-none border-t border-border-row p-0", children: tableItems.map((item) => item.kind === "group" ? (_jsx("li", { "data-testid": `row-group-${item.key}`, className: "border-b border-border-row bg-muted/40", children: renderGroupToggle(item) }, `group-${item.key}`)) : (renderCard(item.row))) })] }) }));
|
|
206
|
+
return (_jsx("div", { "data-testid": testId, className: "contents", children: _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-cards` : "render-list-cards", className: "flex flex-col", children: [_jsx(CardSortSelect, { columns: columns, sort: sort, onSortChange: onSortChange, testId: testId }), _jsx("ul", { className: "m-0 list-none border-t border-border-row p-0", children: tableItems.map((item) => item.kind === "row" ? (renderCard(item.row)) : (_jsx("li", { "data-testid": bandTestId(item), className: cn("border-b border-border-row", bandClassName(item)), children: renderBand(item) }, bandReactKey(item)))) }), totals !== undefined && _jsx(TotalsCard, { totals: totals, columnOf: columnOf })] }) }));
|
|
238
207
|
}
|
|
239
208
|
const tableContent = isNarrow ? cardsInner() : tableInner();
|
|
240
209
|
// The pager is ALWAYS rendered below the table (even with rows=[]) so the
|
|
241
210
|
// user can page back on a filter hit of zero without losing the list.
|
|
242
211
|
// Except total === 0: then there is nothing to paginate. Incompatible with
|
|
243
212
|
// infinite scroll: the caller sets either pager OR onReachEnd.
|
|
244
|
-
const pagerElement = pager !== undefined && pager.total > 0 ? (_jsx(Pager, { page: pager.page, limit: pager.limit, total: pager.total, onPageChange: pager.onPageChange, ...(pager.pageSizeOptions !== undefined && { pageSizeOptions: pager.pageSizeOptions }), ...(pager.onPageSizeChange !== undefined && { onPageSizeChange: pager.onPageSizeChange }), ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-pager` : "render-list-pager", pinned: fillsHeight })) : undefined;
|
|
245
|
-
const sentinelElement = pagerElement === undefined && onReachEnd !== undefined ? (_jsx(InfiniteSentinel, { onReachEnd: onReachEnd, loadingMore: loadingMore === true, hasMore: hasMore !== false, rowsCount: rows.length, testId: testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel" })) : undefined;
|
|
213
|
+
const pagerElement = pager !== undefined && pager.total > 0 && serverGroups === undefined ? (_jsx(Pager, { page: pager.page, limit: pager.limit, total: pager.total, onPageChange: pager.onPageChange, ...(pager.pageSizeOptions !== undefined && { pageSizeOptions: pager.pageSizeOptions }), ...(pager.onPageSizeChange !== undefined && { onPageSizeChange: pager.onPageSizeChange }), ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-pager` : "render-list-pager", pinned: fillsHeight })) : undefined;
|
|
214
|
+
const sentinelElement = pagerElement === undefined && onReachEnd !== undefined && serverGroups === undefined ? (_jsx(InfiniteSentinel, { onReachEnd: onReachEnd, loadingMore: loadingMore === true, hasMore: hasMore !== false, rowsCount: rows.length, testId: testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel" })) : undefined;
|
|
246
215
|
// With scrollBody the footer (pager, else the entry count) sits OUTSIDE the
|
|
247
216
|
// scroll surface so it stays put at any row count; the infinite sentinel
|
|
248
217
|
// stays INSIDE, or it would never intersect the scrollport.
|
|
249
|
-
const content = fillsHeight ? (_jsxs(_Fragment, { children: [_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-scroll` : "render-list-scroll", className: "min-h-0 flex-1 overflow-auto border-t border-border bg-card", children: [tableContent, sentinelElement] }), pagerElement ?? (_jsx(DataTableFooter, { count: rows.length, ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-footer` : "render-list-footer" }))] })) : (_jsxs(_Fragment, { children: [tableContent, pagerElement, sentinelElement] }));
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
218
|
+
const content = fillsHeight ? (_jsxs(_Fragment, { children: [selection.bar, _jsxs("div", { "data-testid": testId !== undefined ? `${testId}-scroll` : "render-list-scroll", className: "min-h-0 flex-1 overflow-auto border-t border-border bg-card", children: [tableContent, sentinelElement] }), pagerElement ?? (_jsx(DataTableFooter, { count: serverGroups?.totalCount ?? rows.length, ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-footer` : "render-list-footer" }))] })) : (_jsxs(_Fragment, { children: [selection.bar, tableContent, pagerElement, sentinelElement] }));
|
|
219
|
+
const facetCluster = buildFacetCluster({
|
|
220
|
+
filterFacets,
|
|
221
|
+
filterValues,
|
|
222
|
+
onFilterChange,
|
|
223
|
+
onFilterReset,
|
|
224
|
+
dateRangeFacets,
|
|
225
|
+
onDateRangeChange,
|
|
226
|
+
rangeFacets,
|
|
227
|
+
onRangeChange,
|
|
228
|
+
resetAll,
|
|
229
|
+
notice,
|
|
230
|
+
dense: fillsHeight,
|
|
231
|
+
});
|
|
232
|
+
const columnLabels = new Map(schemaColumns.map((col) => [col.field, col.label]));
|
|
256
233
|
const collapsesFacetsBehindButton = isNarrow && fillsHeight && facetCluster !== undefined;
|
|
257
|
-
const hasToolbar = toolbarStart !== undefined ||
|
|
234
|
+
const hasToolbar = toolbarStart !== undefined ||
|
|
235
|
+
facetCluster !== undefined ||
|
|
236
|
+
toolbarEndHasContent(view, exportList, toolbarEnd);
|
|
258
237
|
// dashboard-01 pattern: the toolbar (search + facets + "+ New") sits ABOVE
|
|
259
238
|
// the table in the same padding block; no separate bg bar, no screen title
|
|
260
239
|
// (that lives in the shell's breadcrumb).
|
|
@@ -268,11 +247,11 @@ export function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange
|
|
|
268
247
|
? screenPaddingClassName
|
|
269
248
|
: screenPadding === false
|
|
270
249
|
? undefined
|
|
271
|
-
: "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", fillsHeight
|
|
250
|
+
: "p-6")), children: [hasToolbar && (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn("flex flex-wrap items-center", UNIFORM_BAR_CONTROLS_CLASSES, fillsHeight
|
|
272
251
|
? isNarrow
|
|
273
252
|
? "shrink-0 gap-2 p-3"
|
|
274
253
|
: "min-h-13 shrink-0 gap-2 px-6 py-2 md:py-0"
|
|
275
254
|
: "gap-3"), children: [toolbarStart !== undefined && (_jsxs("div", { className: cn(fillsHeight
|
|
276
|
-
? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11
|
|
277
|
-
: "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster),
|
|
255
|
+
? "relative w-full md:w-[300px] max-md:flex-1 [&_input]:border-border-strong [&_input]:pl-8 [&_input]:max-md:h-11"
|
|
256
|
+
: "flex-1 min-w-48 max-w-sm"), children: [fillsHeight && (_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" })), toolbarStart] })), collapsesFacetsBehindButton ? (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] })) : (facetCluster), _jsx(ToolbarEndCluster, { view: view, columnLabels: columnLabels, exportList: exportList, toolbarEnd: toolbarEnd })] })), toolbarDescription !== undefined && (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6"), fillsHeight && !hasToolbar && "pt-4"), children: toolbarDescription })), content] }));
|
|
278
257
|
}
|
|
@@ -26,7 +26,7 @@ export function FacetFilter({ facet, selected, onChange, dense = false, }) {
|
|
|
26
26
|
if (facet.chips !== undefined) {
|
|
27
27
|
return _jsx(FacetChips, { facet: facet, selected: selected, onChange: onChange });
|
|
28
28
|
}
|
|
29
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "
|
|
29
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(UiButton, { variant: "outline", size: "sm", className: cn("h-9", dense && "border-border-strong px-2.5"), "data-testid": `facet-${facet.field}`, children: [facet.label, selected.length > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-1 rounded-sm px-1 font-normal tabular-nums", children: selected.length })), _jsx(ChevronDown, { className: "text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "max-h-[var(--radix-dropdown-menu-content-available-height)] w-44 overflow-y-auto", children: facet.options.map((opt) => (_jsx(DropdownMenuCheckboxItem, { checked: selected.includes(opt.value), onCheckedChange: (checked) => toggle(opt.value, checked), onSelect: (e) => e.preventDefault(), "data-testid": `facet-${facet.field}-${opt.value}`, children: opt.label }, opt.value))) })] }));
|
|
30
30
|
}
|
|
31
31
|
// Time-range filter: two native <input type="date"> instead of a
|
|
32
32
|
// dropdown — the browser supplies the calendar, the locale and the keyboard
|
|
@@ -12,6 +12,9 @@ type FileMeta = {
|
|
|
12
12
|
readonly fileName: string;
|
|
13
13
|
readonly mimeType: string | undefined;
|
|
14
14
|
};
|
|
15
|
+
/** File names are per-user data and the server re-checks access per request, so
|
|
16
|
+
* the cache must not outlive the session that filled it. */
|
|
17
|
+
export declare function clearFileMetaCache(): void;
|
|
15
18
|
export declare function useFileMeta(fileId: string): FileMeta | null;
|
|
16
19
|
export declare function FileNameLink({ fileId, fallbackLabel, className, }: {
|
|
17
20
|
readonly fileId: string;
|
|
@@ -46,6 +46,11 @@ const META_BATCH_SIZE = 100;
|
|
|
46
46
|
const META_CACHE_LIMIT = 500;
|
|
47
47
|
const metaCache = new Map();
|
|
48
48
|
let pendingMetaIds = new Map();
|
|
49
|
+
/** File names are per-user data and the server re-checks access per request, so
|
|
50
|
+
* the cache must not outlive the session that filled it. */
|
|
51
|
+
export function clearFileMetaCache() {
|
|
52
|
+
metaCache.clear();
|
|
53
|
+
}
|
|
49
54
|
function parseBatchMeta(json) {
|
|
50
55
|
const metaById = new Map();
|
|
51
56
|
if (typeof json !== "object" || json === null || !("files" in json))
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type FileValueListProps } from "../../index.ts";
|
|
3
|
-
export declare function FileValueList({ fileIds, testId }: FileValueListProps): ReactNode;
|
|
3
|
+
export declare function FileValueList({ fileIds, previewOpenByDefault, testId, }: FileValueListProps): ReactNode;
|
|
@@ -6,17 +6,17 @@ import { Button } from "../ui/button.js";
|
|
|
6
6
|
import { PdfPreview } from "../widgets/pdf-preview.js";
|
|
7
7
|
import { fileOriginalUrl, useFileMeta } from "./file-thumbnail.js";
|
|
8
8
|
const PDF_MIME_TYPE = "application/pdf";
|
|
9
|
-
function FileValueRow({ fileId, testId, }) {
|
|
9
|
+
function FileValueRow({ fileId, previewOpenInitially, testId, }) {
|
|
10
10
|
const t = useTranslation();
|
|
11
11
|
const meta = useFileMeta(fileId);
|
|
12
|
-
const [previewOpen, setPreviewOpen] = useState(
|
|
12
|
+
const [previewOpen, setPreviewOpen] = useState(previewOpenInitially);
|
|
13
13
|
if (!isUuid(fileId))
|
|
14
14
|
return _jsx("span", { className: "text-sm", children: fileId });
|
|
15
15
|
const downloadUrl = fileOriginalUrl(fileId);
|
|
16
|
-
return (_jsxs("li", { className: "flex flex-col gap-2", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("span", { className: "text-sm", children: meta?.fileName ?? fileId.slice(0, 8) }), _jsx("a", { href: downloadUrl, download: meta?.fileName ?? "", className: "text-primary text-sm underline underline-offset-2", ...(testId !== undefined && { "data-testid": `${testId}-download` }), children: t("kumiko.field.file-list.download") }), meta?.mimeType === PDF_MIME_TYPE && (_jsx(Button, { type: "button", variant: "link", size: "sm", "aria-expanded": previewOpen, onClick: () => setPreviewOpen((open) => !open), ...(testId !== undefined && { "data-testid": `${testId}-preview-toggle` }), children: t(previewOpen ? "kumiko.field.file-list.hidePreview" : "kumiko.field.file-list.preview") }))] }), previewOpen && (_jsx(PdfPreview, { source: { url: downloadUrl }, testId: `file-preview-${fileId}` }))] }));
|
|
16
|
+
return (_jsxs("li", { className: "flex flex-col gap-2", "data-testid": testId, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("span", { className: "text-sm", children: meta?.fileName ?? fileId.slice(0, 8) }), _jsx("a", { href: downloadUrl, download: meta?.fileName ?? "", className: "text-primary text-sm underline underline-offset-2", ...(testId !== undefined && { "data-testid": `${testId}-download` }), children: t("kumiko.field.file-list.download") }), meta?.mimeType === PDF_MIME_TYPE && (_jsx(Button, { type: "button", variant: "link", size: "sm", "aria-expanded": previewOpen, onClick: () => setPreviewOpen((open) => !open), ...(testId !== undefined && { "data-testid": `${testId}-preview-toggle` }), children: t(previewOpen ? "kumiko.field.file-list.hidePreview" : "kumiko.field.file-list.preview") }))] }), previewOpen && meta?.mimeType === PDF_MIME_TYPE && (_jsx(PdfPreview, { source: { url: downloadUrl }, testId: `file-preview-${fileId}` }))] }));
|
|
17
17
|
}
|
|
18
|
-
export function FileValueList({ fileIds, testId }) {
|
|
18
|
+
export function FileValueList({ fileIds, previewOpenByDefault = false, testId, }) {
|
|
19
19
|
if (fileIds.length === 0)
|
|
20
20
|
return null;
|
|
21
|
-
return (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", "data-testid": testId, children: fileIds.map((fileId) => (_jsx(FileValueRow, { fileId: fileId, ...(testId !== undefined && { testId: `${testId}-${fileId}` }) }, fileId))) }));
|
|
21
|
+
return (_jsx("ul", { className: "m-0 flex list-none flex-col gap-3 p-0", "data-testid": testId, children: fileIds.map((fileId, index) => (_jsx(FileValueRow, { fileId: fileId, previewOpenInitially: previewOpenByDefault && index === 0, ...(testId !== undefined && { testId: `${testId}-${fileId}` }) }, fileId))) }));
|
|
22
22
|
}
|
|
@@ -5,6 +5,7 @@ import { Checkbox } from "../ui/checkbox.js";
|
|
|
5
5
|
import { Input as UiInput } from "../ui/input.js";
|
|
6
6
|
import { Switch } from "../ui/switch.js";
|
|
7
7
|
import { Textarea } from "../ui/textarea.js";
|
|
8
|
+
import { CodeInput } from "./code-input.js";
|
|
8
9
|
import { ComboboxInput } from "./combobox.js";
|
|
9
10
|
import { DateInput } from "./date-input.js";
|
|
10
11
|
import { FileUploadInput } from "./file-upload.js";
|
|
@@ -15,6 +16,7 @@ import { NumberInput } from "./number-input.js";
|
|
|
15
16
|
import { hasDescriptionOrGroup } from "./option-groups.js";
|
|
16
17
|
import { TimestampInput } from "./timestamp-input.js";
|
|
17
18
|
import { TzInput } from "./tz-input.js";
|
|
19
|
+
import { useFocusOnMount } from "./use-focus-on-mount.js";
|
|
18
20
|
import "../react-css-custom-properties.js";
|
|
19
21
|
import { FieldDescriptionContext, FieldLayoutContext } from "./contexts.js";
|
|
20
22
|
import { defaultSelectPresentation, FIELD_ICON_CLASS, fieldIconFor, RadioListSelect, SegmentedSelect, textareaMinHeight, unitSuffixId, withFieldIcon, withInlineDisabledHint, withInputAddons, withoutSegmentedSelectPlaceholder, withTrailingAction, } from "./field.js";
|
|
@@ -23,6 +25,7 @@ export function DefaultInput(props) {
|
|
|
23
25
|
// themselves, no manual border-destructive needed anymore.
|
|
24
26
|
const booleanLayout = useContext(FieldLayoutContext);
|
|
25
27
|
const describedBy = useContext(FieldDescriptionContext);
|
|
28
|
+
const textInputRef = useFocusOnMount(props.kind === "text" ? props.autoFocus : undefined);
|
|
26
29
|
const common = {
|
|
27
30
|
id: props.id,
|
|
28
31
|
name: props.name,
|
|
@@ -33,7 +36,9 @@ export function DefaultInput(props) {
|
|
|
33
36
|
};
|
|
34
37
|
switch (props.kind) {
|
|
35
38
|
case "text":
|
|
36
|
-
return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
|
|
39
|
+
return withInputAddons(props, withFieldIcon(props.icon, _jsx(UiInput, { type: "text", ref: textInputRef, ...common, ...props.dataAttributes, "data-testid": props.testId, readOnly: props.readOnly, value: props.value, onChange: (e) => props.onChange(e.target.value), onKeyDown: props.onKeyDown, ...(props.placeholder !== undefined && { placeholder: props.placeholder }), ...(props.autoComplete !== undefined && { autoComplete: props.autoComplete }), className: cn(fieldIconFor(props.icon) !== undefined ? "pl-8" : undefined) })));
|
|
40
|
+
case "code":
|
|
41
|
+
return _jsx(CodeInput, { ...props });
|
|
37
42
|
case "email":
|
|
38
43
|
return (_jsx(UiInput, { type: "email", ...common, "data-testid": props.testId, value: props.value, onChange: (e) => props.onChange(e.target.value), ...(props.placeholder !== undefined && { placeholder: props.placeholder }), autoComplete: props.autoComplete ?? "email" }));
|
|
39
44
|
case "month":
|
|
@@ -61,7 +61,7 @@ const FIELD_CELL_WIDTH_CLASS = {
|
|
|
61
61
|
money: "w-40 shrink-0 [&_label]:whitespace-nowrap",
|
|
62
62
|
date: "w-full sm:w-[200px]",
|
|
63
63
|
timestamp: "w-full sm:w-[328px]",
|
|
64
|
-
select: "w-full sm:w-
|
|
64
|
+
select: "w-full sm:w-60 sm:has-[[data-radio-list]]:w-full",
|
|
65
65
|
full: "w-full",
|
|
66
66
|
// self-start: the label shares the top line with neighbouring labels. The
|
|
67
67
|
// switch (1.15rem) gets (h-9 input − switch) / 2 margin to sit on the input line.
|
|
@@ -75,7 +75,8 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
|
|
|
75
75
|
if (list === true) {
|
|
76
76
|
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "flex flex-col divide-y divide-border", children: children }));
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
const declaresColumns = typeof columns === "number" && columns >= 2;
|
|
79
|
+
if (flow === true && (insideDrawerBody || !declaresColumns)) {
|
|
79
80
|
return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("flex gap-4", insideDrawerBody
|
|
80
81
|
? "flex-col items-start"
|
|
81
82
|
: // items-end aligns inputs when labels wrap to different heights; a
|
|
@@ -116,12 +117,13 @@ export function DefaultGrid({ columns, children, testId, maxRows, flow, list, ar
|
|
|
116
117
|
overflowY: "auto",
|
|
117
118
|
}),
|
|
118
119
|
};
|
|
119
|
-
return (_jsx("div", { ...groupProps, "data-testid": testId, className: "grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]",
|
|
120
|
+
return (_jsx("div", { ...groupProps, "data-testid": testId, className: cn("grid gap-[var(--kumiko-grid-gap,1rem)] grid-cols-1 sm:[grid-template-columns:var(--grid-cols)]", flow === true &&
|
|
121
|
+
"items-end has-[[data-appendix-below]]:items-start has-[[role=alert]]:items-start"), style: style, children: children }));
|
|
120
122
|
}
|
|
121
123
|
export function DefaultGridCell({ span, width, children }) {
|
|
122
124
|
const insideDrawerBody = useContext(DrawerBodyContext);
|
|
123
125
|
if (width !== undefined) {
|
|
124
|
-
return (_jsx("div", { className: cn("min-w-0 max-w-full", insideDrawerBody
|
|
126
|
+
return (_jsx("div", { className: cn("min-w-0 max-w-full", width === "full" && "sm:[grid-column:1/-1]", insideDrawerBody
|
|
125
127
|
? (DRAWER_FIELD_CELL_WIDTH_CLASS[width] ?? FIELD_CELL_WIDTH_CLASS[width])
|
|
126
128
|
: FIELD_CELL_WIDTH_CLASS[width]), children: children }));
|
|
127
129
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { DataTableRangeBound, DataTableRangeFacet } from "../../data-table-aggregate-types.ts";
|
|
3
|
+
/** "From/to" bounds of a number, decimal, money or bigInt field. */
|
|
4
|
+
export declare function NumberRangeFacetFilter({ facet, onChange, }: {
|
|
5
|
+
readonly facet: DataTableRangeFacet;
|
|
6
|
+
readonly onChange: (field: string, bound: DataTableRangeBound, value: string) => void;
|
|
7
|
+
}): ReactNode;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useOptionalTranslation } from "../../index.js";
|
|
4
|
+
const INPUT_CLASS = "h-9 w-24 rounded-md border border-input bg-transparent px-2 text-sm tabular-nums shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 max-md:min-h-11";
|
|
5
|
+
// A keystroke must not refetch the list, so a bound commits on blur or Enter;
|
|
6
|
+
// a value changed from outside (reset, back navigation) replaces the draft.
|
|
7
|
+
function RangeBoundInput({ facet, bound, label, onCommit, }) {
|
|
8
|
+
const committed = facet[bound];
|
|
9
|
+
const [draft, setDraft] = useState(committed);
|
|
10
|
+
const [seen, setSeen] = useState(committed);
|
|
11
|
+
if (seen !== committed) {
|
|
12
|
+
setSeen(committed);
|
|
13
|
+
setDraft(committed);
|
|
14
|
+
}
|
|
15
|
+
const commit = () => {
|
|
16
|
+
const trimmed = draft.trim();
|
|
17
|
+
if (trimmed !== "" && !Number.isFinite(Number(trimmed))) {
|
|
18
|
+
setDraft(committed);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (trimmed !== committed)
|
|
22
|
+
onCommit(facet.field, bound, trimmed);
|
|
23
|
+
};
|
|
24
|
+
return (_jsx("input", { type: "number", inputMode: "decimal", step: "any", className: INPUT_CLASS, value: draft, placeholder: label, "aria-label": `${facet.label} ${label}`, "data-testid": `facet-range-${facet.field}-${bound}`, onChange: (event) => setDraft(event.target.value), onBlur: commit, onKeyDown: (event) => {
|
|
25
|
+
if (event.key === "Enter")
|
|
26
|
+
commit();
|
|
27
|
+
} }));
|
|
28
|
+
}
|
|
29
|
+
/** "From/to" bounds of a number, decimal, money or bigInt field. */
|
|
30
|
+
export function NumberRangeFacetFilter({ facet, onChange, }) {
|
|
31
|
+
const translate = useOptionalTranslation();
|
|
32
|
+
return (_jsxs("div", { className: "flex items-center gap-2", "data-testid": `facet-range-${facet.field}`, children: [_jsx("span", { className: "text-sm text-muted-foreground", children: facet.label }), _jsx(RangeBoundInput, { facet: facet, bound: "min", label: translate?.("kumiko.list.range.min") ?? "From", onCommit: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: "\u2013" }), _jsx(RangeBoundInput, { facet: facet, bound: "max", label: translate?.("kumiko.list.range.max") ?? "To", onCommit: onChange }), facet.unit !== undefined && (_jsx("span", { className: "text-sm text-muted-foreground", children: facet.unit }))] }));
|
|
33
|
+
}
|