@cosmicdrift/kumiko-renderer 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-body.js +10 -2
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/base-path-context.d.ts +9 -0
- package/dist/app/base-path-context.js +13 -0
- package/dist/app/base-path.d.ts +3 -0
- package/dist/app/base-path.js +27 -0
- package/dist/app/config-edit-body.d.ts +1 -1
- package/dist/app/config-edit-body.js +19 -6
- package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
- package/dist/app/config-edit-without-secret-fields.js +28 -0
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +9 -4
- package/dist/app/entity-list-facet-specs.d.ts +3 -0
- package/dist/app/entity-list-facet-specs.js +65 -0
- package/dist/app/entity-list-screen.js +74 -206
- package/dist/app/extension-sections.d.ts +5 -3
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- package/dist/app/kumiko-screen.js +6 -14
- package/dist/app/list-bulk-actions.d.ts +18 -0
- package/dist/app/list-bulk-actions.js +54 -0
- package/dist/app/locked-screen-header.d.ts +9 -0
- package/dist/app/locked-screen-header.js +33 -0
- package/dist/app/projection-list-body.js +51 -46
- package/dist/app/row-actions.d.ts +3 -0
- package/dist/app/row-actions.js +10 -5
- package/dist/app/screen-shared.js +9 -2
- package/dist/app/use-entity-range-facets.d.ts +24 -0
- package/dist/app/use-entity-range-facets.js +64 -0
- package/dist/app/use-projection-date-range.d.ts +12 -0
- package/dist/app/use-projection-date-range.js +21 -0
- package/dist/app/use-screen-list-rows.d.ts +37 -0
- package/dist/app/use-screen-list-rows.js +104 -0
- package/dist/app/use-screen-list-table.d.ts +24 -0
- package/dist/app/use-screen-list-table.js +46 -0
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +1 -1
- package/dist/components/list-group-page-bridges.d.ts +3 -0
- package/dist/components/list-group-page-bridges.js +31 -0
- package/dist/components/list-state-store.d.ts +16 -0
- package/dist/components/list-state-store.js +31 -0
- package/dist/components/related-list-section.js +51 -90
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit.js +11 -8
- package/dist/components/render-field.js +11 -7
- package/dist/components/render-list-toolbar-action.d.ts +27 -0
- package/dist/components/render-list-toolbar-action.js +40 -0
- package/dist/components/render-list.d.ts +13 -29
- package/dist/components/render-list.js +72 -67
- package/dist/current-user.d.ts +2 -0
- package/dist/data-table-aggregate-types.d.ts +47 -0
- package/dist/data-table-aggregate-types.js +1 -0
- package/dist/data-table-bulk-types.d.ts +27 -0
- package/dist/data-table-bulk-types.js +1 -0
- package/dist/data-table-facet-types.d.ts +36 -0
- package/dist/data-table-facet-types.js +1 -0
- package/dist/data-table-view-types.d.ts +73 -0
- package/dist/data-table-view-types.js +1 -0
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/form/form-controller.js +8 -0
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/view-model/edit.js +5 -1
- package/dist/headless/view-model/list.js +184 -149
- package/dist/headless/view-model/types.d.ts +7 -2
- package/dist/hooks/list-column-layout.d.ts +19 -0
- package/dist/hooks/list-column-layout.js +36 -0
- package/dist/hooks/list-group-pages.d.ts +26 -0
- package/dist/hooks/list-group-pages.js +37 -0
- package/dist/hooks/list-reset.d.ts +8 -0
- package/dist/hooks/list-reset.js +15 -0
- package/dist/hooks/list-server-groups.d.ts +39 -0
- package/dist/hooks/list-server-groups.js +84 -0
- package/dist/hooks/list-state-schema.d.ts +28 -0
- package/dist/hooks/list-state-schema.js +76 -0
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/use-bulk-run.d.ts +41 -0
- package/dist/hooks/use-bulk-run.js +65 -0
- package/dist/hooks/use-group-load-state.d.ts +11 -0
- package/dist/hooks/use-group-load-state.js +31 -0
- package/dist/hooks/use-list-aggregates.d.ts +38 -0
- package/dist/hooks/use-list-aggregates.js +107 -0
- package/dist/hooks/use-list-table.d.ts +38 -0
- package/dist/hooks/use-list-table.js +91 -0
- package/dist/hooks/use-list-view-state.d.ts +43 -0
- package/dist/hooks/use-list-view-state.js +161 -0
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +17 -3
- package/dist/hooks/use-related-list-rows.d.ts +27 -0
- package/dist/hooks/use-related-list-rows.js +81 -0
- package/dist/hooks/use-related-list-state.d.ts +15 -0
- package/dist/hooks/use-related-list-state.js +31 -0
- package/dist/hooks/use-remembered-list-params.d.ts +13 -0
- package/dist/hooks/use-remembered-list-params.js +53 -0
- package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
- package/dist/hooks/use-remembered-local-list-state.js +63 -0
- package/dist/i18n-defaults.js +59 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +30 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +15 -7
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/history-leave-guard.d.ts +6 -0
- package/dist/web/app/history-leave-guard.js +115 -0
- package/dist/web/app/list-state-store-boot.d.ts +6 -0
- package/dist/web/app/list-state-store-boot.js +15 -0
- package/dist/web/app/nav.js +7 -54
- package/dist/web/app/session-list-state-store.d.ts +15 -0
- package/dist/web/app/session-list-state-store.js +117 -0
- package/dist/web/index.d.ts +2 -1
- package/dist/web/index.js +2 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/lib/export-xlsx-request.d.ts +11 -0
- package/dist/web/lib/export-xlsx-request.js +46 -0
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.js +1 -1
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
- package/dist/web/primitives/data-table/actions-column.js +34 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
- package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
- package/dist/web/primitives/data-table/card-sort-select.js +40 -0
- package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
- package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
- package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
- package/dist/web/primitives/data-table/column-picker-section.js +35 -0
- package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
- package/dist/web/primitives/data-table/density.d.ts +14 -0
- package/dist/web/primitives/data-table/density.js +20 -0
- package/dist/web/primitives/data-table/export-button.d.ts +5 -0
- package/dist/web/primitives/data-table/export-button.js +37 -0
- package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
- package/dist/web/primitives/data-table/facet-cluster.js +37 -0
- package/dist/web/primitives/data-table/group-band.d.ts +28 -0
- package/dist/web/primitives/data-table/group-band.js +53 -0
- package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
- package/dist/web/primitives/data-table/measure-value.js +28 -0
- package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
- package/dist/web/primitives/data-table/pin-section.js +20 -0
- package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
- package/dist/web/primitives/data-table/reset-all-button.js +8 -0
- package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
- package/dist/web/primitives/data-table/server-group-header.js +51 -0
- package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
- package/dist/web/primitives/data-table/sortable-header.js +41 -0
- package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
- package/dist/web/primitives/data-table/table-chrome.js +30 -0
- package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
- package/dist/web/primitives/data-table/table-footer.js +20 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
- package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
- package/dist/web/primitives/data-table/totals-footer.js +48 -0
- package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
- package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
- package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
- package/dist/web/primitives/data-table/use-column-pins.js +117 -0
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
- package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
- package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
- package/dist/web/primitives/data-table/use-row-selection.js +48 -0
- package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
- package/dist/web/primitives/data-table/use-view-columns.js +17 -0
- package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
- package/dist/web/primitives/data-table/view-menu.js +45 -0
- package/dist/web/primitives/data-table-cell.d.ts +0 -22
- package/dist/web/primitives/data-table-cell.js +1 -58
- package/dist/web/primitives/data-table.d.ts +1 -1
- package/dist/web/primitives/data-table.js +125 -146
- package/dist/web/primitives/facet-filter.js +1 -1
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/file-value-list.d.ts +1 -1
- package/dist/web/primitives/file-value-list.js +5 -5
- package/dist/web/primitives/input.js +6 -1
- package/dist/web/primitives/layout-elements.js +6 -4
- package/dist/web/primitives/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/row-actions.js +3 -3
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +49 -21
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.js +1 -1
- package/dist/web/widgets/detail-list.d.ts +5 -1
- package/dist/web/widgets/detail-list.js +5 -1
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.js +10 -5
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/upload-zone.js +13 -5
- package/package.json +5 -5
- package/src/changes.json +158 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ListStateStore } from "../components/list-state-store.tsx";
|
|
2
|
+
import { type KnownListFields } from "./list-state-schema.ts";
|
|
3
|
+
type RememberedListParamsArgs = {
|
|
4
|
+
readonly screenId: string;
|
|
5
|
+
readonly enabled: boolean;
|
|
6
|
+
readonly store: ListStateStore;
|
|
7
|
+
readonly known: KnownListFields;
|
|
8
|
+
};
|
|
9
|
+
/** URL stays the source of truth: the stored list params are copied into the
|
|
10
|
+
* URL once when the screen opens without any, and every later URL change is
|
|
11
|
+
* written back. A link that already carries list params is never overridden. */
|
|
12
|
+
export declare function useRememberedListParams({ screenId, enabled, store, known, }: RememberedListParamsArgs): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useNav } from "../app/nav.js";
|
|
3
|
+
import { listParamKeys } from "./list-reset.js";
|
|
4
|
+
import { sanitizeStoredListState } from "./list-state-schema.js";
|
|
5
|
+
function stripScreenPrefix(params, screenId) {
|
|
6
|
+
const prefix = `${screenId}.`;
|
|
7
|
+
return Object.fromEntries(listParamKeys(params, screenId).flatMap((key) => {
|
|
8
|
+
const value = params[key];
|
|
9
|
+
return value === undefined ? [] : [[key.slice(prefix.length), value]];
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
function sameParams(a, b) {
|
|
13
|
+
const keys = Object.keys(a);
|
|
14
|
+
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
15
|
+
}
|
|
16
|
+
/** URL stays the source of truth: the stored list params are copied into the
|
|
17
|
+
* URL once when the screen opens without any, and every later URL change is
|
|
18
|
+
* written back. A link that already carries list params is never overridden. */
|
|
19
|
+
export function useRememberedListParams({ screenId, enabled, store, known, }) {
|
|
20
|
+
const nav = useNav();
|
|
21
|
+
const current = useMemo(() => stripScreenPrefix(nav.searchParams, screenId), [nav.searchParams, screenId]);
|
|
22
|
+
const phaseRef = useRef({ screenId, phase: "pending" });
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// skip: remembering is not enabled
|
|
25
|
+
if (!enabled)
|
|
26
|
+
return;
|
|
27
|
+
if (phaseRef.current.screenId !== screenId)
|
|
28
|
+
phaseRef.current = { screenId, phase: "pending" };
|
|
29
|
+
const state = phaseRef.current;
|
|
30
|
+
const currentIsEmpty = Object.keys(current).length === 0;
|
|
31
|
+
if (state.phase === "pending" && currentIsEmpty) {
|
|
32
|
+
const stored = sanitizeStoredListState(store.read(screenId), known)?.params;
|
|
33
|
+
if (stored !== undefined && Object.keys(stored).length > 0) {
|
|
34
|
+
state.phase = "restoring";
|
|
35
|
+
nav.setSearchParams(Object.fromEntries(Object.entries(stored).map(([k, v]) => [`${screenId}.${k}`, v])));
|
|
36
|
+
// skip: restore started, effect reruns with the restored URL
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// skip: waiting for the restored URL to arrive
|
|
41
|
+
if (state.phase === "restoring" && currentIsEmpty)
|
|
42
|
+
return;
|
|
43
|
+
state.phase = "live";
|
|
44
|
+
const existing = store.read(screenId);
|
|
45
|
+
// skip: nothing stored and nothing to store
|
|
46
|
+
if (existing?.params === undefined && currentIsEmpty)
|
|
47
|
+
return;
|
|
48
|
+
// skip: stored state already matches the URL
|
|
49
|
+
if (existing?.params !== undefined && sameParams(existing.params, current))
|
|
50
|
+
return;
|
|
51
|
+
store.write(screenId, { ...existing, params: current });
|
|
52
|
+
}, [enabled, screenId, current, known, store, nav]);
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import type { ListStateStore } from "../components/list-state-store.tsx";
|
|
3
|
+
import { type KnownListFields } from "./list-state-schema.ts";
|
|
4
|
+
import type { ListSort } from "./use-list-url-state.ts";
|
|
5
|
+
type LocalFilters = Readonly<Record<string, readonly string[]>>;
|
|
6
|
+
type LocalListStateArgs = {
|
|
7
|
+
readonly storeKey: string;
|
|
8
|
+
readonly store: ListStateStore;
|
|
9
|
+
/** `table.rememberState`: seeds from the store and writes every change back. */
|
|
10
|
+
readonly remember: boolean;
|
|
11
|
+
readonly defaultSort: ListSort | null;
|
|
12
|
+
readonly known: KnownListFields;
|
|
13
|
+
};
|
|
14
|
+
/** Search, facet filters and sort of a list whose state is component-local
|
|
15
|
+
* (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
|
|
16
|
+
export declare function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }: LocalListStateArgs): {
|
|
17
|
+
search: string;
|
|
18
|
+
filters: LocalFilters;
|
|
19
|
+
sort: ListSort | null;
|
|
20
|
+
setSearch: (search: string) => void;
|
|
21
|
+
setFilters: Dispatch<SetStateAction<Readonly<Record<string, readonly string[]>>>>;
|
|
22
|
+
setSort: (sort: ListSort | null) => void;
|
|
23
|
+
reset: () => void;
|
|
24
|
+
atDefault: boolean;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { sanitizeStoredListState } from "./list-state-schema.js";
|
|
3
|
+
const FILTER_PREFIX = "f.";
|
|
4
|
+
function sameSort(a, b) {
|
|
5
|
+
return a === b || (a !== null && b !== null && a.field === b.field && a.dir === b.dir);
|
|
6
|
+
}
|
|
7
|
+
function toParams(state, defaultSort) {
|
|
8
|
+
const params = {};
|
|
9
|
+
if (state.search !== "")
|
|
10
|
+
params["q"] = state.search;
|
|
11
|
+
if (!sameSort(state.sort, defaultSort) && state.sort !== null) {
|
|
12
|
+
params["sort"] = state.sort.field;
|
|
13
|
+
params["dir"] = state.sort.dir;
|
|
14
|
+
}
|
|
15
|
+
for (const [field, values] of Object.entries(state.filters)) {
|
|
16
|
+
if (values.length > 0)
|
|
17
|
+
params[`${FILTER_PREFIX}${field}`] = values.join(",");
|
|
18
|
+
}
|
|
19
|
+
return params;
|
|
20
|
+
}
|
|
21
|
+
function fromParams(params, defaultSort) {
|
|
22
|
+
const sortField = params?.["sort"];
|
|
23
|
+
const dir = params?.["dir"];
|
|
24
|
+
const sort = sortField !== undefined && (dir === "asc" || dir === "desc")
|
|
25
|
+
? { field: sortField, dir }
|
|
26
|
+
: defaultSort;
|
|
27
|
+
const filters = {};
|
|
28
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
29
|
+
if (key.startsWith(FILTER_PREFIX)) {
|
|
30
|
+
filters[key.slice(FILTER_PREFIX.length)] = value.split(",").filter((v) => v !== "");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { search: params?.["q"] ?? "", filters, sort };
|
|
34
|
+
}
|
|
35
|
+
/** Search, facet filters and sort of a list whose state is component-local
|
|
36
|
+
* (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
|
|
37
|
+
export function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }) {
|
|
38
|
+
const [state, setState] = useState(() => remember
|
|
39
|
+
? fromParams(sanitizeStoredListState(store.read(storeKey), known)?.params, defaultSort)
|
|
40
|
+
: { search: "", filters: {}, sort: defaultSort });
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// skip: remembering is not enabled
|
|
43
|
+
if (!remember)
|
|
44
|
+
return;
|
|
45
|
+
const params = toParams(state, defaultSort);
|
|
46
|
+
const existing = store.read(storeKey);
|
|
47
|
+
// skip: nothing stored and nothing to store
|
|
48
|
+
if (existing?.params === undefined && Object.keys(params).length === 0)
|
|
49
|
+
return;
|
|
50
|
+
store.write(storeKey, { ...existing, params });
|
|
51
|
+
}, [remember, state, defaultSort, store, storeKey]);
|
|
52
|
+
const setSearch = useCallback((search) => setState((prev) => ({ ...prev, search })), []);
|
|
53
|
+
const setFilters = useCallback((next) => setState((prev) => ({
|
|
54
|
+
...prev,
|
|
55
|
+
filters: typeof next === "function" ? next(prev.filters) : next,
|
|
56
|
+
})), []);
|
|
57
|
+
const setSort = useCallback((sort) => setState((prev) => ({ ...prev, sort })), []);
|
|
58
|
+
const reset = useCallback(() => setState({ search: "", filters: {}, sort: defaultSort }), [defaultSort]);
|
|
59
|
+
const atDefault = state.search === "" &&
|
|
60
|
+
Object.values(state.filters).every((values) => values.length === 0) &&
|
|
61
|
+
sameSort(state.sort, defaultSort);
|
|
62
|
+
return { ...state, setSearch, setFilters, setSort, reset, atDefault };
|
|
63
|
+
}
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
// Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete
|
|
1
|
+
// Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete
|
|
2
|
+
// buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation
|
|
3
|
+
// reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can
|
|
4
|
+
// override individual keys via clientFeatures.translations.
|
|
2
5
|
//
|
|
3
|
-
// All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area
|
|
6
|
+
// All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area
|
|
7
|
+
// (actions / list / nav / form / validation).
|
|
4
8
|
export const kumikoDefaultTranslations = {
|
|
5
9
|
en: {
|
|
6
10
|
"kumiko.actions.save": "Save",
|
|
@@ -82,6 +86,44 @@ export const kumikoDefaultTranslations = {
|
|
|
82
86
|
"kumiko.list.sort.unsorted": "Unsorted",
|
|
83
87
|
"kumiko.list.filter.reset": "Reset",
|
|
84
88
|
"kumiko.list.filter.toggle": "Filters",
|
|
89
|
+
"kumiko.list.reset-all": "Reset",
|
|
90
|
+
"kumiko.list.export": "Export",
|
|
91
|
+
"kumiko.list.export-busy": "Exporting…",
|
|
92
|
+
"kumiko.list.export-too-many": "More than {max} rows. Narrow the list with a filter and export again.",
|
|
93
|
+
"kumiko.list.export-failed": "The export failed. Please try again.",
|
|
94
|
+
"kumiko.list.view.menu": "View",
|
|
95
|
+
"kumiko.list.view.columns": "Columns",
|
|
96
|
+
"kumiko.list.view.density": "Density",
|
|
97
|
+
"kumiko.list.view.density.comfortable": "Comfortable",
|
|
98
|
+
"kumiko.list.view.density.compact": "Compact",
|
|
99
|
+
"kumiko.list.view.reset": "Reset view",
|
|
100
|
+
"kumiko.list.group.by": "Group by",
|
|
101
|
+
"kumiko.list.group.none": "None",
|
|
102
|
+
"kumiko.list.group.empty": "(empty)",
|
|
103
|
+
"kumiko.list.group.load-more": "Load more",
|
|
104
|
+
"kumiko.list.group.loading": "Loading…",
|
|
105
|
+
"kumiko.list.group.loaded": "{loaded} of {count}",
|
|
106
|
+
"kumiko.list.measure.sum": "Sum of {label}",
|
|
107
|
+
"kumiko.list.measure.avg": "Average {label}",
|
|
108
|
+
"kumiko.list.measure.count": "Count",
|
|
109
|
+
"kumiko.list.measure.min": "Minimum {label}",
|
|
110
|
+
"kumiko.list.measure.max": "Maximum {label}",
|
|
111
|
+
"kumiko.list.measure.kind.sum": "Sum",
|
|
112
|
+
"kumiko.list.measure.kind.avg": "Average",
|
|
113
|
+
"kumiko.list.measure.kind.count": "Count",
|
|
114
|
+
"kumiko.list.measure.kind.min": "Minimum",
|
|
115
|
+
"kumiko.list.measure.kind.max": "Maximum",
|
|
116
|
+
"kumiko.list.totals.label": "Total",
|
|
117
|
+
"kumiko.list.range.min": "From",
|
|
118
|
+
"kumiko.list.range.max": "To",
|
|
119
|
+
"kumiko.list.notice.truncated": "Not every group is shown. Please narrow the list with a filter.",
|
|
120
|
+
"kumiko.list.notice.searchTruncated": "The search matched too many entries; groups and totals cover only part of them. Please filter further.",
|
|
121
|
+
"kumiko.list.view.column.move-up": "Move {column} up",
|
|
122
|
+
"kumiko.list.view.pinning": "Pinned columns",
|
|
123
|
+
"kumiko.list.view.pin-left": "Pin {column} left",
|
|
124
|
+
"kumiko.list.view.pin-right": "Pin {column} right",
|
|
125
|
+
"kumiko.list.column.resize": "Resize {column}",
|
|
126
|
+
"kumiko.list.view.column.move-down": "Move {column} down",
|
|
85
127
|
"kumiko.list.related-list-truncated": "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
|
|
86
128
|
"kumiko.reference.system-tenant": "System",
|
|
87
129
|
"kumiko.reference.system-user": "System",
|
|
@@ -107,6 +149,21 @@ export const kumikoDefaultTranslations = {
|
|
|
107
149
|
"kumiko.theme.light": "Light theme",
|
|
108
150
|
"kumiko.list.row.collapse": "Collapse {title}",
|
|
109
151
|
"kumiko.list.row.expand": "Expand {title}",
|
|
152
|
+
"kumiko.list.bulk.selected": "{count} selected",
|
|
153
|
+
"kumiko.list.bulk.clear": "Clear selection",
|
|
154
|
+
"kumiko.list.bulk.limit": "At most {max} rows per selection",
|
|
155
|
+
"kumiko.list.bulk.select-all": "Select all rows on this page",
|
|
156
|
+
"kumiko.list.bulk.select-row": "Select {title}",
|
|
157
|
+
"kumiko.list.bulk.progress": "{settled} of {total} done",
|
|
158
|
+
"kumiko.list.bulk.cancel": "Cancel",
|
|
159
|
+
"kumiko.list.bulk.cancelling": "Cancelling…",
|
|
160
|
+
"kumiko.list.bulk.confirm": "Run “{action}” on the selection ({count})?",
|
|
161
|
+
"kumiko.list.bulk.result.title": "“{action}” finished",
|
|
162
|
+
"kumiko.list.bulk.result.succeeded": "Succeeded: {count}",
|
|
163
|
+
"kumiko.list.bulk.result.failed": "Failed: {count}",
|
|
164
|
+
"kumiko.list.bulk.result.skipped": "Skipped: {count}",
|
|
165
|
+
"kumiko.list.bulk.result.not-started": "Not run: {count}",
|
|
166
|
+
"kumiko.list.bulk.result.dismiss": "Dismiss",
|
|
110
167
|
"kumiko.list.sort.by": "Sorted by {column}",
|
|
111
168
|
"kumiko.form.changed": "changed",
|
|
112
169
|
"kumiko.form.on-this-page": "On this page",
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { type Formality, formalLocaleTag } from "@cosmicdrift/kumiko-framework/u
|
|
|
2
2
|
export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.ts";
|
|
3
3
|
export type { AppFeaturesProviderProps } from "./app/app-features-context.tsx";
|
|
4
4
|
export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.tsx";
|
|
5
|
+
export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.ts";
|
|
6
|
+
export { BasePathProvider, useBasePath } from "./app/base-path-context.tsx";
|
|
5
7
|
export type { ActionNotice, ActionNotifier, ClientActionHandler, ClientActionOutcome, ClientActionRuntime, ClientActionsMap, } from "./app/client-actions.tsx";
|
|
6
8
|
export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.tsx";
|
|
7
9
|
export type { ColumnRendererComponent, ColumnRendererProps, ColumnRenderersMap, ColumnRenderersProviderProps, } from "./app/column-renderers.tsx";
|
|
@@ -14,6 +16,7 @@ export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-
|
|
|
14
16
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.tsx";
|
|
15
17
|
export type { DashboardBodyProps, DashboardBodyProviderProps } from "./app/dashboard-body.tsx";
|
|
16
18
|
export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.tsx";
|
|
19
|
+
export { type TenantCurrencyResolution, useTenantCurrency } from "./app/entity-edit-prefill.ts";
|
|
17
20
|
export { type ErrorNotice, ErrorReference, errorNoticeOf } from "./app/error-reference.tsx";
|
|
18
21
|
export type { ExtensionFormRegistry, ExtensionFormSubmitHandler, ExtensionSubmitContext, ExtensionSubmitResult, } from "./app/extension-form-submit.tsx";
|
|
19
22
|
export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.tsx";
|
|
@@ -85,7 +88,7 @@ export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.ts";
|
|
|
85
88
|
export { hasUnsavedInput, registerUnsavedInputSource, requestLeave, setLeaveConfirmer, useUnsavedInputSource, } from "./leave-guard.ts";
|
|
86
89
|
export { LeaveGuardDialog } from "./leave-guard-dialog.tsx";
|
|
87
90
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.ts";
|
|
88
|
-
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode,
|
|
91
|
+
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, BulkRowOutcome, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableBulk, DataTableBulkAction, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.tsx";
|
|
89
92
|
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.tsx";
|
|
90
93
|
export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.tsx";
|
|
91
94
|
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.tsx";
|
package/dist/index.js
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
export { formalLocaleTag } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
12
12
|
export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.js";
|
|
13
13
|
export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.js";
|
|
14
|
+
export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.js";
|
|
15
|
+
export { BasePathProvider, useBasePath } from "./app/base-path-context.js";
|
|
14
16
|
export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.js";
|
|
15
17
|
export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers.js";
|
|
16
18
|
export { CONTENT_EDITOR_ELEMENT_ID, ContentEditorsProvider, TextareaContentEditor, useContentEditor, } from "./app/content-editors.js";
|
|
17
19
|
export { ContentPreview, substituteVariables } from "./app/content-preview.js";
|
|
18
20
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.js";
|
|
19
21
|
export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.js";
|
|
22
|
+
export { useTenantCurrency } from "./app/entity-edit-prefill.js";
|
|
20
23
|
export { ErrorReference, errorNoticeOf } from "./app/error-reference.js";
|
|
21
24
|
export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.js";
|
|
22
25
|
export { ExtensionSectionsProvider, extensionSectionName, useExtensionSectionComponent, } from "./app/extension-sections.js";
|
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
|
|
@@ -203,6 +206,7 @@ export type InputProps = {
|
|
|
203
206
|
readonly name: string;
|
|
204
207
|
readonly value: string;
|
|
205
208
|
readonly onChange: (v: string) => void;
|
|
209
|
+
readonly autoFocus?: boolean;
|
|
206
210
|
readonly disabled?: boolean;
|
|
207
211
|
readonly required?: boolean;
|
|
208
212
|
readonly hasError?: boolean;
|
|
@@ -228,6 +232,24 @@ export type InputProps = {
|
|
|
228
232
|
/** Raw keydown access (e.g. Enter-to-submit at the field instead of
|
|
229
233
|
* the surrounding Form). Forwarded to the element. */
|
|
230
234
|
readonly onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
235
|
+
} | {
|
|
236
|
+
/** Fixed-length numeric one-time code. `onComplete` fires once when input,
|
|
237
|
+
* paste or autofill brings the value to full length; the value holds
|
|
238
|
+
* digits only. */
|
|
239
|
+
readonly kind: "code";
|
|
240
|
+
readonly id: string;
|
|
241
|
+
readonly name: string;
|
|
242
|
+
readonly value: string;
|
|
243
|
+
readonly onChange: (v: string) => void;
|
|
244
|
+
readonly length: number;
|
|
245
|
+
readonly onComplete?: (code: string) => void;
|
|
246
|
+
readonly autoFocus?: boolean;
|
|
247
|
+
readonly disabled?: boolean;
|
|
248
|
+
readonly required?: boolean;
|
|
249
|
+
readonly hasError?: boolean;
|
|
250
|
+
readonly testId?: string;
|
|
251
|
+
/** Default "one-time-code". */
|
|
252
|
+
readonly autoComplete?: string;
|
|
231
253
|
} | {
|
|
232
254
|
readonly kind: "email";
|
|
233
255
|
readonly id: string;
|
|
@@ -628,55 +650,11 @@ export type DataTableRowAction = {
|
|
|
628
650
|
export declare function shouldRenderActionsIconOnly(actions: readonly {
|
|
629
651
|
readonly icon?: IconKey;
|
|
630
652
|
}[]): boolean;
|
|
631
|
-
export type
|
|
632
|
-
|
|
633
|
-
readonly field: string;
|
|
634
|
-
/** Translated Button-Label ("Status", "Active"). */
|
|
635
|
-
readonly label: string;
|
|
636
|
-
/** Selectable values with display labels. `value` is the raw filter value
|
|
637
|
-
* (a select option or `"true"`/`"false"`), transported as a string. */
|
|
638
|
-
readonly options: readonly {
|
|
639
|
-
readonly value: string;
|
|
640
|
-
readonly label: string;
|
|
641
|
-
}[];
|
|
642
|
-
/** Present: render the facet as a row of single-choice toggle chips instead
|
|
643
|
-
* of a dropdown. A chip is pressed while the selection equals its values. */
|
|
644
|
-
readonly chips?: readonly DataTableFacetChip[];
|
|
645
|
-
/** Chips only: show each chip's `count` next to its label. */
|
|
646
|
-
readonly showCounts?: boolean;
|
|
647
|
-
/** Chips only: hide chips whose `count` is 0 (unless pressed). */
|
|
648
|
-
readonly hideEmpty?: boolean;
|
|
649
|
-
};
|
|
650
|
-
export type DataTableFacetChip = {
|
|
651
|
-
readonly id: string;
|
|
652
|
-
/** Translated chip label. */
|
|
653
|
-
readonly label: string;
|
|
654
|
-
/** Selection a click sets; `[]` clears the facet. */
|
|
655
|
-
readonly values: readonly string[];
|
|
656
|
-
/** Matching row count, undefined while unknown. */
|
|
657
|
-
readonly count?: number;
|
|
658
|
-
};
|
|
659
|
-
export type DataTableDateRangeFacet = {
|
|
660
|
-
/** The facet's namespace (its date field) — a key, not a filter value. */
|
|
661
|
-
readonly field: string;
|
|
662
|
-
/** Translated label ("When"). */
|
|
663
|
-
readonly label: string;
|
|
664
|
-
readonly from: string;
|
|
665
|
-
readonly to: string;
|
|
666
|
-
};
|
|
667
|
-
/** Splits rows into collapsible groups (order of first appearance). A
|
|
668
|
-
* renderer without grouping support shows the flat list. A group whose
|
|
669
|
-
* `headerLabel` is undefined shows its rows without a header and never
|
|
670
|
-
* collapses. */
|
|
671
|
-
export type DataTableRowGrouping = {
|
|
672
|
-
readonly keyOf: (row: ListRowViewModel) => string;
|
|
673
|
-
readonly headerLabel: (groupKey: string, rows: readonly ListRowViewModel[]) => string | undefined;
|
|
674
|
-
readonly startsCollapsed: (groupKey: string) => boolean;
|
|
675
|
-
};
|
|
653
|
+
export type { BulkRowOutcome, DataTableBulk, DataTableBulkAction, } from "./data-table-bulk-types.ts";
|
|
654
|
+
export type { DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, } from "./data-table-facet-types.ts";
|
|
676
655
|
export type DataTableProps = {
|
|
677
656
|
readonly columns: readonly ListColumnViewModel[];
|
|
678
657
|
readonly rows: readonly ListRowViewModel[];
|
|
679
|
-
readonly rowGrouping?: DataTableRowGrouping;
|
|
680
658
|
/** Tints a row. Renderers without row tinting ignore it. */
|
|
681
659
|
readonly rowTone?: (row: ListRowViewModel) => SelectOptionTone | undefined;
|
|
682
660
|
readonly onRowClick?: (row: ListRowViewModel) => void;
|
|
@@ -704,6 +682,8 @@ export type DataTableProps = {
|
|
|
704
682
|
* kebab). Actions then sit at the same x-position across all rows
|
|
705
683
|
* (no drifting with label width) and all lists in an app look alike. */
|
|
706
684
|
readonly rowActionMode?: DataTableRowActionMode;
|
|
685
|
+
/** Present: row checkboxes plus a selection bar that runs these actions once per selected row. */
|
|
686
|
+
readonly bulk?: DataTableBulk;
|
|
707
687
|
/** Custom empty-state content (e.g. Icon + Heading + CTA-Button).
|
|
708
688
|
* The default renderer wraps it in a dashed-border box. */
|
|
709
689
|
readonly emptyState?: ReactNode;
|
|
@@ -813,7 +793,8 @@ export type DataTableProps = {
|
|
|
813
793
|
/** Content of the area under an expanded row. Native renderers implement
|
|
814
794
|
* the same three props. */
|
|
815
795
|
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
816
|
-
};
|
|
796
|
+
} & DataTableTableBlock;
|
|
797
|
+
export type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
|
|
817
798
|
/** Cell type for one column of an embedded-list field. Mirrors
|
|
818
799
|
* EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-renderer/headless`. */
|
|
819
800
|
export type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
|
|
@@ -1384,6 +1365,8 @@ export type JsonViewProps = {
|
|
|
1384
1365
|
* PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
|
|
1385
1366
|
export type FileValueListProps = {
|
|
1386
1367
|
readonly fileIds: readonly string[];
|
|
1368
|
+
/** Starts the inline PDF preview of the first file open. */
|
|
1369
|
+
readonly previewOpenByDefault?: boolean;
|
|
1387
1370
|
readonly testId?: string;
|
|
1388
1371
|
};
|
|
1389
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,9 +3,10 @@ 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
|
+
import { clearFileMetaCache } from "../primitives/file-thumbnail.js";
|
|
9
10
|
import { defaultPrimitives, ScreenWidthProvider } from "../primitives/index.js";
|
|
10
11
|
import { ToastProvider } from "../primitives/toast.js";
|
|
11
12
|
import { createEventSourceLiveEvents } from "../sse/live-events.js";
|
|
@@ -18,6 +19,7 @@ import { stackWrappers } from "./client-plugin.js";
|
|
|
18
19
|
import { WebDashboardBody } from "./dashboard-body.js";
|
|
19
20
|
import { DocumentLangSync } from "./document-lang-sync.js";
|
|
20
21
|
import { createBrowserDraftStorage } from "./draft-storage.js";
|
|
22
|
+
import { ListStateStoreBoot } from "./list-state-store-boot.js";
|
|
21
23
|
import { useBrowserNavApi } from "./nav.js";
|
|
22
24
|
import { NavProvidersProvider } from "./nav-providers-context.js";
|
|
23
25
|
import { ResolversProvider } from "./resolvers-context.js";
|
|
@@ -236,7 +238,7 @@ function KumikoAppRoot(props) {
|
|
|
236
238
|
warnMissingCustomScreens(loaded, props.customScreens);
|
|
237
239
|
setApp(loaded);
|
|
238
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 }) }));
|
|
239
|
-
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 }))) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
|
|
240
242
|
}
|
|
241
243
|
// Sits INSIDE the clientFeature gates (e.g. an auth gate) — a gate that
|
|
242
244
|
// renders a login placeholder instead of children keeps this component
|
|
@@ -255,9 +257,13 @@ function AppSchemaBoundary({ app, screenQn, schemaUrl, navAdapter, onAppLoaded,
|
|
|
255
257
|
onUnmountResetSchemaRef.current = onUnmountResetSchema;
|
|
256
258
|
// A real unmount (the gate withdrawing children, e.g. on logout) resets
|
|
257
259
|
// the schema so the next mount re-fetches instead of reusing this
|
|
258
|
-
// session's — fetch-mode only, see isFetchMode in KumikoAppRoot.
|
|
260
|
+
// session's — fetch-mode only, see isFetchMode in KumikoAppRoot. The file
|
|
261
|
+
// meta cache is dropped in every mode.
|
|
259
262
|
useEffect(() => {
|
|
260
|
-
return () =>
|
|
263
|
+
return () => {
|
|
264
|
+
clearFileMetaCache();
|
|
265
|
+
onUnmountResetSchemaRef.current?.();
|
|
266
|
+
};
|
|
261
267
|
}, []);
|
|
262
268
|
if (app === undefined) {
|
|
263
269
|
return (_jsx(AppSchemaFetchBoot, { onLoaded: onAppLoaded, ...(schemaUrl !== undefined && { schemaUrl }) }));
|
|
@@ -335,7 +341,9 @@ export function createKumikoApp(options = {}) {
|
|
|
335
341
|
}
|
|
336
342
|
}
|
|
337
343
|
const localeResolver = options.locale ?? createBrowserLocaleResolver();
|
|
338
|
-
const
|
|
344
|
+
const { basePath } = options;
|
|
345
|
+
const useBrowserNavApiWithBasePath = (navOptions) => useBrowserNavApi({ ...navOptions, ...(basePath !== undefined && { basePath }) });
|
|
346
|
+
const navAdapter = options.navAdapter ?? useBrowserNavApiWithBasePath;
|
|
339
347
|
const reporter = options.reportClientErrors === false ? undefined : createClientErrorReporter();
|
|
340
348
|
reporter?.installGlobalHandlers();
|
|
341
349
|
const handleBoundaryError = (error, info, context) => {
|
|
@@ -348,7 +356,7 @@ export function createKumikoApp(options = {}) {
|
|
|
348
356
|
options.onError?.(error, info, context);
|
|
349
357
|
};
|
|
350
358
|
const root = createRoot(container);
|
|
351
|
-
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 }) }) }) }) }));
|
|
352
360
|
return { root };
|
|
353
361
|
}
|
|
354
362
|
// TokensBoot uses the browser-backed TokensApi hook (class-based
|
|
@@ -498,7 +506,7 @@ function RoutedScreen({ app, fallbackQn, translate, onRowClick, }) {
|
|
|
498
506
|
}, [nav]);
|
|
499
507
|
if (feature === undefined || activeScreen === undefined) {
|
|
500
508
|
const landingExists = findOwnerFeature(app.features, fallbackQn) !== undefined;
|
|
501
|
-
return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: qn }));
|
|
509
|
+
return (_jsx(ScreenMissingState, { ...(landingExists && { homeScreenId: lastSegment(fallbackQn) }), screenQn: nav.route?.screenId ?? qn }));
|
|
502
510
|
}
|
|
503
511
|
// KumikoScreen takes a single-feature schema: the feature owning the active screen.
|
|
504
512
|
const schemaForScreen = feature;
|
|
@@ -140,6 +140,20 @@ function useVisibilityGateResults() {
|
|
|
140
140
|
}, []);
|
|
141
141
|
return { results, report, forget };
|
|
142
142
|
}
|
|
143
|
+
const NOOP_REFETCH = async () => { };
|
|
144
|
+
// A gate reports through an effect, so for its first commit (and after a payload change) no
|
|
145
|
+
// result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
|
|
146
|
+
function withPendingGates(results, gateKeys) {
|
|
147
|
+
if (gateKeys.every((key) => results.has(key)))
|
|
148
|
+
return results;
|
|
149
|
+
const merged = new Map(results);
|
|
150
|
+
for (const key of gateKeys) {
|
|
151
|
+
if (!merged.has(key)) {
|
|
152
|
+
merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return merged;
|
|
156
|
+
}
|
|
143
157
|
function distinctVisibilityGates(panels, screenParams) {
|
|
144
158
|
const gates = new Map();
|
|
145
159
|
for (const panel of panels) {
|
|
@@ -227,6 +241,8 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
|
|
|
227
241
|
const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
|
|
228
242
|
const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
|
|
229
243
|
const gateResults = useVisibilityGateResults();
|
|
244
|
+
const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
|
|
245
|
+
const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
|
|
230
246
|
// The record query is not a panel query: it needs the interval before the
|
|
231
247
|
// registry context below exists, and `refresh.field` reads from its own row.
|
|
232
248
|
const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
|
|
@@ -271,7 +287,7 @@ export function WebDashboardBody({ featureName, screen, translate, }) {
|
|
|
271
287
|
// The shell header owns the title; the badge and the actions sit in it there.
|
|
272
288
|
const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
|
|
273
289
|
const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
|
|
274
|
-
return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value:
|
|
290
|
+
return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
|
|
275
291
|
if (panel.kind === "screen") {
|
|
276
292
|
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
|
|
277
293
|
}
|
|
@@ -311,7 +311,7 @@ function TextBlockView({ block, translate, }) {
|
|
|
311
311
|
? "text-sm font-medium text-foreground"
|
|
312
312
|
: "text-sm text-muted-foreground", children: translate(block.text) }));
|
|
313
313
|
case "list":
|
|
314
|
-
return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: block.items.map((
|
|
314
|
+
return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: withOccurrenceKeys(block.items, (item) => item).map(({ entry, key }) => (_jsx("li", { children: translate(entry) }, key))) }));
|
|
315
315
|
case "code":
|
|
316
316
|
return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
|
|
317
317
|
case "hint":
|
|
@@ -330,6 +330,16 @@ function TextBlockLink({ block, translate, }) {
|
|
|
330
330
|
const target = { screenId: lastSegment(block.screen) };
|
|
331
331
|
return (_jsx(Link, { href: nav.hrefFor(target), onPress: () => nav.navigate(target), testId: `dashboard-text-link-${target.screenId}`, children: translate(block.text) }));
|
|
332
332
|
}
|
|
333
|
+
// Content-derived keys collide for identical entries; the occurrence count keeps them unique.
|
|
334
|
+
function withOccurrenceKeys(entries, contentKey) {
|
|
335
|
+
const seen = new Map();
|
|
336
|
+
return entries.map((entry) => {
|
|
337
|
+
const content = contentKey(entry);
|
|
338
|
+
const occurrence = seen.get(content) ?? 0;
|
|
339
|
+
seen.set(content, occurrence + 1);
|
|
340
|
+
return { entry, key: `${content}#${occurrence}` };
|
|
341
|
+
});
|
|
342
|
+
}
|
|
333
343
|
function textBlockKey(block) {
|
|
334
344
|
switch (block.kind) {
|
|
335
345
|
case "list":
|
|
@@ -341,7 +351,7 @@ function textBlockKey(block) {
|
|
|
341
351
|
}
|
|
342
352
|
}
|
|
343
353
|
export function TextPanelBody({ panel, label, translate, }) {
|
|
344
|
-
const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: panel.blocks.map((
|
|
354
|
+
const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: withOccurrenceKeys(panel.blocks, textBlockKey).map(({ entry, key }) => (_jsx(TextBlockView, { block: entry, translate: translate }, key))) }));
|
|
345
355
|
const testId = `dashboard-panel-${panel.id}`;
|
|
346
356
|
if (label !== undefined) {
|
|
347
357
|
return (_jsx(PanelShell, { testId: testId, title: label, children: blocks }));
|
|
@@ -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;
|