@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.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/projection-detail-body.js +59 -394
- package/dist/app/projection-detail-header-model.d.ts +11 -0
- package/dist/app/projection-detail-header-model.js +33 -0
- package/dist/app/projection-detail-header-region.d.ts +28 -0
- package/dist/app/projection-detail-header-region.js +81 -0
- package/dist/app/projection-detail-header.d.ts +29 -0
- package/dist/app/projection-detail-header.js +84 -0
- package/dist/app/projection-detail-navigation.d.ts +13 -0
- package/dist/app/projection-detail-navigation.js +91 -0
- package/dist/app/projection-detail-states.d.ts +22 -0
- package/dist/app/projection-detail-states.js +26 -0
- package/dist/app/projection-drawer-host.d.ts +12 -0
- package/dist/app/projection-drawer-host.js +10 -0
- package/dist/app/projection-list-body.d.ts +1 -1
- package/dist/app/projection-list-body.js +69 -189
- package/dist/app/projection-list-render-props.d.ts +56 -0
- package/dist/app/projection-list-render-props.js +47 -0
- package/dist/app/projection-list-render.d.ts +26 -0
- package/dist/app/projection-list-render.js +20 -0
- package/dist/app/projection-list-row-click.d.ts +12 -0
- package/dist/app/projection-list-row-click.js +14 -0
- package/dist/app/use-projection-detail-actions.d.ts +35 -0
- package/dist/app/use-projection-detail-actions.js +156 -0
- package/dist/app/use-projection-detail-data.d.ts +19 -0
- package/dist/app/use-projection-detail-data.js +47 -0
- package/dist/app/use-projection-detail-layout.d.ts +17 -0
- package/dist/app/use-projection-detail-layout.js +23 -0
- package/dist/app/use-projection-list-actions.d.ts +31 -0
- package/dist/app/use-projection-list-actions.js +73 -0
- package/dist/app/use-projection-list-data.d.ts +20 -0
- package/dist/app/use-projection-list-data.js +26 -0
- package/dist/app/use-projection-list-definitions.d.ts +11 -0
- package/dist/app/use-projection-list-definitions.js +20 -0
- package/dist/app/use-projection-list-query.d.ts +28 -0
- package/dist/app/use-projection-list-query.js +83 -0
- package/dist/components/render-edit-banners.d.ts +12 -0
- package/dist/components/render-edit-banners.js +90 -0
- package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
- package/dist/components/render-edit-draft-save-helpers.js +42 -0
- package/dist/components/render-edit-field-grid.d.ts +4 -0
- package/dist/components/render-edit-field-grid.js +40 -0
- package/dist/components/render-edit-fields-section.d.ts +4 -0
- package/dist/components/render-edit-fields-section.js +76 -0
- package/dist/components/render-edit-form-actions.d.ts +4 -0
- package/dist/components/render-edit-form-actions.js +66 -0
- package/dist/components/render-edit-header-actions.d.ts +11 -0
- package/dist/components/render-edit-header-actions.js +120 -0
- package/dist/components/render-edit-layout-flags.d.ts +29 -0
- package/dist/components/render-edit-layout-flags.js +106 -0
- package/dist/components/render-edit-layout.d.ts +4 -0
- package/dist/components/render-edit-layout.js +79 -0
- package/dist/components/render-edit-render-model.d.ts +63 -0
- package/dist/components/render-edit-render-model.js +14 -0
- package/dist/components/render-edit-section-kinds.d.ts +4 -0
- package/dist/components/render-edit-section-kinds.js +115 -0
- package/dist/components/render-edit-submit-result.d.ts +8 -0
- package/dist/components/render-edit-submit-result.js +77 -0
- package/dist/components/render-edit-submit.d.ts +7 -0
- package/dist/components/render-edit-submit.js +113 -0
- package/dist/components/render-edit-titles.d.ts +17 -0
- package/dist/components/render-edit-titles.js +99 -0
- package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
- package/dist/components/render-edit-wizard-navigation.js +115 -0
- package/dist/components/render-edit.d.ts +2 -2
- package/dist/components/render-edit.js +63 -1128
- package/dist/components/render-field-choice-inputs.d.ts +7 -0
- package/dist/components/render-field-choice-inputs.js +52 -0
- package/dist/components/render-field-control.d.ts +12 -0
- package/dist/components/render-field-control.js +93 -0
- package/dist/components/render-field-display-text.d.ts +4 -0
- package/dist/components/render-field-display-text.js +48 -0
- package/dist/components/render-field-fill-action.d.ts +13 -0
- package/dist/components/render-field-fill-action.js +63 -0
- package/dist/components/render-field-input-shared.d.ts +39 -0
- package/dist/components/render-field-input-shared.js +20 -0
- package/dist/components/render-field-input.d.ts +3 -0
- package/dist/components/render-field-input.js +34 -0
- package/dist/components/render-field-meta.d.ts +6 -0
- package/dist/components/render-field-meta.js +51 -0
- package/dist/components/render-field-read-only.d.ts +11 -0
- package/dist/components/render-field-read-only.js +75 -0
- package/dist/components/render-field-reference.d.ts +15 -0
- package/dist/components/render-field-reference.js +103 -0
- package/dist/components/render-field-scalar-inputs.d.ts +10 -0
- package/dist/components/render-field-scalar-inputs.js +35 -0
- package/dist/components/render-field-text-inputs.d.ts +3 -0
- package/dist/components/render-field-text-inputs.js +34 -0
- package/dist/components/render-field-values.d.ts +18 -0
- package/dist/components/render-field-values.js +111 -0
- package/dist/components/render-field-write-only.d.ts +9 -0
- package/dist/components/render-field-write-only.js +29 -0
- package/dist/components/render-field.d.ts +2 -2
- package/dist/components/render-field.js +39 -670
- package/dist/components/render-list-header-slot.d.ts +5 -0
- package/dist/components/render-list-header-slot.js +30 -0
- package/dist/components/render-list-reference-bridge.d.ts +3 -0
- package/dist/components/render-list-reference-bridge.js +25 -0
- package/dist/components/render-list-reference-columns.d.ts +25 -0
- package/dist/components/render-list-reference-columns.js +137 -0
- package/dist/components/render-list-table-props.d.ts +52 -0
- package/dist/components/render-list-table-props.js +53 -0
- package/dist/components/render-list-toolbar-model.d.ts +42 -0
- package/dist/components/render-list-toolbar-model.js +82 -0
- package/dist/components/render-list-toolbar.d.ts +38 -0
- package/dist/components/render-list-toolbar.js +40 -0
- package/dist/components/render-list-types.d.ts +113 -0
- package/dist/components/render-list-types.js +1 -0
- package/dist/components/render-list.d.ts +4 -113
- package/dist/components/render-list.js +91 -307
- package/dist/components/use-reference-input-state.d.ts +20 -0
- package/dist/components/use-reference-input-state.js +85 -0
- package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
- package/dist/components/use-render-edit-controlled-sync.js +26 -0
- package/dist/components/use-render-edit-draft-save.d.ts +29 -0
- package/dist/components/use-render-edit-draft-save.js +141 -0
- package/dist/components/use-render-edit-form.d.ts +10 -0
- package/dist/components/use-render-edit-form.js +101 -0
- package/dist/components/use-render-edit-sections.d.ts +15 -0
- package/dist/components/use-render-edit-sections.js +100 -0
- package/dist/components/use-render-edit-state.d.ts +65 -0
- package/dist/components/use-render-edit-state.js +158 -0
- package/dist/components/use-render-edit-steps.d.ts +13 -0
- package/dist/components/use-render-edit-steps.js +52 -0
- package/dist/components/use-render-list-columns.d.ts +18 -0
- package/dist/components/use-render-list-columns.js +41 -0
- package/dist/components/use-render-list-search.d.ts +4 -0
- package/dist/components/use-render-list-search.js +50 -0
- package/dist/primitives.d.ts +1 -1
- package/dist/web/app/dashboard-body.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +42 -246
- package/dist/web/app/dashboard-header.d.ts +34 -0
- package/dist/web/app/dashboard-header.js +58 -0
- package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
- package/dist/web/app/dashboard-screen-controls.js +42 -0
- package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
- package/dist/web/app/dashboard-screen-tiles.js +42 -0
- package/dist/web/app/dashboard-visibility.d.ts +42 -0
- package/dist/web/app/dashboard-visibility.js +102 -0
- package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
- package/dist/web/app/use-dashboard-screen-state.js +69 -0
- package/dist/web/primitives/data-table/actions-column.js +6 -5
- package/dist/web/primitives/data-table/card-body.d.ts +6 -0
- package/dist/web/primitives/data-table/card-body.js +32 -0
- package/dist/web/primitives/data-table/card-row.d.ts +5 -0
- package/dist/web/primitives/data-table/card-row.js +31 -0
- package/dist/web/primitives/data-table/card-view.d.ts +3 -0
- package/dist/web/primitives/data-table/card-view.js +13 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
- package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
- package/dist/web/primitives/data-table/data-cell-props.js +8 -0
- package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
- package/dist/web/primitives/data-table/row-render-parts.js +35 -0
- package/dist/web/primitives/data-table/table-body.d.ts +3 -0
- package/dist/web/primitives/data-table/table-body.js +32 -0
- package/dist/web/primitives/data-table/table-content.d.ts +3 -0
- package/dist/web/primitives/data-table/table-content.js +41 -0
- package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
- package/dist/web/primitives/data-table/table-data-cell.js +25 -0
- package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
- package/dist/web/primitives/data-table/table-header-row.js +21 -0
- package/dist/web/primitives/data-table/table-view.d.ts +3 -0
- package/dist/web/primitives/data-table/table-view.js +25 -0
- package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
- package/dist/web/primitives/data-table/toolbar.js +53 -0
- package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
- package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
- package/dist/web/primitives/data-table.d.ts +3 -4
- package/dist/web/primitives/data-table.js +32 -252
- package/dist/web/primitives/file-value-list.js +1 -1
- package/dist/web/primitives/form-footer-parts.d.ts +25 -0
- package/dist/web/primitives/form-footer-parts.js +55 -0
- package/dist/web/primitives/form.js +33 -43
- package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
- package/dist/web/primitives/input-choice-kinds.js +67 -0
- package/dist/web/primitives/input-file-kinds.d.ts +3 -0
- package/dist/web/primitives/input-file-kinds.js +6 -0
- package/dist/web/primitives/input-number-kinds.d.ts +4 -0
- package/dist/web/primitives/input-number-kinds.js +16 -0
- package/dist/web/primitives/input-optional-props.d.ts +20 -0
- package/dist/web/primitives/input-optional-props.js +24 -0
- package/dist/web/primitives/input-select-presentation.d.ts +13 -0
- package/dist/web/primitives/input-select-presentation.js +19 -0
- package/dist/web/primitives/input-shared.d.ts +22 -0
- package/dist/web/primitives/input-shared.js +10 -0
- package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
- package/dist/web/primitives/input-temporal-kinds.js +10 -0
- package/dist/web/primitives/input-text-kinds.d.ts +7 -0
- package/dist/web/primitives/input-text-kinds.js +33 -0
- package/dist/web/primitives/input.js +35 -161
- package/dist/web/widgets/chart-data.d.ts +35 -0
- package/dist/web/widgets/chart-data.js +37 -0
- package/dist/web/widgets/chart-markers.d.ts +26 -0
- package/dist/web/widgets/chart-markers.js +64 -0
- package/dist/web/widgets/chart-parts.d.ts +20 -0
- package/dist/web/widgets/chart-parts.js +15 -0
- package/dist/web/widgets/charts.d.ts +5 -78
- package/dist/web/widgets/charts.js +7 -485
- package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
- package/dist/web/widgets/drawer-panel-class.js +40 -0
- package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
- package/dist/web/widgets/drawer-resize-controls.js +12 -0
- package/dist/web/widgets/drawer-sizing.d.ts +19 -0
- package/dist/web/widgets/drawer-sizing.js +60 -0
- package/dist/web/widgets/drawer.js +17 -116
- package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
- package/dist/web/widgets/stacked-area-brush.js +91 -0
- package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
- package/dist/web/widgets/stacked-area-chart.js +45 -0
- package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
- package/dist/web/widgets/stacked-area-chrome.js +77 -0
- package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
- package/dist/web/widgets/stacked-area-geometry.js +83 -0
- package/dist/web/widgets/stacked-area-model.d.ts +11 -0
- package/dist/web/widgets/stacked-area-model.js +1 -0
- package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
- package/dist/web/widgets/stacked-area-plot.js +51 -0
- package/dist/web/widgets/stacked-area-props.d.ts +83 -0
- package/dist/web/widgets/stacked-area-props.js +16 -0
- package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
- package/dist/web/widgets/stacked-area-svg.js +38 -0
- package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
- package/dist/web/widgets/stacked-area-tooltip.js +86 -0
- package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
- package/dist/web/widgets/use-drawer-resize.js +52 -0
- package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
- package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
- package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
- package/dist/web/widgets/use-stacked-area-window.js +76 -0
- package/package.json +9 -9
- package/src/changes.json +54 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { EmbeddedScreenProvider, evalVisibleWhen, KumikoScreen, useEmbeddedScreen, } from "../../index.js";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
6
|
+
import { SectionCard } from "../widgets/section-card.js";
|
|
7
|
+
import { PanelError, WIDE_PANEL } from "./dashboard-shared.js";
|
|
8
|
+
import { NO_GATE_PAYLOAD, VisibilityQueryContext, visibilityGate, } from "./dashboard-visibility.js";
|
|
9
|
+
function EmbeddedScreenPanelTile({ panel, target, translate, }) {
|
|
10
|
+
const screenBody = _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
|
|
11
|
+
const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
|
|
12
|
+
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: panel.label !== undefined && panel.chromeless !== true ? (_jsx(SectionCard, { title: translate(panel.label), children: embedded })) : (_jsxs(_Fragment, { children: [panel.label !== undefined && (_jsx("h3", { className: "mb-3 text-sm font-semibold text-foreground", children: translate(panel.label) })), embedded] })) }));
|
|
13
|
+
}
|
|
14
|
+
// Own component so the target lookup (a hook) only runs for panels that actually fall back.
|
|
15
|
+
function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
|
|
16
|
+
const target = useEmbeddedScreen(featureName, fallback);
|
|
17
|
+
if (target === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
20
|
+
}
|
|
21
|
+
export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
22
|
+
const visibleWhen = panel.visibleWhen;
|
|
23
|
+
const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
|
|
24
|
+
const target = useEmbeddedScreen(featureName, panel.screen);
|
|
25
|
+
const verdict = visibleWhen === undefined ? "visible" : evalVisibleWhen(visibleWhen, visibility);
|
|
26
|
+
// Nothing while the gate loads: a fallback that flashes before the real panel flickers.
|
|
27
|
+
if (verdict === "loading")
|
|
28
|
+
return null;
|
|
29
|
+
if (verdict !== "visible" && panel.fallback !== undefined) {
|
|
30
|
+
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
|
|
31
|
+
}
|
|
32
|
+
if (target === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
// A failed gate query must not silently drop the panel: on the
|
|
35
|
+
// account-security page that would hide every MFA enable/disable path.
|
|
36
|
+
if (verdict === "error" && visibility?.error) {
|
|
37
|
+
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: panel.label !== undefined ? translate(panel.label) : panel.id, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
|
|
38
|
+
}
|
|
39
|
+
if (verdict !== "visible")
|
|
40
|
+
return null;
|
|
41
|
+
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DashboardPanelDefinition, DashboardScreenPanel } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { type UseQueryResult } from "../../index.ts";
|
|
4
|
+
import { type ScreenParams } from "./dashboard-shared.tsx";
|
|
5
|
+
type VisibilityQueryResult = UseQueryResult<Readonly<Record<string, unknown>>>;
|
|
6
|
+
export type VisibilityQueryResults = ReadonlyMap<string, VisibilityQueryResult>;
|
|
7
|
+
export declare const VisibilityQueryContext: import("react").Context<VisibilityQueryResults>;
|
|
8
|
+
export type VisibilityGate = {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
readonly query: string;
|
|
11
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
export declare const NO_GATE_PAYLOAD: Readonly<Record<string, unknown>>;
|
|
14
|
+
export declare function visibilityGate(query: string, payload: Readonly<Record<string, unknown>>): VisibilityGate;
|
|
15
|
+
export declare function VisibilityGateQueries({ gates, onResult, onForget, }: {
|
|
16
|
+
readonly gates: readonly VisibilityGate[];
|
|
17
|
+
readonly onResult: (key: string, result: VisibilityQueryResult) => void;
|
|
18
|
+
readonly onForget: (key: string) => void;
|
|
19
|
+
}): ReactNode;
|
|
20
|
+
export declare function useVisibilityGateResults(): {
|
|
21
|
+
readonly results: VisibilityQueryResults;
|
|
22
|
+
readonly report: (key: string, result: VisibilityQueryResult) => void;
|
|
23
|
+
readonly forget: (key: string) => void;
|
|
24
|
+
};
|
|
25
|
+
export declare function distinctVisibilityGates(panels: readonly DashboardPanelDefinition[], screenParams: ScreenParams): VisibilityGate[];
|
|
26
|
+
export declare function GatedGridCell({ panel, screenParams, label, className, children, }: {
|
|
27
|
+
readonly panel: Exclude<DashboardPanelDefinition, DashboardScreenPanel>;
|
|
28
|
+
readonly screenParams: ScreenParams;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly className: string | undefined;
|
|
31
|
+
readonly children: ReactNode;
|
|
32
|
+
}): ReactNode;
|
|
33
|
+
export declare function useDashboardVisibility(panels: readonly DashboardPanelDefinition[], screenParams: ScreenParams): {
|
|
34
|
+
visibilityGates: VisibilityGate[];
|
|
35
|
+
gateResults: {
|
|
36
|
+
readonly results: VisibilityQueryResults;
|
|
37
|
+
readonly report: (key: string, result: VisibilityQueryResult) => void;
|
|
38
|
+
readonly forget: (key: string) => void;
|
|
39
|
+
};
|
|
40
|
+
gateResultsWithPending: VisibilityQueryResults;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
3
|
+
import { evalVisibleWhen } from "../../index.js";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { PanelError, useDashboardQuery } from "./dashboard-shared.js";
|
|
6
|
+
export const VisibilityQueryContext = createContext(new Map());
|
|
7
|
+
export const NO_GATE_PAYLOAD = {};
|
|
8
|
+
export function visibilityGate(query, payload) {
|
|
9
|
+
return { key: `${query}|${JSON.stringify(payload)}`, query, payload };
|
|
10
|
+
}
|
|
11
|
+
// Embedded screens carry no screen filter; in-grid panels pass the filter
|
|
12
|
+
// (unless ignoreScreenFilter, never for a stat-group) and the time range.
|
|
13
|
+
function panelVisibilityGate(panel, screenParams) {
|
|
14
|
+
if (panel.kind === "custom" || panel.visibleWhen === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (panel.kind === "screen")
|
|
17
|
+
return visibilityGate(panel.visibleWhen.query, NO_GATE_PAYLOAD);
|
|
18
|
+
const dropsFilter = panel.kind !== "stat-group" && panel.kind !== "text" && panel.ignoreScreenFilter === true;
|
|
19
|
+
return visibilityGate(panel.visibleWhen.query, {
|
|
20
|
+
...(dropsFilter ? {} : screenParams.filterParams),
|
|
21
|
+
...screenParams.rangeParams,
|
|
22
|
+
...screenParams.recordParams,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// One live query per distinct gate (query + payload), shared by every panel
|
|
26
|
+
// that gates on it — N panels on the same status query would otherwise each
|
|
27
|
+
// open their own request, SSE subscription and refetch per event. The queries
|
|
28
|
+
// run as keyed siblings of the grid so a changing gate count never changes the
|
|
29
|
+
// grid's position in the tree (which would remount every panel).
|
|
30
|
+
function VisibilityGateQuery({ gate, onResult, onForget, }) {
|
|
31
|
+
const { data, error, loading, refetch } = useDashboardQuery(gate.query, gate.payload);
|
|
32
|
+
const { key } = gate;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
onResult(key, { data, error, loading, refetch });
|
|
35
|
+
}, [key, data, error, loading, refetch, onResult]);
|
|
36
|
+
useEffect(() => () => onForget(key), [key, onForget]);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export function VisibilityGateQueries({ gates, onResult, onForget, }) {
|
|
40
|
+
return gates.map((gate) => (_jsx(VisibilityGateQuery, { gate: gate, onResult: onResult, onForget: onForget }, gate.key)));
|
|
41
|
+
}
|
|
42
|
+
export function useVisibilityGateResults() {
|
|
43
|
+
const [results, setResults] = useState(new Map());
|
|
44
|
+
const report = useCallback((key, result) => {
|
|
45
|
+
setResults((prev) => new Map(prev).set(key, result));
|
|
46
|
+
}, []);
|
|
47
|
+
const forget = useCallback((key) => {
|
|
48
|
+
setResults((prev) => {
|
|
49
|
+
if (!prev.has(key))
|
|
50
|
+
return prev;
|
|
51
|
+
const next = new Map(prev);
|
|
52
|
+
next.delete(key);
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
}, []);
|
|
56
|
+
return { results, report, forget };
|
|
57
|
+
}
|
|
58
|
+
const NOOP_REFETCH = async () => { };
|
|
59
|
+
// A gate reports through an effect, so for its first commit (and after a payload change) no
|
|
60
|
+
// result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
|
|
61
|
+
function withPendingGates(results, gateKeys) {
|
|
62
|
+
if (gateKeys.every((key) => results.has(key)))
|
|
63
|
+
return results;
|
|
64
|
+
const merged = new Map(results);
|
|
65
|
+
for (const key of gateKeys) {
|
|
66
|
+
if (!merged.has(key)) {
|
|
67
|
+
merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return merged;
|
|
71
|
+
}
|
|
72
|
+
export function distinctVisibilityGates(panels, screenParams) {
|
|
73
|
+
const gates = new Map();
|
|
74
|
+
for (const panel of panels) {
|
|
75
|
+
const gate = panelVisibilityGate(panel, screenParams);
|
|
76
|
+
if (gate !== undefined)
|
|
77
|
+
gates.set(gate.key, gate);
|
|
78
|
+
}
|
|
79
|
+
return [...gates.values()];
|
|
80
|
+
}
|
|
81
|
+
export function GatedGridCell({ panel, screenParams, label, className, children, }) {
|
|
82
|
+
const gate = panelVisibilityGate(panel, screenParams);
|
|
83
|
+
const visibility = useContext(VisibilityQueryContext).get(gate?.key ?? "");
|
|
84
|
+
const visibleWhen = panel.kind === "custom" ? undefined : panel.visibleWhen;
|
|
85
|
+
if (visibleWhen !== undefined) {
|
|
86
|
+
const verdict = evalVisibleWhen(visibleWhen, visibility);
|
|
87
|
+
if (verdict === "error" && visibility?.error) {
|
|
88
|
+
return (_jsx("div", { className: cn("min-w-0", className), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: label, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
|
|
89
|
+
}
|
|
90
|
+
// Loading renders nothing too: a skeleton that then vanishes flickers.
|
|
91
|
+
if (verdict !== "visible")
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return _jsx("div", { className: cn("min-w-0", className), children: children });
|
|
95
|
+
}
|
|
96
|
+
export function useDashboardVisibility(panels, screenParams) {
|
|
97
|
+
const visibilityGates = distinctVisibilityGates(panels, screenParams);
|
|
98
|
+
const gateResults = useVisibilityGateResults();
|
|
99
|
+
const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
|
|
100
|
+
const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
|
|
101
|
+
return { visibilityGates, gateResults, gateResultsWithPending };
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ErrorNotice } from "../../app/error-reference.tsx";
|
|
3
|
+
import type { Translate } from "../../headless/index.ts";
|
|
4
|
+
import { type useNav } from "../../index.ts";
|
|
5
|
+
import { type ScreenParams } from "./dashboard-shared.tsx";
|
|
6
|
+
type Nav = ReturnType<typeof useNav>;
|
|
7
|
+
export declare function useDashboardScreenParams(screen: DashboardScreenDefinition, translate: Translate, nav: Nav): {
|
|
8
|
+
recordBinding: import("@cosmicdrift/kumiko-types/screen").DashboardRecordBinding | undefined;
|
|
9
|
+
recordId: string;
|
|
10
|
+
recordMissing: boolean;
|
|
11
|
+
recordPayloadKey: string | undefined;
|
|
12
|
+
recordParams: Readonly<Record<string, unknown>>;
|
|
13
|
+
recordQueryName: string | undefined;
|
|
14
|
+
picker: import("react").ReactNode;
|
|
15
|
+
rangeControl: import("react").ReactNode;
|
|
16
|
+
screenParams: ScreenParams;
|
|
17
|
+
loadedAtMs: number;
|
|
18
|
+
reportLoaded: (atMs: number) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function useDashboardRecord(screen: DashboardScreenDefinition, params: Pick<ReturnType<typeof useDashboardScreenParams>, "recordQueryName" | "recordParams" | "recordMissing">): {
|
|
21
|
+
recordQuery: import("../../index.ts").UseQueryResult<Readonly<Record<string, unknown>>>;
|
|
22
|
+
recordRow: Readonly<Record<string, unknown>> | null;
|
|
23
|
+
refetchRecord: () => Promise<void>;
|
|
24
|
+
queryRuntime: {
|
|
25
|
+
refreshIntervalMs: number | undefined;
|
|
26
|
+
register: (refetch: () => Promise<void>) => () => void;
|
|
27
|
+
};
|
|
28
|
+
refetchAll: () => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
export declare function useDashboardHeaderActions(screen: DashboardScreenDefinition, params: Pick<ReturnType<typeof useDashboardScreenParams>, "recordPayloadKey" | "recordId" | "recordQueryName" | "recordMissing">, record: Pick<ReturnType<typeof useDashboardRecord>, "recordRow" | "refetchAll">, translate: Translate): {
|
|
31
|
+
actionError: ErrorNotice | null;
|
|
32
|
+
setActionError: import("react").Dispatch<import("react").SetStateAction<ErrorNotice | null>>;
|
|
33
|
+
headerActions: readonly import("../../index.ts").RenderEditAction[] | undefined;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useQuery, useRecordActions } from "../../index.js";
|
|
3
|
+
import { resolveRefreshIntervalMs } from "./dashboard-refresh.js";
|
|
4
|
+
import { useFilterParams, useTimeRange } from "./dashboard-screen-controls.js";
|
|
5
|
+
import { useLoadedAt, useRefetchRegistry } from "./dashboard-shared.js";
|
|
6
|
+
export function useDashboardScreenParams(screen, translate, nav) {
|
|
7
|
+
const recordBinding = screen.record;
|
|
8
|
+
const recordId = recordBinding !== undefined ? (nav.searchParams[recordBinding.param] ?? "") : "";
|
|
9
|
+
const recordMissing = recordBinding !== undefined && recordId === "";
|
|
10
|
+
const recordPayloadKey = recordBinding?.payloadKey ?? recordBinding?.param;
|
|
11
|
+
const recordParams = recordPayloadKey !== undefined && recordId !== "" ? { [recordPayloadKey]: recordId } : {};
|
|
12
|
+
const recordQueryName = recordBinding?.query;
|
|
13
|
+
const { params: filterParams, picker } = useFilterParams(screen, !recordMissing);
|
|
14
|
+
const { params: rangeParams, control: rangeControl } = useTimeRange(screen, translate);
|
|
15
|
+
const screenParams = { filterParams, rangeParams, recordParams };
|
|
16
|
+
const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
|
|
17
|
+
return {
|
|
18
|
+
recordBinding,
|
|
19
|
+
recordId,
|
|
20
|
+
recordMissing,
|
|
21
|
+
recordPayloadKey,
|
|
22
|
+
recordParams,
|
|
23
|
+
recordQueryName,
|
|
24
|
+
picker,
|
|
25
|
+
rangeControl,
|
|
26
|
+
screenParams,
|
|
27
|
+
loadedAtMs,
|
|
28
|
+
reportLoaded,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function useDashboardRecord(screen, params) {
|
|
32
|
+
const { recordQueryName, recordParams, recordMissing } = params;
|
|
33
|
+
// The record query is not a panel query: it needs the interval before the
|
|
34
|
+
// registry context below exists, and `refresh.field` reads from its own row.
|
|
35
|
+
const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
|
|
36
|
+
const recordQuery = useQuery(recordQueryName ?? "", recordParams, {
|
|
37
|
+
enabled: recordQueryName !== undefined && !recordMissing,
|
|
38
|
+
live: true,
|
|
39
|
+
...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
|
|
40
|
+
});
|
|
41
|
+
const { data: recordRow, refetch: refetchRecord } = recordQuery;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setRefreshIntervalMs(resolveRefreshIntervalMs(screen.refresh, recordRow));
|
|
44
|
+
}, [screen.refresh, recordRow]);
|
|
45
|
+
const { register, refetchRegistered } = useRefetchRegistry();
|
|
46
|
+
const queryRuntime = useMemo(() => ({ refreshIntervalMs, register }), [refreshIntervalMs, register]);
|
|
47
|
+
const refetchAll = useCallback(async () => {
|
|
48
|
+
await Promise.all([
|
|
49
|
+
recordQueryName !== undefined ? refetchRecord() : Promise.resolve(),
|
|
50
|
+
refetchRegistered(),
|
|
51
|
+
]);
|
|
52
|
+
}, [recordQueryName, refetchRecord, refetchRegistered]);
|
|
53
|
+
return { recordQuery, recordRow, refetchRecord, queryRuntime, refetchAll };
|
|
54
|
+
}
|
|
55
|
+
const NO_RECORD_ROW = {};
|
|
56
|
+
export function useDashboardHeaderActions(screen, params, record, translate) {
|
|
57
|
+
const { recordPayloadKey, recordId, recordQueryName, recordMissing } = params;
|
|
58
|
+
const [actionError, setActionError] = useState(null);
|
|
59
|
+
const idOnlyRow = useMemo(() => (recordPayloadKey !== undefined ? { id: recordId, [recordPayloadKey]: recordId } : null), [recordId, recordPayloadKey]);
|
|
60
|
+
const actionRow = recordQueryName !== undefined ? record.recordRow : idOnlyRow;
|
|
61
|
+
const headerActions = useRecordActions({
|
|
62
|
+
actions: actionRow !== null && !recordMissing ? screen.actions : undefined,
|
|
63
|
+
record: actionRow ?? NO_RECORD_ROW,
|
|
64
|
+
recordId,
|
|
65
|
+
translate,
|
|
66
|
+
onWriteSuccess: record.refetchAll,
|
|
67
|
+
});
|
|
68
|
+
return { actionError, setActionError, headerActions };
|
|
69
|
+
}
|
|
@@ -26,9 +26,10 @@ export function ActionsBodyCell({ testId, fillsHeight, measureContent, children,
|
|
|
26
26
|
// viewports. bg-background sets the column apart during
|
|
27
27
|
// scroll — no border-l (divider too heavy).
|
|
28
28
|
className: cn("whitespace-nowrap text-right", stickyActionsCellClass(fillsHeight), ROW_FOCUS_RING_CELL_CLASS),
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// Only under column sizing is the empty cell leftover width that belongs to the row;
|
|
30
|
+
// otherwise it is padding around the buttons, where a misclick must not open the row.
|
|
31
|
+
onClick: (e) => {
|
|
32
|
+
if (!measureContent || e.target !== e.currentTarget)
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
}, onKeyDown: (e) => e.stopPropagation(), children: measureContent ? (_jsx("div", { [ACTIONS_CONTENT_ATTRIBUTE]: "", className: "ml-auto w-max", children: children })) : (children) }));
|
|
34
35
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { cardColumnRoles } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { ListColumnViewModel, ListRowViewModel } from "../../../headless/index.ts";
|
|
4
|
+
import type { DataTableModel } from "./use-data-table-state.ts";
|
|
5
|
+
export type CardRoles = ReturnType<typeof cardColumnRoles<ListColumnViewModel>>;
|
|
6
|
+
export declare function renderCardBody(m: DataTableModel, row: ListRowViewModel, roles: CardRoles): ReactNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import { cellLinkProp, DataTableCell, isBadgeColumn, isEmptyCellValue, isUnlabeledFalse, showsBareCheckMark, } from "../data-table-cell.js";
|
|
4
|
+
import { CardLeadingImage } from "../file-thumbnail.js";
|
|
5
|
+
import { columnCellPresentationProps } from "./data-cell-props.js";
|
|
6
|
+
import { showFullTextOnHover } from "./table-footer.js";
|
|
7
|
+
function cardCell(m, row, col) {
|
|
8
|
+
const { onCellChange } = m;
|
|
9
|
+
// A bare check mark in the subtitle line says nothing without its column.
|
|
10
|
+
if (showsBareCheckMark(col.type, row.values[col.field], col.renderer))
|
|
11
|
+
return col.label;
|
|
12
|
+
return (_jsx(DataTableCell, { value: row.values[col.field], row: row.values, field: col.field, type: col.type, renderer: col.renderer, translate: m.tableTranslate, locale: m.tableLocale, ...columnCellPresentationProps(col), ...(col.fileDisplay !== undefined && { fileDisplay: col.fileDisplay, compactFile: true }), ...(onCellChange !== undefined && {
|
|
13
|
+
onChange: (value) => onCellChange(row.id, col.field, value),
|
|
14
|
+
}), ...cellLinkProp(col, row, m.rowOpener(row) === undefined) }));
|
|
15
|
+
}
|
|
16
|
+
function cellTestIdOf(m, row, field) {
|
|
17
|
+
return m.getCellTestId?.(row, field) ?? `cell-${row.id}-${field}`;
|
|
18
|
+
}
|
|
19
|
+
function renderCardMeta(m, row, metaColumns) {
|
|
20
|
+
return (_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": cellTestIdOf(m, row, col.field), className: "min-w-0 truncate leading-5", onPointerEnter: showFullTextOnHover, children: isBadgeColumn(col) ? (_jsx("span", { className: "inline-flex align-middle", children: cardCell(m, row, col) })) : (cardCell(m, row, col)) })] }, col.field))) }) }));
|
|
21
|
+
}
|
|
22
|
+
function renderCardHeading(m, row, roles) {
|
|
23
|
+
const { title: cardTitleColumn, status: cardStatusColumn } = roles;
|
|
24
|
+
const showStatus = cardStatusColumn !== undefined && !isEmptyCellValue(row.values[cardStatusColumn.field]);
|
|
25
|
+
return (_jsxs("div", { className: "flex min-w-0 items-start justify-between gap-2", children: [cardTitleColumn !== undefined && (_jsx("span", { "data-testid": cellTestIdOf(m, row, cardTitleColumn.field), className: "min-w-0 truncate text-base font-semibold text-foreground", onPointerEnter: showFullTextOnHover, children: cardCell(m, row, cardTitleColumn) })), showStatus && (_jsx("span", { "data-testid": cellTestIdOf(m, row, cardStatusColumn.field), className: "shrink-0", children: cardCell(m, row, cardStatusColumn) }))] }));
|
|
26
|
+
}
|
|
27
|
+
export function renderCardBody(m, row, roles) {
|
|
28
|
+
const { image: cardImageColumn, meta: cardMetaColumns } = roles;
|
|
29
|
+
const metaColumns = cardMetaColumns.filter((col) => !isEmptyCellValue(row.values[col.field]) &&
|
|
30
|
+
!isUnlabeledFalse(col.type, row.values[col.field], col.renderer));
|
|
31
|
+
return (_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: [renderCardHeading(m, row, roles), metaColumns.length > 0 && renderCardMeta(m, row, metaColumns)] }), m.rowOpener(row) !== undefined && (_jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }))] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ListRowViewModel } from "../../../headless/index.ts";
|
|
3
|
+
import { type CardRoles } from "./card-body.tsx";
|
|
4
|
+
import type { DataTableModel } from "./use-data-table-state.ts";
|
|
5
|
+
export declare function renderCard(m: DataTableModel, row: ListRowViewModel, roles: CardRoles): ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { rowToneClass } from "../data-table-cell.js";
|
|
5
|
+
import { RowActionsCell } from "../row-actions.js";
|
|
6
|
+
import { renderCardBody } from "./card-body.js";
|
|
7
|
+
import { renderExpandToggle, renderExpansion } from "./row-render-parts.js";
|
|
8
|
+
function renderCardMenu(m, row, menuActions) {
|
|
9
|
+
return (_jsx("div", { className: "shrink-0", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(RowActionsCell, { row: row, actions: menuActions, mode: m.rowActionMode, rowIsLink: m.onRowClick !== undefined, compact: true }) }));
|
|
10
|
+
}
|
|
11
|
+
function renderCardOpenButton(openRow, body) {
|
|
12
|
+
return (_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) => {
|
|
13
|
+
if (e.key !== "Enter")
|
|
14
|
+
return;
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
e.stopPropagation();
|
|
17
|
+
openRow();
|
|
18
|
+
}, children: body }));
|
|
19
|
+
}
|
|
20
|
+
export function renderCard(m, row, roles) {
|
|
21
|
+
const { rowTone, menuActions } = m;
|
|
22
|
+
const hasMenu = menuActions !== undefined && menuActions.length > 0;
|
|
23
|
+
const openRow = m.rowOpener(row);
|
|
24
|
+
const body = renderCardBody(m, row, roles);
|
|
25
|
+
const rowItem = (_jsxs("li", { "data-testid": m.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)), m.expansion !== undefined || m.selection.active ? "pl-2" : "pl-4", hasMenu ? "pr-2" : "pr-4", m.density.card), ...(openRow !== undefined && { onClick: openRow }), children: [m.selection.cardCheckbox(row), renderExpandToggle(m, row), openRow !== undefined ? renderCardOpenButton(openRow, body) : body, hasMenu && renderCardMenu(m, row, menuActions)] }, row.id));
|
|
26
|
+
if (m.expansion === undefined)
|
|
27
|
+
return rowItem;
|
|
28
|
+
// The expansion is a sibling <li>, outside the row's click target, so
|
|
29
|
+
// clicks in the sub-area never navigate the parent row.
|
|
30
|
+
return (_jsxs(Fragment, { children: [rowItem, renderExpansion(m, row, "item", 0)] }, row.id));
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cardColumnRoles } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { renderCard } from "./card-row.js";
|
|
5
|
+
import { CardSortSelect } from "./card-sort-select.js";
|
|
6
|
+
import { bandClassName, bandReactKey, bandTestId } from "./group-band.js";
|
|
7
|
+
import { renderBand } from "./row-render-parts.js";
|
|
8
|
+
import { TotalsCard } from "./totals-footer.js";
|
|
9
|
+
export function renderCardsView(m) {
|
|
10
|
+
const { testId, totals, columns } = m;
|
|
11
|
+
const roles = cardColumnRoles(columns);
|
|
12
|
+
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: m.sort, onSortChange: m.onSortChange, testId: testId }), _jsx("ul", { className: "m-0 list-none border-t border-border-row p-0", children: m.tableItems.map((item) => item.kind === "row" ? (renderCard(m, item.row, roles)) : (_jsx("li", { "data-testid": bandTestId(item), className: cn("border-b border-border-row", bandClassName(item)), children: renderBand(m, item) }, bandReactKey(item)))) }), totals !== undefined && _jsx(TotalsCard, { totals: totals, columnOf: m.columnOf })] }) }));
|
|
13
|
+
}
|
|
@@ -39,7 +39,9 @@ export function ColumnResizeHandle({ field, label, sizing, }) {
|
|
|
39
39
|
event.preventDefault();
|
|
40
40
|
sizing.commit(field, clampToBounds(width + delta.dx));
|
|
41
41
|
};
|
|
42
|
-
return (_jsx("hr", { "aria-orientation": "vertical", "aria-valuenow": Math.round(width), "aria-valuemin": minWidth, "aria-valuemax": MAX_COLUMN_WIDTH_PX,
|
|
42
|
+
return (_jsx("hr", { "aria-orientation": "vertical", "aria-valuenow": Math.round(width), "aria-valuemin": minWidth, "aria-valuemax": MAX_COLUMN_WIDTH_PX,
|
|
43
|
+
// title, not aria-label: getByLabel("<column>") would match a resize handle for every column.
|
|
44
|
+
title: translate?.("kumiko.list.column.resize", { column: label }) ?? `Resize ${label}`, tabIndex: 0, "data-testid": `column-resize-${field}`, className: HANDLE_CLASS, onClick: (event) => event.stopPropagation(), onKeyDown: onKeyDown, ...drag, onPointerDown: (event) => {
|
|
43
45
|
event.stopPropagation();
|
|
44
46
|
drag.onPointerDown(event);
|
|
45
47
|
} }));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ListColumnViewModel } from "../../../headless/index.ts";
|
|
2
|
+
/** Optional presentation props every DataTableCell gets from its column. */
|
|
3
|
+
export declare function columnCellPresentationProps(col: ListColumnViewModel): {
|
|
4
|
+
optionLabels?: Readonly<Record<string, string>> | undefined;
|
|
5
|
+
optionTones?: Readonly<Partial<Record<string, "bad" | "neutral" | "ok" | "warn">>> | undefined;
|
|
6
|
+
grouping?: boolean | undefined;
|
|
7
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Optional presentation props every DataTableCell gets from its column. */
|
|
2
|
+
export function columnCellPresentationProps(col) {
|
|
3
|
+
return {
|
|
4
|
+
...(col.optionLabels !== undefined && { optionLabels: col.optionLabels }),
|
|
5
|
+
...(col.optionTones !== undefined && { optionTones: col.optionTones }),
|
|
6
|
+
...(col.grouping !== undefined && { grouping: col.grouping }),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ListRowViewModel } from "../../../headless/index.ts";
|
|
3
|
+
import { type DataTableBandItem } from "./group-band.tsx";
|
|
4
|
+
import type { DataTableModel } from "./use-data-table-state.ts";
|
|
5
|
+
export declare function renderExpandToggle(m: DataTableModel, row: ListRowViewModel): ReactNode;
|
|
6
|
+
export declare function renderExpansion(m: DataTableModel, row: ListRowViewModel, as: "row" | "item", colSpan: number): ReactNode;
|
|
7
|
+
export declare function renderBand(m: DataTableModel, item: DataTableBandItem): ReactNode;
|
|
8
|
+
export declare function renderEmptyBlock(m: DataTableModel): ReactNode;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
import { TableCell, TableRow } from "../../ui/table.js";
|
|
4
|
+
import { cellTitle } from "../data-table-cell.js";
|
|
5
|
+
import { GroupBand } from "./group-band.js";
|
|
6
|
+
import { EXPANSION_CELL_CLASS, EXPANSION_CONTENT_CLASS, RowExpandToggle } from "./table-chrome.js";
|
|
7
|
+
// The expand toggle of a row, shared by table and card layout. Named after
|
|
8
|
+
// the first data column, the one a user recognizes the row by.
|
|
9
|
+
export function renderExpandToggle(m, row) {
|
|
10
|
+
const { expansion, expandedRowIds, columns, tableTranslate } = m;
|
|
11
|
+
if (expansion === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const expanded = expandedRowIds?.has(row.id) === true;
|
|
14
|
+
const firstColumn = columns[0];
|
|
15
|
+
const title = (firstColumn !== undefined ? cellTitle(row.values[firstColumn.field]) : undefined) ?? "";
|
|
16
|
+
const labelKey = expanded ? "kumiko.list.row.collapse" : "kumiko.list.row.expand";
|
|
17
|
+
return (_jsx(RowExpandToggle, { expanded: expanded, controlsId: m.expansionDomId(row), label: tableTranslate?.(labelKey, { title }) ?? `${expanded ? "Collapse" : "Expand"} ${title}`, onToggle: () => expansion.onToggleRowExpanded(row.id), testId: `${m.rowTestId(row)}-toggle` }));
|
|
18
|
+
}
|
|
19
|
+
export function renderExpansion(m, row, as, colSpan) {
|
|
20
|
+
const { expansion, expandedRowIds } = m;
|
|
21
|
+
if (expansion === undefined || expandedRowIds?.has(row.id) !== true)
|
|
22
|
+
return null;
|
|
23
|
+
const content = expansion.renderExpandedRow(row);
|
|
24
|
+
if (as === "item") {
|
|
25
|
+
return (_jsx("li", { id: m.expansionDomId(row), "data-testid": `${m.rowTestId(row)}-expansion`, className: "border-b border-border-row bg-muted px-4 pt-1 pb-3.5", children: content }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(TableRow, { id: m.expansionDomId(row), "data-testid": `${m.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 }) }) }));
|
|
28
|
+
}
|
|
29
|
+
export function renderBand(m, item) {
|
|
30
|
+
return _jsx(GroupBand, { item: item, columnOf: m.columnOf });
|
|
31
|
+
}
|
|
32
|
+
export function renderEmptyBlock(m) {
|
|
33
|
+
const { testId, chromeless, fillsHeight, emptyState, tableTranslate } = m;
|
|
34
|
+
return (_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." }) }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from "react";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { TableBody, TableCell, TableRow } from "../../ui/table.js";
|
|
5
|
+
import { rowToneClass } from "../data-table-cell.js";
|
|
6
|
+
import { RowActionsCell } from "../row-actions.js";
|
|
7
|
+
import { ActionsBodyCell } from "./actions-column.js";
|
|
8
|
+
import { bandClassName, bandReactKey, bandTestId } from "./group-band.js";
|
|
9
|
+
import { renderBand, renderExpandToggle, renderExpansion } from "./row-render-parts.js";
|
|
10
|
+
import { EXPAND_TOGGLE_CELL_CLASS } from "./table-chrome.js";
|
|
11
|
+
import { renderDataCell } from "./table-data-cell.js";
|
|
12
|
+
import { EXPAND_COLUMN_KEY, SELECT_COLUMN_KEY } from "./use-column-pins.js";
|
|
13
|
+
function renderBandRow(m, item) {
|
|
14
|
+
return (_jsx(TableRow, { "data-testid": bandTestId(item), children: _jsx(TableCell, { colSpan: m.fullColSpan, className: bandClassName(item), children: renderBand(m, item) }) }, bandReactKey(item)));
|
|
15
|
+
}
|
|
16
|
+
function renderExpandCell(m, row) {
|
|
17
|
+
const expandBodyPin = m.pins.bodyCell(EXPAND_COLUMN_KEY);
|
|
18
|
+
return (_jsx(TableCell, { "data-testid": m.getCellTestId?.(row, "expand") ?? `cell-${row.id}-expand`, className: cn(EXPAND_TOGGLE_CELL_CLASS, expandBodyPin.className), style: expandBodyPin.style, children: renderExpandToggle(m, row) }));
|
|
19
|
+
}
|
|
20
|
+
function renderActionsCell(m, row, tableActions) {
|
|
21
|
+
return (_jsx(ActionsBodyCell, { testId: m.getCellTestId?.(row, "actions") ?? `cell-${row.id}-actions`, fillsHeight: m.fillsHeight, measureContent: m.sizing !== undefined, children: _jsx(RowActionsCell, { row: row, actions: tableActions, mode: m.rowActionMode, rowIsLink: m.onRowClick !== undefined }) }));
|
|
22
|
+
}
|
|
23
|
+
function renderDataRow(m, row) {
|
|
24
|
+
const { rowTone, density, tableActions } = m;
|
|
25
|
+
const openRow = m.rowOpener(row);
|
|
26
|
+
const opensOnClick = openRow !== undefined;
|
|
27
|
+
const keyboard = m.keyboardRow(row.id, openRow);
|
|
28
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { "data-testid": m.rowTestId(row), "data-tone": rowTone?.(row), onClick: openRow, className: cn(rowToneClass(rowTone?.(row)), opensOnClick && "cursor-pointer", m.fillsHeight && "h-10 border-border-row hover:bg-muted", density.row, keyboard.className), ...keyboard.props, children: [m.selection.bodyCell(row, m.pins.bodyCell(SELECT_COLUMN_KEY)), m.expansion !== undefined && renderExpandCell(m, row), m.columns.map((col, colIndex) => renderDataCell(m, row, col, colIndex, opensOnClick)), m.hasTableActions && tableActions !== undefined && renderActionsCell(m, row, tableActions)] }), renderExpansion(m, row, "row", m.fullColSpan)] }, row.id));
|
|
29
|
+
}
|
|
30
|
+
export function renderTableBody(m) {
|
|
31
|
+
return (_jsx(TableBody, { children: m.tableItems.map((item) => item.kind === "row" ? renderDataRow(m, item.row) : renderBandRow(m, item)) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { InfiniteSentinel, Pager } from "../pager.js";
|
|
3
|
+
import { renderCardsView } from "./card-view.js";
|
|
4
|
+
import { renderEmptyBlock } from "./row-render-parts.js";
|
|
5
|
+
import { DataTableFooter } from "./table-footer.js";
|
|
6
|
+
import { renderTableView } from "./table-view.js";
|
|
7
|
+
// The pager is ALWAYS rendered below the table (even with rows=[]) so the
|
|
8
|
+
// user can page back on a filter hit of zero without losing the list.
|
|
9
|
+
// Except total === 0: then there is nothing to paginate. Incompatible with
|
|
10
|
+
// infinite scroll: the caller sets either pager OR onReachEnd.
|
|
11
|
+
function renderPager(m) {
|
|
12
|
+
const { pager, serverGroups, itemNoun, testId } = m;
|
|
13
|
+
if (pager === undefined || !(pager.total > 0) || serverGroups !== undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
return (_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: m.fillsHeight }));
|
|
16
|
+
}
|
|
17
|
+
function renderSentinel(m, pagerElement) {
|
|
18
|
+
const { onReachEnd, serverGroups, testId } = m;
|
|
19
|
+
if (pagerElement !== undefined || onReachEnd === undefined || serverGroups !== undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return (_jsx(InfiniteSentinel, { onReachEnd: onReachEnd, loadingMore: m.loadingMore === true, hasMore: m.hasMore !== false, rowsCount: m.rows.length, testId: testId !== undefined ? `${testId}-sentinel` : "render-list-sentinel" }));
|
|
23
|
+
}
|
|
24
|
+
function renderTableBodyContent(m) {
|
|
25
|
+
if (m.isEmpty)
|
|
26
|
+
return renderEmptyBlock(m);
|
|
27
|
+
return m.isNarrow ? renderCardsView(m) : renderTableView(m);
|
|
28
|
+
}
|
|
29
|
+
export function renderTableContent(m) {
|
|
30
|
+
const { testId, itemNoun, serverGroups } = m;
|
|
31
|
+
const tableContent = renderTableBodyContent(m);
|
|
32
|
+
const pagerElement = renderPager(m);
|
|
33
|
+
const sentinelElement = renderSentinel(m, pagerElement);
|
|
34
|
+
// With scrollBody the footer (pager, else the entry count) sits OUTSIDE the
|
|
35
|
+
// scroll surface so it stays put at any row count; the infinite sentinel
|
|
36
|
+
// stays INSIDE, or it would never intersect the scrollport.
|
|
37
|
+
if (!m.fillsHeight) {
|
|
38
|
+
return (_jsxs(_Fragment, { children: [m.selection.bar, tableContent, pagerElement, sentinelElement] }));
|
|
39
|
+
}
|
|
40
|
+
return (_jsxs(_Fragment, { children: [m.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 ?? m.rows.length, ...(itemNoun !== undefined && { itemNoun }), testId: testId !== undefined ? `${testId}-footer` : "render-list-footer" }))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ListColumnViewModel, ListRowViewModel } from "../../../headless/index.ts";
|
|
3
|
+
import type { DataTableModel } from "./use-data-table-state.ts";
|
|
4
|
+
export declare function renderDataCell(m: DataTableModel, row: ListRowViewModel, col: ListColumnViewModel, colIndex: number, opensOnClick: boolean): ReactNode;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
import { TableCell } from "../../ui/table.js";
|
|
4
|
+
import { cellLinkProp, cellTitle, DataTableCell, FIT_CONTENT_CELL_CLASS, FirstCellLink, isEmptyCellValue, NUMERIC_COLUMN_TYPES, TABULAR_COLUMN_TYPES, } from "../data-table-cell.js";
|
|
5
|
+
import { columnCellPresentationProps } from "./data-cell-props.js";
|
|
6
|
+
// Text wraps instead of truncating: nowrap cells keep their
|
|
7
|
+
// full width, so a wide table scrolled sideways and its
|
|
8
|
+
// last column slid under the sticky actions column with
|
|
9
|
+
// nothing visibly cut off. max-w-xs bounds a single column;
|
|
10
|
+
// badges and dates stay nowrap by their own classes.
|
|
11
|
+
function dataCellClassName(m, col, colIndex, pinClassName) {
|
|
12
|
+
return cn(col.fileDisplay === undefined
|
|
13
|
+
? cn(m.sizing ? "overflow-hidden" : "max-w-xs", "whitespace-normal break-words")
|
|
14
|
+
: FIT_CONTENT_CELL_CLASS, colIndex === 0 ? "font-medium text-foreground" : "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", m.density.cell, pinClassName);
|
|
15
|
+
}
|
|
16
|
+
export function renderDataCell(m, row, col, colIndex, opensOnClick) {
|
|
17
|
+
const { getCellTestId, onCellChange, onRowClick, tableTranslate, tableLocale } = m;
|
|
18
|
+
const bodyPin = m.pins.bodyCell(col.field);
|
|
19
|
+
const linksFirstCell = colIndex === 0 && opensOnClick && onCellChange === undefined;
|
|
20
|
+
return (_jsx(TableCell, { "data-testid": getCellTestId?.(row, col.field) ?? `cell-${row.id}-${col.field}`, "data-highlighted": col.highlighted === true ? "true" : undefined, className: dataCellClassName(m, col, colIndex, bodyPin.className), style: bodyPin.style, title: cellTitle(row.values[col.field]), children: _jsx(FirstCellLink, { enabled: linksFirstCell, onOpen: () => onRowClick?.(row), empty: isEmptyCellValue(row.values[col.field]), emptyLabel: m.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, ...columnCellPresentationProps(col), ...(col.fileDisplay !== undefined && {
|
|
21
|
+
fileDisplay: col.fileDisplay,
|
|
22
|
+
}), ...(onCellChange !== undefined && {
|
|
23
|
+
onChange: (value) => onCellChange(row.id, col.field, value),
|
|
24
|
+
}), ...cellLinkProp(col, row, !linksFirstCell) }) }) }, col.field));
|
|
25
|
+
}
|