@cosmicdrift/kumiko-renderer 0.388.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/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 +15 -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-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/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/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 +37 -89
- package/dist/components/render-field.js +6 -5
- 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 +42 -50
- 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/headless/view-model/edit.js +3 -1
- package/dist/headless/view-model/types.d.ts +4 -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/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-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 +53 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/primitives.d.ts +11 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +7 -4
- 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 +1 -0
- package/dist/web/index.js +1 -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/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 +122 -144
- 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/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +48 -26
- package/dist/web/widgets/pdf-preview.js +9 -4
- package/package.json +5 -5
- package/src/changes.json +109 -0
package/dist/primitives.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ConfigCascade, ConfigScope, ConfigValueSource } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import type { FieldIconKey, FormWidth, IconKey, NavIconKey, RowActionDisplay, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { type ComponentType, type FormEvent, type KeyboardEvent, type ReactNode, type Ref } from "react";
|
|
4
|
+
import type { DataTableBulk } from "./data-table-bulk-types.ts";
|
|
5
|
+
import type { DataTableDateRangeFacet, DataTableFacet } from "./data-table-facet-types.ts";
|
|
6
|
+
import type { DataTableTableBlock } from "./data-table-view-types.ts";
|
|
4
7
|
import type { FieldIssue, ListColumnViewModel, ListRowViewModel, RuntimeRenderer } from "./headless/index.ts";
|
|
5
8
|
export type { RuntimeRenderer };
|
|
6
9
|
/** Escape hatch for `data-*` test hooks (e.g. an E2E selector) that don't
|
|
@@ -647,55 +650,11 @@ export type DataTableRowAction = {
|
|
|
647
650
|
export declare function shouldRenderActionsIconOnly(actions: readonly {
|
|
648
651
|
readonly icon?: IconKey;
|
|
649
652
|
}[]): boolean;
|
|
650
|
-
export type
|
|
651
|
-
|
|
652
|
-
readonly field: string;
|
|
653
|
-
/** Translated Button-Label ("Status", "Active"). */
|
|
654
|
-
readonly label: string;
|
|
655
|
-
/** Selectable values with display labels. `value` is the raw filter value
|
|
656
|
-
* (a select option or `"true"`/`"false"`), transported as a string. */
|
|
657
|
-
readonly options: readonly {
|
|
658
|
-
readonly value: string;
|
|
659
|
-
readonly label: string;
|
|
660
|
-
}[];
|
|
661
|
-
/** Present: render the facet as a row of single-choice toggle chips instead
|
|
662
|
-
* of a dropdown. A chip is pressed while the selection equals its values. */
|
|
663
|
-
readonly chips?: readonly DataTableFacetChip[];
|
|
664
|
-
/** Chips only: show each chip's `count` next to its label. */
|
|
665
|
-
readonly showCounts?: boolean;
|
|
666
|
-
/** Chips only: hide chips whose `count` is 0 (unless pressed). */
|
|
667
|
-
readonly hideEmpty?: boolean;
|
|
668
|
-
};
|
|
669
|
-
export type DataTableFacetChip = {
|
|
670
|
-
readonly id: string;
|
|
671
|
-
/** Translated chip label. */
|
|
672
|
-
readonly label: string;
|
|
673
|
-
/** Selection a click sets; `[]` clears the facet. */
|
|
674
|
-
readonly values: readonly string[];
|
|
675
|
-
/** Matching row count, undefined while unknown. */
|
|
676
|
-
readonly count?: number;
|
|
677
|
-
};
|
|
678
|
-
export type DataTableDateRangeFacet = {
|
|
679
|
-
/** The facet's namespace (its date field) — a key, not a filter value. */
|
|
680
|
-
readonly field: string;
|
|
681
|
-
/** Translated label ("When"). */
|
|
682
|
-
readonly label: string;
|
|
683
|
-
readonly from: string;
|
|
684
|
-
readonly to: string;
|
|
685
|
-
};
|
|
686
|
-
/** Splits rows into collapsible groups (order of first appearance). A
|
|
687
|
-
* renderer without grouping support shows the flat list. A group whose
|
|
688
|
-
* `headerLabel` is undefined shows its rows without a header and never
|
|
689
|
-
* collapses. */
|
|
690
|
-
export type DataTableRowGrouping = {
|
|
691
|
-
readonly keyOf: (row: ListRowViewModel) => string;
|
|
692
|
-
readonly headerLabel: (groupKey: string, rows: readonly ListRowViewModel[]) => string | undefined;
|
|
693
|
-
readonly startsCollapsed: (groupKey: string) => boolean;
|
|
694
|
-
};
|
|
653
|
+
export type { BulkRowOutcome, DataTableBulk, DataTableBulkAction, } from "./data-table-bulk-types.ts";
|
|
654
|
+
export type { DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, } from "./data-table-facet-types.ts";
|
|
695
655
|
export type DataTableProps = {
|
|
696
656
|
readonly columns: readonly ListColumnViewModel[];
|
|
697
657
|
readonly rows: readonly ListRowViewModel[];
|
|
698
|
-
readonly rowGrouping?: DataTableRowGrouping;
|
|
699
658
|
/** Tints a row. Renderers without row tinting ignore it. */
|
|
700
659
|
readonly rowTone?: (row: ListRowViewModel) => SelectOptionTone | undefined;
|
|
701
660
|
readonly onRowClick?: (row: ListRowViewModel) => void;
|
|
@@ -723,6 +682,8 @@ export type DataTableProps = {
|
|
|
723
682
|
* kebab). Actions then sit at the same x-position across all rows
|
|
724
683
|
* (no drifting with label width) and all lists in an app look alike. */
|
|
725
684
|
readonly rowActionMode?: DataTableRowActionMode;
|
|
685
|
+
/** Present: row checkboxes plus a selection bar that runs these actions once per selected row. */
|
|
686
|
+
readonly bulk?: DataTableBulk;
|
|
726
687
|
/** Custom empty-state content (e.g. Icon + Heading + CTA-Button).
|
|
727
688
|
* The default renderer wraps it in a dashed-border box. */
|
|
728
689
|
readonly emptyState?: ReactNode;
|
|
@@ -832,7 +793,8 @@ export type DataTableProps = {
|
|
|
832
793
|
/** Content of the area under an expanded row. Native renderers implement
|
|
833
794
|
* the same three props. */
|
|
834
795
|
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
835
|
-
};
|
|
796
|
+
} & DataTableTableBlock;
|
|
797
|
+
export type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
|
|
836
798
|
/** Cell type for one column of an embedded-list field. Mirrors
|
|
837
799
|
* EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-renderer/headless`. */
|
|
838
800
|
export type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
|
|
@@ -1403,6 +1365,8 @@ export type JsonViewProps = {
|
|
|
1403
1365
|
* PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
|
|
1404
1366
|
export type FileValueListProps = {
|
|
1405
1367
|
readonly fileIds: readonly string[];
|
|
1368
|
+
/** Starts the inline PDF preview of the first file open. */
|
|
1369
|
+
readonly previewOpenByDefault?: boolean;
|
|
1406
1370
|
readonly testId?: string;
|
|
1407
1371
|
};
|
|
1408
1372
|
/** Band above the tabs of a record detail: optional subtitle line plus the
|
|
@@ -92,6 +92,11 @@ export type CreateKumikoAppOptions = {
|
|
|
92
92
|
readonly hasWorkspaces?: boolean;
|
|
93
93
|
readonly features?: readonly FeatureSchema[];
|
|
94
94
|
}) => NavApi;
|
|
95
|
+
/** URL prefix the app is mounted under (for example "/admin"). The default nav adapter reads
|
|
96
|
+
* and writes URLs relative to it, and the auth gates and pages, which sit outside the
|
|
97
|
+
* NavProvider, use it to match and build their paths. A custom `navAdapter` handles its own
|
|
98
|
+
* prefix. */
|
|
99
|
+
readonly basePath?: string;
|
|
95
100
|
/** App-wide default width for form/detail screens that don't set their
|
|
96
101
|
* own `layout.width`. Default "4xl" (unchanged behavior) — a
|
|
97
102
|
* consumer app that wants e.g. bundled-feature screens to match its own
|
|
@@ -3,7 +3,7 @@ import { isOpenToAllGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
5
|
import { rendererError, rendererWarn } from "../../dev-console.js";
|
|
6
|
-
import { AppFeaturesProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
6
|
+
import { AppFeaturesProvider, BasePathProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
7
7
|
import { createLiveDispatcher } from "../../live/index.js";
|
|
8
8
|
import { lastSegment } from "../layout/nav-tree.js";
|
|
9
9
|
import { clearFileMetaCache } from "../primitives/file-thumbnail.js";
|
|
@@ -19,6 +19,7 @@ import { stackWrappers } from "./client-plugin.js";
|
|
|
19
19
|
import { WebDashboardBody } from "./dashboard-body.js";
|
|
20
20
|
import { DocumentLangSync } from "./document-lang-sync.js";
|
|
21
21
|
import { createBrowserDraftStorage } from "./draft-storage.js";
|
|
22
|
+
import { ListStateStoreBoot } from "./list-state-store-boot.js";
|
|
22
23
|
import { useBrowserNavApi } from "./nav.js";
|
|
23
24
|
import { NavProvidersProvider } from "./nav-providers-context.js";
|
|
24
25
|
import { ResolversProvider } from "./resolvers-context.js";
|
|
@@ -237,7 +238,7 @@ function KumikoAppRoot(props) {
|
|
|
237
238
|
warnMissingCustomScreens(loaded, props.customScreens);
|
|
238
239
|
setApp(loaded);
|
|
239
240
|
}, schemaIsRoleProjected: isFetchMode, ...(isFetchMode && { onUnmountResetSchema: () => setApp(undefined) }), ...(props.translate !== undefined && { translate: props.translate }), ...(props.onRowClick !== undefined && { onRowClick: props.onRowClick }), ...(props.shell !== undefined && { shell: props.shell }) }));
|
|
240
|
-
return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(LeaveGuardDialog, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, screenNode)) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
|
|
241
|
+
return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(LeaveGuardDialog, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, _jsx(ListStateStoreBoot, { children: screenNode }))) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
|
|
241
242
|
}
|
|
242
243
|
// Sits INSIDE the clientFeature gates (e.g. an auth gate) — a gate that
|
|
243
244
|
// renders a login placeholder instead of children keeps this component
|
|
@@ -340,7 +341,9 @@ export function createKumikoApp(options = {}) {
|
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
const localeResolver = options.locale ?? createBrowserLocaleResolver();
|
|
343
|
-
const
|
|
344
|
+
const { basePath } = options;
|
|
345
|
+
const useBrowserNavApiWithBasePath = (navOptions) => useBrowserNavApi({ ...navOptions, ...(basePath !== undefined && { basePath }) });
|
|
346
|
+
const navAdapter = options.navAdapter ?? useBrowserNavApiWithBasePath;
|
|
344
347
|
const reporter = options.reportClientErrors === false ? undefined : createClientErrorReporter();
|
|
345
348
|
reporter?.installGlobalHandlers();
|
|
346
349
|
const handleBoundaryError = (error, info, context) => {
|
|
@@ -353,7 +356,7 @@ export function createKumikoApp(options = {}) {
|
|
|
353
356
|
options.onError?.(error, info, context);
|
|
354
357
|
};
|
|
355
358
|
const root = createRoot(container);
|
|
356
|
-
root.render(_jsx(ErrorBoundary, { scope: "root", onError: handleBoundaryError, fallback: ({ errorId }) => _jsx(RootErrorFallback, { errorId: errorId }), children: _jsx(BoundaryErrorHandlerContext.Provider, { value: handleBoundaryError, children: _jsx(KumikoAppRoot, { initialApp: initialApp, screenQn: options.screenQn, ...(options.schemaUrl !== undefined && { schemaUrl: options.schemaUrl }), clientFeatures: clientFeatures, providers: providers, gates: gates, customScreens: customScreens, columnRenderers: columnRenderers, extensionSectionComponents: extensionSectionComponents, clientActions: clientActions, contentEditors: contentEditors, resolvers: resolvers, dispatcher: dispatcher, sessionEndedSignal: sessionEndedSignal, draftStorage: draftStorage, primitives: primitives, liveEvents: liveEvents, localeResolver: localeResolver, navAdapter: navAdapter, screenWidth: options.screenWidth, ...(options.translate !== undefined && { translate: options.translate }), ...(options.onRowClick !== undefined && { onRowClick: options.onRowClick }), ...(options.shell !== undefined && { shell: options.shell }) }) }) }));
|
|
359
|
+
root.render(_jsx(ErrorBoundary, { scope: "root", onError: handleBoundaryError, fallback: ({ errorId }) => _jsx(RootErrorFallback, { errorId: errorId }), children: _jsx(BoundaryErrorHandlerContext.Provider, { value: handleBoundaryError, children: _jsx(BasePathProvider, { basePath: basePath, children: _jsx(KumikoAppRoot, { initialApp: initialApp, screenQn: options.screenQn, ...(options.schemaUrl !== undefined && { schemaUrl: options.schemaUrl }), clientFeatures: clientFeatures, providers: providers, gates: gates, customScreens: customScreens, columnRenderers: columnRenderers, extensionSectionComponents: extensionSectionComponents, clientActions: clientActions, contentEditors: contentEditors, resolvers: resolvers, dispatcher: dispatcher, sessionEndedSignal: sessionEndedSignal, draftStorage: draftStorage, primitives: primitives, liveEvents: liveEvents, localeResolver: localeResolver, navAdapter: navAdapter, screenWidth: options.screenWidth, ...(options.translate !== undefined && { translate: options.translate }), ...(options.onRowClick !== undefined && { onRowClick: options.onRowClick }), ...(options.shell !== undefined && { shell: options.shell }) }) }) }) }));
|
|
357
360
|
return { root };
|
|
358
361
|
}
|
|
359
362
|
// TokensBoot uses the browser-backed TokensApi hook (class-based
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function committedUrl(): string;
|
|
2
|
+
/** Adopts the entry the browser currently shows as the committed one. */
|
|
3
|
+
export declare function adoptCurrentEntry(): void;
|
|
4
|
+
export declare function pushEntry(url: string): void;
|
|
5
|
+
export declare function replaceEntry(url: string): void;
|
|
6
|
+
export declare function handlePopstate(notify: () => void): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { hasUnsavedInput, requestLeave } from "../../index.js";
|
|
3
|
+
// Back/Forward cannot be vetoed, so the browser has already moved by the time the guard asks.
|
|
4
|
+
// Each entry we create carries its position in history.state; with it a declined move is
|
|
5
|
+
// undone with history.go(-delta) and a confirmed one replayed with history.go(delta), so the
|
|
6
|
+
// stack keeps its shape (no duplicated or truncated entries).
|
|
7
|
+
const INDEX_KEY = "kumikoNavIndex";
|
|
8
|
+
const committed = { url: "", index: 0 };
|
|
9
|
+
let leaveRequestOpen = false;
|
|
10
|
+
// Each history.go() we issue produces one popstate that must not be treated as user navigation.
|
|
11
|
+
const pendingMoves = [];
|
|
12
|
+
// Entries without a position (written by other code) cannot be undone relatively.
|
|
13
|
+
let fallbackTargetUrl;
|
|
14
|
+
function readUrl() {
|
|
15
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
16
|
+
}
|
|
17
|
+
function stateAt(index) {
|
|
18
|
+
return { [INDEX_KEY]: index };
|
|
19
|
+
}
|
|
20
|
+
function readIndex() {
|
|
21
|
+
const value = propertyOf(window.history.state, INDEX_KEY);
|
|
22
|
+
return typeof value === "number" ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function committedUrl() {
|
|
25
|
+
return committed.url;
|
|
26
|
+
}
|
|
27
|
+
/** Adopts the entry the browser currently shows as the committed one. */
|
|
28
|
+
export function adoptCurrentEntry() {
|
|
29
|
+
const index = readIndex();
|
|
30
|
+
committed.url = readUrl();
|
|
31
|
+
if (index === undefined) {
|
|
32
|
+
window.history.replaceState(stateAt(committed.index), "", committed.url);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
committed.index = index;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function commitCurrentUrl() {
|
|
39
|
+
committed.url = readUrl();
|
|
40
|
+
committed.index = readIndex() ?? committed.index;
|
|
41
|
+
}
|
|
42
|
+
export function pushEntry(url) {
|
|
43
|
+
window.history.pushState(stateAt(committed.index + 1), "", url);
|
|
44
|
+
committed.index += 1;
|
|
45
|
+
committed.url = readUrl();
|
|
46
|
+
}
|
|
47
|
+
export function replaceEntry(url) {
|
|
48
|
+
window.history.replaceState(stateAt(committed.index), "", url);
|
|
49
|
+
commitCurrentUrl();
|
|
50
|
+
}
|
|
51
|
+
export function handlePopstate(notify) {
|
|
52
|
+
const move = pendingMoves.shift();
|
|
53
|
+
if (move !== undefined) {
|
|
54
|
+
if (move === "replay") {
|
|
55
|
+
commitCurrentUrl();
|
|
56
|
+
notify();
|
|
57
|
+
}
|
|
58
|
+
// skip: this popstate is the echo of our own history.go()
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (fallbackTargetUrl !== undefined) {
|
|
62
|
+
const target = fallbackTargetUrl;
|
|
63
|
+
fallbackTargetUrl = undefined;
|
|
64
|
+
// After a forward move the entry below our restore-push is not the target.
|
|
65
|
+
if (readUrl() !== target)
|
|
66
|
+
window.history.replaceState(stateAt(committed.index), "", target);
|
|
67
|
+
commitCurrentUrl();
|
|
68
|
+
notify();
|
|
69
|
+
// skip: the pending fallback move is settled
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const newIndex = readIndex();
|
|
73
|
+
const delta = newIndex === undefined ? 0 : newIndex - committed.index;
|
|
74
|
+
if (leaveRequestOpen) {
|
|
75
|
+
// Another Back/Forward while the dialog is open must neither stack an entry nor replace the
|
|
76
|
+
// open request: step back to the entry the dialog is about.
|
|
77
|
+
if (delta !== 0) {
|
|
78
|
+
pendingMoves.push("undo");
|
|
79
|
+
window.history.go(-delta);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
window.history.replaceState(stateAt(committed.index), "", committed.url);
|
|
83
|
+
}
|
|
84
|
+
// skip: the open leave dialog keeps the decision
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!hasUnsavedInput()) {
|
|
88
|
+
commitCurrentUrl();
|
|
89
|
+
notify();
|
|
90
|
+
// skip: nothing unsaved, the move stands
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const targetUrl = readUrl();
|
|
94
|
+
leaveRequestOpen = true;
|
|
95
|
+
const settle = () => {
|
|
96
|
+
leaveRequestOpen = false;
|
|
97
|
+
};
|
|
98
|
+
if (delta !== 0) {
|
|
99
|
+
pendingMoves.push("undo");
|
|
100
|
+
window.history.go(-delta);
|
|
101
|
+
requestLeave(() => {
|
|
102
|
+
settle();
|
|
103
|
+
pendingMoves.push("replay");
|
|
104
|
+
window.history.go(delta);
|
|
105
|
+
}, settle);
|
|
106
|
+
// skip: the leave request owns the rest of the move
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
window.history.pushState(stateAt(committed.index), "", committed.url);
|
|
110
|
+
requestLeave(() => {
|
|
111
|
+
settle();
|
|
112
|
+
fallbackTargetUrl = targetUrl;
|
|
113
|
+
window.history.back();
|
|
114
|
+
}, settle);
|
|
115
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Provides the tab-scoped list-state store and drops state remembered under
|
|
3
|
+
* another user or tenant. Mount below the auth gates so the user is known. */
|
|
4
|
+
export declare function ListStateStoreBoot({ children }: {
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
}): ReactNode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { ListStateStoreProvider } from "../../components/list-state-store.js";
|
|
4
|
+
import { useOptionalCurrentUser } from "../../current-user.js";
|
|
5
|
+
import { resolveListStateStore } from "./session-list-state-store.js";
|
|
6
|
+
/** Provides the tab-scoped list-state store and drops state remembered under
|
|
7
|
+
* another user or tenant. Mount below the auth gates so the user is known. */
|
|
8
|
+
export function ListStateStoreBoot({ children }) {
|
|
9
|
+
const user = useOptionalCurrentUser();
|
|
10
|
+
const userId = user?.id;
|
|
11
|
+
const tenantId = user?.tenantId;
|
|
12
|
+
// Must run before the first list renders; an effect would let it read the previous identity's state.
|
|
13
|
+
const store = useMemo(() => resolveListStateStore(userId === undefined ? undefined : { userId, tenantId }), [userId, tenantId]);
|
|
14
|
+
return _jsx(ListStateStoreProvider, { store: store, children: children });
|
|
15
|
+
}
|
package/dist/web/app/nav.js
CHANGED
|
@@ -12,7 +12,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
12
12
|
// notification triggers (our pushPath and the popstate event), so this is not
|
|
13
13
|
// generalizable.
|
|
14
14
|
import { useCallback, useMemo, useSyncExternalStore, } from "react";
|
|
15
|
+
import { normalizeBasePath, prependBasePath, stripBasePath } from "../../app/base-path.js";
|
|
15
16
|
import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, useNav, } from "../../index.js";
|
|
17
|
+
import { adoptCurrentEntry, handlePopstate, pushEntry, replaceEntry, } from "./history-leave-guard.js";
|
|
16
18
|
// basePath allows the app to be mounted under a URL prefix
|
|
17
19
|
// (for example, `/admin`). The read path strips the prefix before parsePath;
|
|
18
20
|
// the write path prepends it before pushState/replaceState/href.
|
|
@@ -22,45 +24,11 @@ import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, us
|
|
|
22
24
|
// returns route=undefined, and the app renders its "outside" state
|
|
23
25
|
// (Not-Found, marketing layer, server-routing pickup, …). There is no
|
|
24
26
|
// automatic redirect to the app root—the host app decides.
|
|
25
|
-
function normalizeBasePath(raw) {
|
|
26
|
-
if (raw === undefined || raw === "" || raw === "/")
|
|
27
|
-
return "";
|
|
28
|
-
const withLeading = raw.startsWith("/") ? raw : `/${raw}`;
|
|
29
|
-
return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
|
|
30
|
-
}
|
|
31
|
-
// Returns the in-app (basePath-relative) path when `path` is under basePath;
|
|
32
|
-
// otherwise returns `undefined`. The segment boundary is strict: "/administrator"
|
|
33
|
-
// does not match "/admin"—otherwise a string-level prefix check would produce
|
|
34
|
-
// false matches.
|
|
35
|
-
function stripBasePath(path, basePath) {
|
|
36
|
-
if (basePath === "")
|
|
37
|
-
return path;
|
|
38
|
-
if (path === basePath)
|
|
39
|
-
return "/";
|
|
40
|
-
if (path.startsWith(`${basePath}/`))
|
|
41
|
-
return path.slice(basePath.length);
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
function prependBasePath(path, basePath) {
|
|
45
|
-
if (basePath === "")
|
|
46
|
-
return path;
|
|
47
|
-
// formatPath always returns an absolute in-app path: "/" or "/screen-id"
|
|
48
|
-
// or "/screen-id/entity-id". "/" becomes basePath; otherwise, concatenate.
|
|
49
|
-
if (path === "/" || path === "")
|
|
50
|
-
return basePath;
|
|
51
|
-
return `${basePath}${path}`;
|
|
52
|
-
}
|
|
53
27
|
// pushState does not fire popstate, so we maintain a listener set that
|
|
54
28
|
// navigate() notifies. Back/Forward navigation goes through a window listener
|
|
55
29
|
// wired exactly once.
|
|
56
30
|
const listeners = new Set();
|
|
57
31
|
let popstateWired = false;
|
|
58
|
-
// URL the app currently shows; popstate has already moved the browser to the
|
|
59
|
-
// target by the time we can ask, so a declined leave must restore this one.
|
|
60
|
-
let committedUrl = "";
|
|
61
|
-
function readUrl() {
|
|
62
|
-
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
63
|
-
}
|
|
64
32
|
// Stable identity for the omitted-features case — `?? []` would create a
|
|
65
33
|
// fresh array every render, busting the useMemo below on every render for
|
|
66
34
|
// every existing useBrowserNavApi() call site (none of which pass features).
|
|
@@ -71,19 +39,7 @@ function ensurePopstateWired() {
|
|
|
71
39
|
if (typeof window === "undefined")
|
|
72
40
|
return;
|
|
73
41
|
window.addEventListener("popstate", () => {
|
|
74
|
-
|
|
75
|
-
committedUrl = readUrl();
|
|
76
|
-
for (const l of listeners)
|
|
77
|
-
l();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const targetUrl = readUrl();
|
|
81
|
-
window.history.pushState(null, "", committedUrl);
|
|
82
|
-
requestLeave(() => {
|
|
83
|
-
// replaceState, not history.back(): the declined entry is already ours
|
|
84
|
-
// and a forward navigation would otherwise land on the wrong entry.
|
|
85
|
-
window.history.replaceState(null, "", targetUrl);
|
|
86
|
-
committedUrl = targetUrl;
|
|
42
|
+
handlePopstate(() => {
|
|
87
43
|
for (const l of listeners)
|
|
88
44
|
l();
|
|
89
45
|
});
|
|
@@ -99,7 +55,7 @@ function ensurePopstateWired() {
|
|
|
99
55
|
function subscribe(listener) {
|
|
100
56
|
ensurePopstateWired();
|
|
101
57
|
if (typeof window !== "undefined" && listeners.size === 0)
|
|
102
|
-
|
|
58
|
+
adoptCurrentEntry();
|
|
103
59
|
listeners.add(listener);
|
|
104
60
|
return () => {
|
|
105
61
|
listeners.delete(listener);
|
|
@@ -146,8 +102,7 @@ function applySearchParamUpdates(updates) {
|
|
|
146
102
|
if (nextSearch === window.location.search)
|
|
147
103
|
return;
|
|
148
104
|
const url = `${window.location.pathname}${nextSearch}${window.location.hash}`;
|
|
149
|
-
|
|
150
|
-
committedUrl = readUrl();
|
|
105
|
+
replaceEntry(url);
|
|
151
106
|
for (const l of listeners)
|
|
152
107
|
l();
|
|
153
108
|
}
|
|
@@ -168,8 +123,7 @@ function pushPath(path, search) {
|
|
|
168
123
|
if (unchanged)
|
|
169
124
|
return;
|
|
170
125
|
requestLeave(() => {
|
|
171
|
-
|
|
172
|
-
committedUrl = readUrl();
|
|
126
|
+
pushEntry(`${path}${search ?? ""}`);
|
|
173
127
|
for (const l of listeners)
|
|
174
128
|
l();
|
|
175
129
|
});
|
|
@@ -185,8 +139,7 @@ function replacePath(path, search) {
|
|
|
185
139
|
const leavesCurrentPage = window.location.pathname !== path ||
|
|
186
140
|
(search !== undefined && window.location.search !== search);
|
|
187
141
|
const apply = () => {
|
|
188
|
-
|
|
189
|
-
committedUrl = readUrl();
|
|
142
|
+
replaceEntry(nextUrl);
|
|
190
143
|
for (const l of listeners)
|
|
191
144
|
l();
|
|
192
145
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ListStateStore } from "../../components/list-state-store.tsx";
|
|
2
|
+
export type ListStateIdentity = {
|
|
3
|
+
readonly userId: string;
|
|
4
|
+
readonly tenantId?: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
/** Remembered list state for the lifetime of the browser tab, keyed per tenant
|
|
7
|
+
* and user. Every storage failure (blocked, full) degrades to "nothing remembered". */
|
|
8
|
+
export declare function createSessionListStateStore(storage: Storage, identity: ListStateIdentity): ListStateStore;
|
|
9
|
+
/** Forgets all remembered list state and the identity marker. Logout calls it
|
|
10
|
+
* before a page reload, which would otherwise hide the identity change. */
|
|
11
|
+
export declare function clearRememberedListState(): void;
|
|
12
|
+
/** The store for the current identity. State remembered under another identity,
|
|
13
|
+
* or under one that has since gone away, is dropped first. Without an identity
|
|
14
|
+
* (app without auth) only the in-memory store is used and nothing is persisted. */
|
|
15
|
+
export declare function resolveListStateStore(identity: ListStateIdentity | undefined): ListStateStore;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getDefaultMemoryListStateStore } from "../../components/list-state-store.js";
|
|
2
|
+
import { parseStoredListStateJson } from "../../hooks/list-state-schema.js";
|
|
3
|
+
const KEY_PREFIX = "kumiko.list.";
|
|
4
|
+
const IDENTITY_KEY = `${KEY_PREFIX}identity`;
|
|
5
|
+
const NO_TENANT = "-";
|
|
6
|
+
function identityMarker(identity) {
|
|
7
|
+
return `${identity.tenantId ?? NO_TENANT}.${identity.userId}`;
|
|
8
|
+
}
|
|
9
|
+
function openSessionStorage() {
|
|
10
|
+
try {
|
|
11
|
+
return typeof window === "undefined" ? undefined : window.sessionStorage;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Access itself throws when storage is blocked (privacy modes, sandboxed frames).
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function prefixedKeys(storage) {
|
|
19
|
+
return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter((key) => key !== null && key.startsWith(KEY_PREFIX));
|
|
20
|
+
}
|
|
21
|
+
function removeAllRemembered(storage) {
|
|
22
|
+
for (const key of prefixedKeys(storage))
|
|
23
|
+
storage.removeItem(key);
|
|
24
|
+
}
|
|
25
|
+
/** Remembered list state for the lifetime of the browser tab, keyed per tenant
|
|
26
|
+
* and user. Every storage failure (blocked, full) degrades to "nothing remembered". */
|
|
27
|
+
export function createSessionListStateStore(storage, identity) {
|
|
28
|
+
const keyPrefix = `${KEY_PREFIX}${identityMarker(identity)}.`;
|
|
29
|
+
return {
|
|
30
|
+
read: (key) => {
|
|
31
|
+
try {
|
|
32
|
+
const raw = storage.getItem(`${keyPrefix}${key}`);
|
|
33
|
+
return raw === null ? undefined : parseStoredListStateJson(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
write: (key, state) => {
|
|
40
|
+
try {
|
|
41
|
+
storage.setItem(`${keyPrefix}${key}`, JSON.stringify(state));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Quota or blocked storage: the list keeps working, it just is not remembered.
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
remove: (key) => {
|
|
48
|
+
try {
|
|
49
|
+
storage.removeItem(`${keyPrefix}${key}`);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// See write.
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
clear: () => {
|
|
56
|
+
try {
|
|
57
|
+
for (const key of prefixedKeys(storage).filter((k) => k !== IDENTITY_KEY)) {
|
|
58
|
+
storage.removeItem(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// See write.
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Forgets all remembered list state and the identity marker. Logout calls it
|
|
68
|
+
* before a page reload, which would otherwise hide the identity change. */
|
|
69
|
+
export function clearRememberedListState() {
|
|
70
|
+
getDefaultMemoryListStateStore().clear();
|
|
71
|
+
lastMemoryIdentity = undefined;
|
|
72
|
+
const storage = openSessionStorage();
|
|
73
|
+
// skip: no sessionStorage available, memory store already cleared
|
|
74
|
+
if (storage === undefined)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
removeAllRemembered(storage);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// See createSessionListStateStore.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// The in-memory store survives SPA identity changes, so it needs its own marker.
|
|
84
|
+
let lastMemoryIdentity;
|
|
85
|
+
/** The store for the current identity. State remembered under another identity,
|
|
86
|
+
* or under one that has since gone away, is dropped first. Without an identity
|
|
87
|
+
* (app without auth) only the in-memory store is used and nothing is persisted. */
|
|
88
|
+
export function resolveListStateStore(identity) {
|
|
89
|
+
const storage = openSessionStorage();
|
|
90
|
+
const memory = getDefaultMemoryListStateStore();
|
|
91
|
+
const marker = identity === undefined ? undefined : identityMarker(identity);
|
|
92
|
+
if (lastMemoryIdentity !== marker) {
|
|
93
|
+
if (lastMemoryIdentity !== undefined)
|
|
94
|
+
memory.clear();
|
|
95
|
+
lastMemoryIdentity = marker;
|
|
96
|
+
}
|
|
97
|
+
if (storage === undefined)
|
|
98
|
+
return memory;
|
|
99
|
+
try {
|
|
100
|
+
const storedMarker = storage.getItem(IDENTITY_KEY);
|
|
101
|
+
if (marker === undefined) {
|
|
102
|
+
if (storedMarker !== null) {
|
|
103
|
+
removeAllRemembered(storage);
|
|
104
|
+
memory.clear();
|
|
105
|
+
}
|
|
106
|
+
return memory;
|
|
107
|
+
}
|
|
108
|
+
if (storedMarker !== marker) {
|
|
109
|
+
removeAllRemembered(storage);
|
|
110
|
+
storage.setItem(IDENTITY_KEY, marker);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return memory;
|
|
115
|
+
}
|
|
116
|
+
return identity === undefined ? memory : createSessionListStateStore(storage, identity);
|
|
117
|
+
}
|
package/dist/web/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { KumikoLink, useBrowserNavApi } from "./app/nav.tsx";
|
|
|
13
13
|
export { PlainContentEditor } from "./app/plain-content-editor.tsx";
|
|
14
14
|
export { useResolvers } from "./app/resolvers-context.tsx";
|
|
15
15
|
export { RichContentEditor } from "./app/rich-content-editor.tsx";
|
|
16
|
+
export { clearRememberedListState } from "./app/session-list-state-store.ts";
|
|
16
17
|
export type { NavIconKey } from "./icons.tsx";
|
|
17
18
|
export { Icon } from "./icons.tsx";
|
|
18
19
|
export type { AppLayoutProps } from "./layout/app-layout.tsx";
|
package/dist/web/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { KumikoLink, useBrowserNavApi } from "./app/nav.js";
|
|
|
12
12
|
export { PlainContentEditor } from "./app/plain-content-editor.js";
|
|
13
13
|
export { useResolvers } from "./app/resolvers-context.js";
|
|
14
14
|
export { RichContentEditor } from "./app/rich-content-editor.js";
|
|
15
|
+
export { clearRememberedListState } from "./app/session-list-state-store.js";
|
|
15
16
|
export { Icon } from "./icons.js";
|
|
16
17
|
export { AppLayout } from "./layout/app-layout.js";
|
|
17
18
|
export { Avatar } from "./layout/avatar.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DataTableExport } from "../../data-table-view-types.ts";
|
|
2
|
+
export type XlsxExportResult = {
|
|
3
|
+
readonly status: "ok";
|
|
4
|
+
readonly blob: Blob;
|
|
5
|
+
} | {
|
|
6
|
+
readonly status: "too-many";
|
|
7
|
+
readonly maxRows: number | undefined;
|
|
8
|
+
} | {
|
|
9
|
+
readonly status: "failed";
|
|
10
|
+
};
|
|
11
|
+
export declare function requestXlsxExport(source: DataTableExport): Promise<XlsxExportResult>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { CSRF_HEADER_NAME, LOCALE_HEADER_NAME, readActiveLocale, readCsrfToken, } from "../../live/index.js";
|
|
3
|
+
const TOO_MANY_ROWS_HTTP_STATUS = 413;
|
|
4
|
+
async function readMaxRows(res) {
|
|
5
|
+
try {
|
|
6
|
+
const body = await res.json();
|
|
7
|
+
const error = isPlainObject(body) ? body["error"] : undefined;
|
|
8
|
+
const details = isPlainObject(error) ? error["details"] : undefined;
|
|
9
|
+
const maxRows = isPlainObject(details) ? details["maxRows"] : undefined;
|
|
10
|
+
return typeof maxRows === "number" ? maxRows : undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function requestXlsxExport(source) {
|
|
17
|
+
const headers = { "Content-Type": "application/json" };
|
|
18
|
+
const csrf = readCsrfToken();
|
|
19
|
+
if (csrf !== undefined)
|
|
20
|
+
headers[CSRF_HEADER_NAME] = csrf;
|
|
21
|
+
const locale = readActiveLocale();
|
|
22
|
+
if (locale !== undefined)
|
|
23
|
+
headers[LOCALE_HEADER_NAME] = locale;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch("/api/export/xlsx", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
credentials: "include",
|
|
28
|
+
headers,
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
screen: source.screen,
|
|
31
|
+
payload: source.payload,
|
|
32
|
+
columns: source.columns,
|
|
33
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
if (res.ok)
|
|
37
|
+
return { status: "ok", blob: await res.blob() };
|
|
38
|
+
if (res.status === TOO_MANY_ROWS_HTTP_STATUS) {
|
|
39
|
+
return { status: "too-many", maxRows: await readMaxRows(res) };
|
|
40
|
+
}
|
|
41
|
+
return { status: "failed" };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { status: "failed" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
export declare const ACTIONS_CONTENT_ATTRIBUTE = "data-actions-content";
|
|
3
|
+
export declare function stickyActionsCellClass(fillsHeight: boolean): string;
|
|
4
|
+
export declare function ActionsHeaderCell({ label, style, absorbsLeftover, }: {
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly style?: CSSProperties | undefined;
|
|
7
|
+
/** Fixed table layout: `w-px` would pin this column to 1px, so it must size as auto and take the leftover width. */
|
|
8
|
+
readonly absorbsLeftover: boolean;
|
|
9
|
+
}): ReactNode;
|
|
10
|
+
export declare function ActionsBodyCell({ testId, fillsHeight, measureContent, children, }: {
|
|
11
|
+
readonly testId: string;
|
|
12
|
+
readonly fillsHeight: boolean;
|
|
13
|
+
/** Wraps the content so the column's natural width stays measurable under a fixed table layout. */
|
|
14
|
+
readonly measureContent: boolean;
|
|
15
|
+
readonly children: ReactNode;
|
|
16
|
+
}): ReactNode;
|