@cosmicdrift/kumiko-renderer 0.349.0 → 0.351.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/facet-count-bridge.d.ts +3 -1
- package/dist/app/facet-count-bridge.js +17 -7
- package/dist/app/kumiko-screen.js +8 -3
- package/dist/app/list-facets.js +1 -1
- package/dist/app/nav.d.ts +8 -3
- package/dist/hooks/use-list-url-state.d.ts +2 -0
- package/dist/hooks/use-list-url-state.js +6 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/primitives.d.ts +6 -0
- package/package.json +5 -5
|
@@ -3,8 +3,10 @@ export type FacetCountQuery = {
|
|
|
3
3
|
readonly key: string;
|
|
4
4
|
readonly payload: Readonly<Record<string, unknown>>;
|
|
5
5
|
};
|
|
6
|
-
export declare function FacetCountBridges({ queryType, queries, onCount, }: {
|
|
6
|
+
export declare function FacetCountBridges({ queryType, queries, onCount, refreshNonce, }: {
|
|
7
7
|
readonly queryType: string;
|
|
8
8
|
readonly queries: readonly FacetCountQuery[];
|
|
9
9
|
readonly onCount: (key: string, count: number) => void;
|
|
10
|
+
/** Bumped after a write; every bridge refetches its count. */
|
|
11
|
+
readonly refreshNonce: number;
|
|
10
12
|
}): ReactNode;
|
|
@@ -2,19 +2,29 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
// One invisible bridge per facet chip, mirroring ReferenceFacetBridges — the
|
|
3
3
|
// chip count varies per screen, so useQuery can't be called in a loop. Each
|
|
4
4
|
// bridge asks the list query for a single row plus `total`.
|
|
5
|
-
// ponytail: one extra list query per chip, refetched
|
|
6
|
-
//
|
|
7
|
-
|
|
5
|
+
// ponytail: one extra list query per chip, refetched with the list (live
|
|
6
|
+
// events and `refreshNonce` bumps after writes); a server-side facet-count
|
|
7
|
+
// handler replaces this if it hurts.
|
|
8
|
+
import { useEffect, useRef } from "react";
|
|
8
9
|
import { useQuery } from "../hooks/use-query.js";
|
|
9
|
-
function FacetCountBridge({ countKey, queryType, payload, onCount, }) {
|
|
10
|
-
const result = useQuery(queryType, payload);
|
|
10
|
+
function FacetCountBridge({ countKey, queryType, payload, onCount, refreshNonce, }) {
|
|
11
|
+
const result = useQuery(queryType, payload, { live: true });
|
|
11
12
|
const total = result.data?.total;
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
if (total !== undefined)
|
|
14
15
|
onCount(countKey, total);
|
|
15
16
|
}, [total, countKey, onCount]);
|
|
17
|
+
const { refetch } = result;
|
|
18
|
+
const seenNonce = useRef(refreshNonce);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// skip: the mount fetch already covers the nonce the bridge started with
|
|
21
|
+
if (seenNonce.current === refreshNonce)
|
|
22
|
+
return;
|
|
23
|
+
seenNonce.current = refreshNonce;
|
|
24
|
+
void refetch();
|
|
25
|
+
}, [refreshNonce, refetch]);
|
|
16
26
|
return null;
|
|
17
27
|
}
|
|
18
|
-
export function FacetCountBridges({ queryType, queries, onCount, }) {
|
|
19
|
-
return (_jsx(_Fragment, { children: queries.map((query) => (_jsx(FacetCountBridge, { countKey: query.key, queryType: queryType, payload: query.payload, onCount: onCount }, query.key))) }));
|
|
28
|
+
export function FacetCountBridges({ queryType, queries, onCount, refreshNonce, }) {
|
|
29
|
+
return (_jsx(_Fragment, { children: queries.map((query) => (_jsx(FacetCountBridge, { countKey: query.key, queryType: queryType, payload: query.payload, onCount: onCount, refreshNonce: refreshNonce }, query.key))) }));
|
|
20
30
|
}
|
|
@@ -3,8 +3,8 @@ import { evalFieldCondition, isWriteFormEditSection, metricField, parseRefTarget
|
|
|
3
3
|
import { applyFormatSpec, buildOptionLabels, computeRelatedListSectionViewModel, fieldLabelKey, fieldOptionLabelKey, isSafeHref, } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
5
5
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
6
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
6
7
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
-
import { Temporal } from "temporal-polyfill";
|
|
8
8
|
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
9
9
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
10
10
|
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
@@ -1193,7 +1193,11 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1193
1193
|
cursor,
|
|
1194
1194
|
]);
|
|
1195
1195
|
const rowsQuery = useQuery(queryType, queryPayload, { live: true });
|
|
1196
|
+
// Facet counts come from their own queries, which a write leaves untouched.
|
|
1197
|
+
const [facetCountRefreshNonce, setFacetCountRefreshNonce] = useState(0);
|
|
1198
|
+
const refreshFacetCounts = useCallback(() => setFacetCountRefreshNonce((n) => n + 1), []);
|
|
1196
1199
|
const refreshRowsAfterWrite = useCallback(async () => {
|
|
1200
|
+
refreshFacetCounts();
|
|
1197
1201
|
if (useInfinite) {
|
|
1198
1202
|
setAccumulated([]);
|
|
1199
1203
|
setCursor(undefined);
|
|
@@ -1201,7 +1205,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1201
1205
|
return;
|
|
1202
1206
|
}
|
|
1203
1207
|
await refetchAfterWrite(rowsQuery.refetch);
|
|
1204
|
-
}, [useInfinite, rowsQuery.refetch]);
|
|
1208
|
+
}, [useInfinite, rowsQuery.refetch, refreshFacetCounts]);
|
|
1205
1209
|
// Infinite-Scroll: bei jedem erfolgreichen Result die rows appenden +
|
|
1206
1210
|
// hasMore aus nextCursor ableiten. Live-Updates (postgres NOTIFY) und
|
|
1207
1211
|
// initiale Loads laufen beide hier durch — das useEffect-Dep-Array
|
|
@@ -1510,7 +1514,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1510
1514
|
// Bei Infinite-Mode: rows kommen aus accumulated (über mehrere
|
|
1511
1515
|
// useQuery-Calls gesammelt), bei pages/false aus dem aktuellen Result.
|
|
1512
1516
|
const renderRows = useInfinite ? accumulated : (rowsQuery.data?.rows ?? []);
|
|
1513
|
-
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount }), _jsx(RenderList, { screen: screen, entity: entity, rows: renderRows, featureName: featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1517
|
+
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: entityFacetSpecs, onOptions: handleEntityFacetOptions }), _jsx(FacetCountBridges, { queryType: queryType, queries: facetCountQueries, onCount: handleFacetCount, refreshNonce: facetCountRefreshNonce }), _jsx(RenderList, { screen: screen, entity: entity, rows: renderRows, featureName: featureName, searchable: searchable, searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1514
1518
|
? { screenPadding: false, chromeless: true }
|
|
1515
1519
|
: { screenPadding: true }), ...(screenFillsHeight(screen) && { scrollBody: true }), ...(pager !== undefined && { pager }), ...(rowActions !== undefined && { rowActions }), ...(screen.rowActionMode !== undefined && { rowActionMode: screen.rowActionMode }), ...(toolbarActions !== undefined && toolbarActions.length > 0 && { toolbarActions }), ...(useInfinite && {
|
|
1516
1520
|
onReachEnd: loadMore,
|
|
@@ -1528,6 +1532,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1528
1532
|
}) }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
1529
1533
|
closeDrawer();
|
|
1530
1534
|
invalidateExpansions();
|
|
1535
|
+
refreshFacetCounts();
|
|
1531
1536
|
void rowsQuery.refetch();
|
|
1532
1537
|
} })] }));
|
|
1533
1538
|
}
|
package/dist/app/list-facets.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Split from kumiko-screen.tsx: related-list-section.tsx renders through
|
|
2
2
|
// kumiko-screen.tsx, so importing this back from there would be a require cycle.
|
|
3
3
|
import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
|
-
import { Temporal } from "temporal
|
|
4
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
// Fills in a "reference" spec's initially-empty `options` from the
|
|
6
6
|
// field-keyed options ReferenceFacetBridges resolved, shared by every caller.
|
|
7
7
|
export function mergeReferenceFacetOptions(specs, optionsByField) {
|
package/dist/app/nav.d.ts
CHANGED
|
@@ -16,23 +16,28 @@ export type ObjectTarget = {
|
|
|
16
16
|
readonly id: string;
|
|
17
17
|
};
|
|
18
18
|
export type NavTarget = ScreenTarget | ObjectTarget;
|
|
19
|
+
export type NavigateOptions = {
|
|
20
|
+
/** Search params of the target route, set in the same history entry as
|
|
21
|
+
* the path. Without them the target starts with an empty query. */
|
|
22
|
+
readonly searchParams?: Readonly<Record<string, string>>;
|
|
23
|
+
};
|
|
19
24
|
export type NavApi = {
|
|
20
25
|
/** Current route — `undefined` when the URL is at the root / there's
|
|
21
26
|
* no route selected. Caller's initial fallback kicks in then. */
|
|
22
27
|
readonly route: NavRoute | undefined;
|
|
23
28
|
/** Push a new route. Platform-specific Impl writes to
|
|
24
29
|
* history/stack and notifies subscribers. */
|
|
25
|
-
readonly navigate: (target: NavTarget) => void;
|
|
30
|
+
readonly navigate: (target: NavTarget, options?: NavigateOptions) => void;
|
|
26
31
|
/** Replace the current route in place. Same effect as navigate from
|
|
27
32
|
* the user's perspective, but doesn't add a history entry — used for
|
|
28
33
|
* mount-time URL fills (e.g. WorkspaceShell defaulting to `/admin/x`
|
|
29
34
|
* when the user typed `/`). Browser Back must take the user out of
|
|
30
35
|
* the app, not back to the original empty path. */
|
|
31
|
-
readonly replace: (target: NavTarget) => void;
|
|
36
|
+
readonly replace: (target: NavTarget, options?: NavigateOptions) => void;
|
|
32
37
|
/** Build the href a click on {target} would produce. Used by
|
|
33
38
|
* platform-specific Link-Komponenten (Web: `<a href>`; Native
|
|
34
39
|
* typically doesn't need this). */
|
|
35
|
-
readonly hrefFor: (target: NavTarget) => string;
|
|
40
|
+
readonly hrefFor: (target: NavTarget, options?: NavigateOptions) => string;
|
|
36
41
|
/** Lese-Snapshot der aktuellen Search-Params (Browser: ?key=value-
|
|
37
42
|
* Pairs nach dem Pfad). Native-Impls die kein URL-Konzept haben
|
|
38
43
|
* liefern ein leeres Object. Wert ist ein Plain-Record (kein Map)
|
|
@@ -41,4 +41,6 @@ export type ListUrlStateApi = ListUrlState & {
|
|
|
41
41
|
}) => void;
|
|
42
42
|
};
|
|
43
43
|
export declare const PAGE_SIZE_OPTIONS: readonly number[];
|
|
44
|
+
/** URL key of a list facet filter. Its value is the comma-joined selected values. */
|
|
45
|
+
export declare function listFilterUrlKey(screenId: string, field: string): string;
|
|
44
46
|
export declare function useListUrlState(screenId: string, defaultFilters?: Readonly<Record<string, readonly string[]>>): ListUrlStateApi;
|
|
@@ -33,6 +33,10 @@ function parsePageSize(value) {
|
|
|
33
33
|
function key(screenId, suffix) {
|
|
34
34
|
return `${screenId}.${suffix}`;
|
|
35
35
|
}
|
|
36
|
+
/** URL key of a list facet filter. Its value is the comma-joined selected values. */
|
|
37
|
+
export function listFilterUrlKey(screenId, field) {
|
|
38
|
+
return key(screenId, `f.${field}`);
|
|
39
|
+
}
|
|
36
40
|
function parseDir(value) {
|
|
37
41
|
return value === "asc" || value === "desc" ? value : undefined;
|
|
38
42
|
}
|
|
@@ -55,7 +59,7 @@ export function useListUrlState(screenId, defaultFilters) {
|
|
|
55
59
|
const q = params[key(screenId, "q")] ?? "";
|
|
56
60
|
const page = parsePage(params[key(screenId, "page")]);
|
|
57
61
|
const pageSize = parsePageSize(params[key(screenId, "size")]);
|
|
58
|
-
const filterPrefix =
|
|
62
|
+
const filterPrefix = listFilterUrlKey(screenId, "");
|
|
59
63
|
const filters = useMemo(() => {
|
|
60
64
|
const out = {};
|
|
61
65
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -110,7 +114,7 @@ export function useListUrlState(screenId, defaultFilters) {
|
|
|
110
114
|
}, [nav, screenId]);
|
|
111
115
|
const setFilter = useCallback((field, values) => {
|
|
112
116
|
nav.setSearchParams({
|
|
113
|
-
[
|
|
117
|
+
[listFilterUrlKey(screenId, field)]: values.length === 0 ? clearedFilterValue(field) : values.join(","),
|
|
114
118
|
[key(screenId, "page")]: null,
|
|
115
119
|
});
|
|
116
120
|
}, [nav, screenId, clearedFilterValue]);
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type { KumikoScreenProps } from "./app/kumiko-screen.js";
|
|
|
22
22
|
export { KumikoScreen, qualifyNavId, qualifyScreenId } from "./app/kumiko-screen.js";
|
|
23
23
|
export type { DateRangeBound, DateRangeValue, ResolvedDateRangeFacet } from "./app/list-facets.js";
|
|
24
24
|
export { buildDateRangePayload, clampDateRange, readDateRange, resolveDateRangeFacets, } from "./app/list-facets.js";
|
|
25
|
-
export type { NavApi, NavProviderProps, NavRoute, NavTarget, ObjectTarget, ScreenTarget, } from "./app/nav.js";
|
|
25
|
+
export type { NavApi, NavigateOptions, NavProviderProps, NavRoute, NavTarget, ObjectTarget, ScreenTarget, } from "./app/nav.js";
|
|
26
26
|
export { formatPath, hasDetailScreen, NavProvider, parsePath, resolveTarget, useNav, useNavigateWithInitialValues, } from "./app/nav.js";
|
|
27
27
|
export { lastSegment } from "./app/qn.js";
|
|
28
28
|
export type { ReturnHost, ReturnTo } from "./app/return-to.js";
|
|
@@ -57,7 +57,7 @@ export { useDisclosure } from "./hooks/use-disclosure.js";
|
|
|
57
57
|
export type { UseFormOptions, UseFormResult } from "./hooks/use-form.js";
|
|
58
58
|
export { useForm } from "./hooks/use-form.js";
|
|
59
59
|
export type { ListSort, ListSortDir, ListUrlState, ListUrlStateApi, } from "./hooks/use-list-url-state.js";
|
|
60
|
-
export { useListUrlState } from "./hooks/use-list-url-state.js";
|
|
60
|
+
export { listFilterUrlKey, useListUrlState } from "./hooks/use-list-url-state.js";
|
|
61
61
|
export type { UseMutationResult } from "./hooks/use-mutation.js";
|
|
62
62
|
export { useMutation } from "./hooks/use-mutation.js";
|
|
63
63
|
export type { UseQueryOptions, UseQueryResult } from "./hooks/use-query.js";
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export { REFERENCE_COMBOBOX_LIMIT, REFERENCE_LIST_LOOKUP_LIMIT, REFERENCE_SEARCH
|
|
|
42
42
|
export { AI_TEXT_RUN_QN, useAiTextAction, useCompletion } from "./hooks/use-ai-text.js";
|
|
43
43
|
export { useDisclosure } from "./hooks/use-disclosure.js";
|
|
44
44
|
export { useForm } from "./hooks/use-form.js";
|
|
45
|
-
export { useListUrlState } from "./hooks/use-list-url-state.js";
|
|
45
|
+
export { listFilterUrlKey, useListUrlState } from "./hooks/use-list-url-state.js";
|
|
46
46
|
export { useMutation } from "./hooks/use-mutation.js";
|
|
47
47
|
export { entityFromQueryType, useQuery } from "./hooks/use-query.js";
|
|
48
48
|
export { useReportStepComplete } from "./hooks/use-report-step-complete.js";
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1081,6 +1081,12 @@ type LightboxBaseProps = {
|
|
|
1081
1081
|
readonly open: boolean;
|
|
1082
1082
|
readonly onOpenChange: (open: boolean) => void;
|
|
1083
1083
|
readonly testId?: string;
|
|
1084
|
+
/** Controls for the shown image, e.g. a download button. Rendered in the
|
|
1085
|
+
* top-left corner; with a set the app picks the image from its `index`. */
|
|
1086
|
+
readonly actions?: ReactNode;
|
|
1087
|
+
/** `false` hides the `{current} / {total}` counter of a set, for images
|
|
1088
|
+
* that carry their own. Default `true`. */
|
|
1089
|
+
readonly showPosition?: boolean;
|
|
1084
1090
|
};
|
|
1085
1091
|
/** Image lightbox — full-size preview on click. Web renders Radix overlay;
|
|
1086
1092
|
* trigger (thumbnail) and open state live in the app. Takes either one image
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.351.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.351.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.351.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.351.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.351.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|