@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1
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-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// useListUrlState — bündelt sort/dir/page/q einer entityList in einen
|
|
2
|
-
// per-screen-id namespaced URL-State. Mit Screen-ID-Prefix damit zwei
|
|
3
|
-
// Listen auf derselben Route (z.B. Dashboard mit Orders + Incidents)
|
|
4
|
-
// nicht über dieselben Query-Keys streiten.
|
|
5
|
-
//
|
|
6
|
-
// Param-Schema pro Liste:
|
|
7
|
-
// <screenId>.sort — field name (string)
|
|
8
|
-
// <screenId>.dir — "asc" | "desc"
|
|
9
|
-
// <screenId>.q — search term (URL-encoded)
|
|
10
|
-
// <screenId>.page — 1-based page number (nur bei pagination="pages")
|
|
11
|
-
// <screenId>.f.<field> — Faceted-Filter: comma-joined selected values
|
|
12
|
-
//
|
|
13
|
-
// Schreibt mit setSearchParams (replaceState — kein push), damit
|
|
14
|
-
// Sort/Filter-Toggles nicht die Browser-History fluten.
|
|
15
|
-
|
|
16
|
-
import { useCallback, useMemo } from "react";
|
|
17
|
-
import { useNav } from "../app/nav";
|
|
18
|
-
import type { DataTableSort, DataTableSortDir } from "../primitives";
|
|
19
|
-
|
|
20
|
-
// ListSort + DataTableSort hatten dieselbe Shape und drohten zu driften
|
|
21
|
-
// — aliased auf den primitives-Type (eine Quelle, kein Cast in RenderList).
|
|
22
|
-
export type ListSortDir = DataTableSortDir;
|
|
23
|
-
export type ListSort = DataTableSort;
|
|
24
|
-
|
|
25
|
-
export type ListUrlState = {
|
|
26
|
-
/** Aktive Sortierung (oder null = unsorted, Server liefert Default-Order). */
|
|
27
|
-
readonly sort: ListSort | null;
|
|
28
|
-
/** Search-Term (leer wenn nicht gesetzt). */
|
|
29
|
-
readonly q: string;
|
|
30
|
-
/** 1-basierte Page-Nummer. Bei pagination="infinite" oder false ist
|
|
31
|
-
* der Wert ignoriert; Caller liest ihn nur wenn relevant. */
|
|
32
|
-
readonly page: number;
|
|
33
|
-
/** User-chosen page size from `PAGE_SIZE_OPTIONS`; undefined = screen default. */
|
|
34
|
-
readonly pageSize: number | undefined;
|
|
35
|
-
/** Aktive Faceted-Filter: field → ausgewählte Werte (Strings; Caller
|
|
36
|
-
* coerced pro Field-Type). Leer wenn kein Filter gesetzt. */
|
|
37
|
-
readonly filters: Readonly<Record<string, readonly string[]>>;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type ListUrlStateApi = ListUrlState & {
|
|
41
|
-
/** Setzt sort + dir atomar. null = unsorted (löscht beide Keys). */
|
|
42
|
-
readonly setSort: (next: ListSort | null) => void;
|
|
43
|
-
/** Setzt den Search-Term. Empty-String löscht den Key. Caller debounced
|
|
44
|
-
* selber (z.B. useDebouncedCallback in der Search-Input-Komponente). */
|
|
45
|
-
readonly setQ: (next: string) => void;
|
|
46
|
-
/** Setzt die Page. 1 oder kleiner löscht den Key (Default-Page). */
|
|
47
|
-
readonly setPage: (next: number) => void;
|
|
48
|
-
/** Sets the page size and resets the page; undefined clears the key. */
|
|
49
|
-
readonly setPageSize: (next: number | undefined) => void;
|
|
50
|
-
/** Setzt die ausgewählten Werte eines Facet-Felds. Leeres Array löscht
|
|
51
|
-
* den Key. Resettet die Page (wie sort/search). */
|
|
52
|
-
readonly setFilter: (field: string, values: readonly string[]) => void;
|
|
53
|
-
/** Löscht alle aktiven Faceted-Filter (Reset-Button). */
|
|
54
|
-
readonly clearFilters: () => void;
|
|
55
|
-
/** Sets both bounds of a dateRange facet in ONE params update — two
|
|
56
|
-
* setFilter calls would overwrite each other, since the second still sees
|
|
57
|
-
* the old searchParams. An empty string clears that bound. Resets the page
|
|
58
|
-
* like setFilter. */
|
|
59
|
-
readonly setDateRange: (
|
|
60
|
-
field: string,
|
|
61
|
-
next: { readonly from: string; readonly to: string },
|
|
62
|
-
) => void;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// `.` als Trenner: lesbar (`?orders.sort=name`), kollidiert nicht mit
|
|
66
|
-
// üblichen Field-Namen (kebab- oder camelCase ohne Punkt). Boot-Validator
|
|
67
|
-
// pinnt screen.id ohne Punkt — siehe boot-validator entityList Section.
|
|
68
|
-
export const PAGE_SIZE_OPTIONS: readonly number[] = [25, 50, 100];
|
|
69
|
-
|
|
70
|
-
function parsePageSize(value: string | undefined): number | undefined {
|
|
71
|
-
if (value === undefined) return undefined;
|
|
72
|
-
const n = Number.parseInt(value, 10);
|
|
73
|
-
return PAGE_SIZE_OPTIONS.includes(n) ? n : undefined;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function key(screenId: string, suffix: string): string {
|
|
77
|
-
return `${screenId}.${suffix}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function parseDir(value: string | undefined): ListSortDir | undefined {
|
|
81
|
-
return value === "asc" || value === "desc" ? value : undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parsePage(value: string | undefined): number {
|
|
85
|
-
if (value === undefined) return 1;
|
|
86
|
-
const n = Number.parseInt(value, 10);
|
|
87
|
-
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function useListUrlState(screenId: string): ListUrlStateApi {
|
|
91
|
-
const nav = useNav();
|
|
92
|
-
const params = nav.searchParams;
|
|
93
|
-
|
|
94
|
-
const sort = useMemo<ListSort | null>(() => {
|
|
95
|
-
const field = params[key(screenId, "sort")];
|
|
96
|
-
const dir = parseDir(params[key(screenId, "dir")]);
|
|
97
|
-
if (field === undefined || field === "" || dir === undefined) return null;
|
|
98
|
-
return { field, dir };
|
|
99
|
-
}, [params, screenId]);
|
|
100
|
-
|
|
101
|
-
const q = params[key(screenId, "q")] ?? "";
|
|
102
|
-
const page = parsePage(params[key(screenId, "page")]);
|
|
103
|
-
const pageSize = parsePageSize(params[key(screenId, "size")]);
|
|
104
|
-
|
|
105
|
-
const filterPrefix = `${screenId}.f.`;
|
|
106
|
-
const filters = useMemo<Readonly<Record<string, readonly string[]>>>(() => {
|
|
107
|
-
const out: Record<string, readonly string[]> = {};
|
|
108
|
-
for (const [k, v] of Object.entries(params)) {
|
|
109
|
-
if (!k.startsWith(filterPrefix) || v === "") continue;
|
|
110
|
-
const field = k.slice(filterPrefix.length);
|
|
111
|
-
if (field === "") continue;
|
|
112
|
-
// ponytail: comma-join — Facet-Werte sind select-Options/booleans
|
|
113
|
-
// ohne Komma; upgrade auf repeated-keys falls je Komma-Werte nötig.
|
|
114
|
-
out[field] = v.split(",").filter((s) => s !== "");
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}, [params, filterPrefix]);
|
|
118
|
-
|
|
119
|
-
const setSort = useCallback(
|
|
120
|
-
(next: ListSort | null) => {
|
|
121
|
-
// Atomares Update: bei jedem Sort-Wechsel resetten wir auch die
|
|
122
|
-
// Page (sonst wäre der User auf "Seite 5 von alter Sortierung"
|
|
123
|
-
// hängen, was visuell verwirrend ist). Page-Reset gilt auch bei
|
|
124
|
-
// null — gleiche Logik.
|
|
125
|
-
nav.setSearchParams({
|
|
126
|
-
[key(screenId, "sort")]: next === null ? null : next.field,
|
|
127
|
-
[key(screenId, "dir")]: next === null ? null : next.dir,
|
|
128
|
-
[key(screenId, "page")]: null,
|
|
129
|
-
});
|
|
130
|
-
},
|
|
131
|
-
[nav, screenId],
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const setQ = useCallback(
|
|
135
|
-
(next: string) => {
|
|
136
|
-
// Search-Change resettet Page (gleicher Grund wie Sort) UND
|
|
137
|
-
// Sort? Nein — User kann mit aktivem Sort suchen wollen, das
|
|
138
|
-
// soll die Sortierung nicht zerlegen. Nur Page wird gereset.
|
|
139
|
-
nav.setSearchParams({
|
|
140
|
-
[key(screenId, "q")]: next === "" ? null : next,
|
|
141
|
-
[key(screenId, "page")]: null,
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
[nav, screenId],
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const setPage = useCallback(
|
|
148
|
-
(next: number) => {
|
|
149
|
-
nav.setSearchParams({
|
|
150
|
-
[key(screenId, "page")]: next <= 1 ? null : String(next),
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
[nav, screenId],
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const setPageSize = useCallback(
|
|
157
|
-
(next: number | undefined) => {
|
|
158
|
-
nav.setSearchParams({
|
|
159
|
-
[key(screenId, "size")]: next === undefined ? null : String(next),
|
|
160
|
-
[key(screenId, "page")]: null,
|
|
161
|
-
});
|
|
162
|
-
},
|
|
163
|
-
[nav, screenId],
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const setFilter = useCallback(
|
|
167
|
-
(field: string, values: readonly string[]) => {
|
|
168
|
-
nav.setSearchParams({
|
|
169
|
-
[key(screenId, `f.${field}`)]: values.length === 0 ? null : values.join(","),
|
|
170
|
-
[key(screenId, "page")]: null,
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
[nav, screenId],
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const setDateRange = useCallback(
|
|
177
|
-
(field: string, next: { readonly from: string; readonly to: string }) => {
|
|
178
|
-
nav.setSearchParams({
|
|
179
|
-
[key(screenId, `f.${field}.from`)]: next.from === "" ? null : next.from,
|
|
180
|
-
[key(screenId, `f.${field}.to`)]: next.to === "" ? null : next.to,
|
|
181
|
-
[key(screenId, "page")]: null,
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
[nav, screenId],
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const clearFilters = useCallback(() => {
|
|
188
|
-
const updates: Record<string, string | null> = { [key(screenId, "page")]: null };
|
|
189
|
-
for (const field of Object.keys(filters)) updates[key(screenId, `f.${field}`)] = null;
|
|
190
|
-
nav.setSearchParams(updates);
|
|
191
|
-
}, [nav, screenId, filters]);
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
sort,
|
|
195
|
-
q,
|
|
196
|
-
page,
|
|
197
|
-
pageSize,
|
|
198
|
-
filters,
|
|
199
|
-
setSort,
|
|
200
|
-
setQ,
|
|
201
|
-
setPage,
|
|
202
|
-
setPageSize,
|
|
203
|
-
setFilter,
|
|
204
|
-
clearFilters,
|
|
205
|
-
setDateRange,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { DispatcherError, WriteResult } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import { useCallback, useRef, useState } from "react";
|
|
3
|
-
import { useDispatcher } from "../context/dispatcher-context";
|
|
4
|
-
|
|
5
|
-
// React wrapper around dispatcher.write — the write-side sibling of
|
|
6
|
-
// useQuery. One hook instance per handler-type; `mutate` carries the
|
|
7
|
-
// payload so a single instance serves list-row actions with varying
|
|
8
|
-
// payloads.
|
|
9
|
-
//
|
|
10
|
-
// `mutate` resolves with the raw WriteResult so callers can branch
|
|
11
|
-
// (navigate on success, keep the form open on failure) without waiting
|
|
12
|
-
// for a re-render of `error`/`data`.
|
|
13
|
-
|
|
14
|
-
export type UseMutationResult<TData> = {
|
|
15
|
-
readonly mutate: (payload: unknown) => Promise<WriteResult<TData>>;
|
|
16
|
-
readonly pending: boolean;
|
|
17
|
-
readonly error: DispatcherError | null;
|
|
18
|
-
readonly data: TData | null;
|
|
19
|
-
readonly reset: () => void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export function useMutation<TData = unknown>(type: string): UseMutationResult<TData> {
|
|
23
|
-
const dispatcher = useDispatcher();
|
|
24
|
-
const [pending, setPending] = useState(false);
|
|
25
|
-
const [error, setError] = useState<DispatcherError | null>(null);
|
|
26
|
-
const [data, setData] = useState<TData | null>(null);
|
|
27
|
-
// Sequence guard: `mutate` has no abort (unlike useQuery's GETs, a write
|
|
28
|
-
// already landed server-side and can't be cancelled). Two overlapping
|
|
29
|
-
// calls on one instance (e.g. two list-row actions) must not let the
|
|
30
|
-
// first-to-resolve clobber pending/error/data set by the second — only
|
|
31
|
-
// the most recently STARTED call's outcome may update shared state.
|
|
32
|
-
const sequence = useRef(0);
|
|
33
|
-
|
|
34
|
-
const mutate = useCallback(
|
|
35
|
-
async (payload: unknown): Promise<WriteResult<TData>> => {
|
|
36
|
-
const callSeq = ++sequence.current;
|
|
37
|
-
setPending(true);
|
|
38
|
-
setError(null);
|
|
39
|
-
const result = await dispatcher.write<TData>(type, payload);
|
|
40
|
-
if (callSeq === sequence.current) {
|
|
41
|
-
if (result.isSuccess) {
|
|
42
|
-
setData(result.data);
|
|
43
|
-
} else {
|
|
44
|
-
setError(result.error);
|
|
45
|
-
}
|
|
46
|
-
setPending(false);
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
},
|
|
50
|
-
[dispatcher, type],
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const reset = useCallback(() => {
|
|
54
|
-
sequence.current++; // invalidate any in-flight mutate's late update
|
|
55
|
-
setPending(false);
|
|
56
|
-
setError(null);
|
|
57
|
-
setData(null);
|
|
58
|
-
}, []);
|
|
59
|
-
|
|
60
|
-
return { mutate, pending, error, data, reset };
|
|
61
|
-
}
|
package/src/hooks/use-query.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { useDispatcher } from "../context/dispatcher-context";
|
|
4
|
-
import { useLiveEvents } from "../sse/live-events";
|
|
5
|
-
|
|
6
|
-
// React wrapper around dispatcher.query. Fires on mount, re-fires
|
|
7
|
-
// whenever `type` or the serialized payload change, and exposes a
|
|
8
|
-
// `refetch` imperative hook for after-mutation reloads.
|
|
9
|
-
//
|
|
10
|
-
// Cancellation: each fetch owns an AbortController; a newer fetch
|
|
11
|
-
// (change in deps or manual refetch) aborts the older one's network
|
|
12
|
-
// call before firing, so a slow request can't overwrite a fresh one.
|
|
13
|
-
// Unmount aborts any in-flight call too — React will throw the
|
|
14
|
-
// "state on unmounted" warning if we set state after unmount, and
|
|
15
|
-
// aborting pre-emptively makes that path a no-op.
|
|
16
|
-
//
|
|
17
|
-
// Result shape modeled on the dispatcher's own QueryResult: a
|
|
18
|
-
// discriminated state so callers can branch on `loading | data | error`
|
|
19
|
-
// without undefined-juggling. The first render is `loading: true,
|
|
20
|
-
// data: null, error: null` (no optimistic "online" cache — add that
|
|
21
|
-
// later if needed).
|
|
22
|
-
|
|
23
|
-
export type UseQueryResult<TData> = {
|
|
24
|
-
readonly data: TData | null;
|
|
25
|
-
readonly error: DispatcherError | null;
|
|
26
|
-
readonly loading: boolean;
|
|
27
|
-
readonly refetch: () => Promise<void>;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type UseQueryOptions = {
|
|
31
|
-
// When false, skips the automatic fetch-on-mount and re-fetch on
|
|
32
|
-
// dep-change. Useful for lazy queries that only run after a user
|
|
33
|
-
// action. `refetch()` still works as an imperative trigger.
|
|
34
|
-
readonly enabled?: boolean;
|
|
35
|
-
// When true, subscribe to SSE events for the entity this query
|
|
36
|
-
// targets and refetch on any create/update/delete/restore event.
|
|
37
|
-
// The entity-name is parsed from the query type using Kumiko's
|
|
38
|
-
// `<feature>:query:<entity>:<verb>` convention — if the type
|
|
39
|
-
// doesn't follow that shape, live-mode is a no-op.
|
|
40
|
-
readonly live?: boolean;
|
|
41
|
-
// Re-runs the query every N ms while enabled. Ticks are silent (no
|
|
42
|
-
// `loading` flip) and skipped while a fetch is still in flight. Only
|
|
43
|
-
// timers are used, so it behaves the same on web and React Native.
|
|
44
|
-
readonly refetchIntervalMs?: number;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Extract the entity-name from a standard Kumiko query type. Returns
|
|
48
|
-
// undefined for non-conforming types so the live-mode silently skips
|
|
49
|
-
// them instead of subscribing to a channel no event will ever match.
|
|
50
|
-
export function entityFromQueryType(type: string): string | undefined {
|
|
51
|
-
// Expected shape: "<feature>:query:<entity>:<verb>"
|
|
52
|
-
const parts = type.split(":");
|
|
53
|
-
if (parts.length !== 4) return undefined;
|
|
54
|
-
if (parts[1] !== "query") return undefined;
|
|
55
|
-
return parts[2];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function useQuery<TData = unknown>(
|
|
59
|
-
type: string,
|
|
60
|
-
payload: unknown,
|
|
61
|
-
options: UseQueryOptions = {},
|
|
62
|
-
): UseQueryResult<TData> {
|
|
63
|
-
const dispatcher = useDispatcher();
|
|
64
|
-
const { enabled = true, live = false, refetchIntervalMs } = options;
|
|
65
|
-
|
|
66
|
-
const [data, setData] = useState<TData | null>(null);
|
|
67
|
-
const [error, setError] = useState<DispatcherError | null>(null);
|
|
68
|
-
const [loading, setLoading] = useState<boolean>(enabled);
|
|
69
|
-
|
|
70
|
-
// Track the AbortController for the most recent fetch so a newer
|
|
71
|
-
// call can cancel the older one. Ref, not state, because the
|
|
72
|
-
// controller identity shouldn't trigger a re-render.
|
|
73
|
-
const activeCtrl = useRef<AbortController | null>(null);
|
|
74
|
-
// A tick that aborted a running fetch would starve slow queries, so
|
|
75
|
-
// ticks consult this flag instead.
|
|
76
|
-
const fetchInFlight = useRef(false);
|
|
77
|
-
|
|
78
|
-
// Serialize payload so object-identity-changes across renders (the
|
|
79
|
-
// caller building a fresh `{}` every render) don't loop. Strings are
|
|
80
|
-
// stable by reference in React's dep-array check.
|
|
81
|
-
const payloadKey = JSON.stringify(payload);
|
|
82
|
-
|
|
83
|
-
// `dispatcher` is stable (context identity); `type` + `payloadKey`
|
|
84
|
-
// are the meaningful re-run triggers. `payload` itself is intentionally
|
|
85
|
-
// not in deps — it's serialized into payloadKey above.
|
|
86
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
|
|
87
|
-
const runFetch = useCallback(
|
|
88
|
-
async (background: boolean): Promise<void> => {
|
|
89
|
-
// Abort whatever's in flight. observer on Safari 17+ handles this
|
|
90
|
-
// without raising a fetch-throw in the previous caller — they
|
|
91
|
-
// already set the error path.
|
|
92
|
-
activeCtrl.current?.abort();
|
|
93
|
-
const ctrl = new AbortController();
|
|
94
|
-
activeCtrl.current = ctrl;
|
|
95
|
-
|
|
96
|
-
fetchInFlight.current = true;
|
|
97
|
-
if (!background) setLoading(true);
|
|
98
|
-
let result: Awaited<ReturnType<typeof dispatcher.query<TData>>>;
|
|
99
|
-
try {
|
|
100
|
-
result = await dispatcher.query<TData>(type, payload, { signal: ctrl.signal });
|
|
101
|
-
} finally {
|
|
102
|
-
// A rejected fetch must not leave polling stuck; a superseded one must
|
|
103
|
-
// not clear the newer fetch's flag.
|
|
104
|
-
if (activeCtrl.current === ctrl) fetchInFlight.current = false;
|
|
105
|
-
}
|
|
106
|
-
// skip: a newer fetch already superseded this one, don't clobber its state
|
|
107
|
-
if (ctrl.signal.aborted) return;
|
|
108
|
-
if (result.isSuccess) {
|
|
109
|
-
setData(result.data);
|
|
110
|
-
setError(null);
|
|
111
|
-
} else {
|
|
112
|
-
// A cancelled request comes back with code "aborted" from the
|
|
113
|
-
// skip: aborted request, a newer run already replaces this result
|
|
114
|
-
if (result.error.code === "aborted") return;
|
|
115
|
-
setError(result.error);
|
|
116
|
-
}
|
|
117
|
-
setLoading(false);
|
|
118
|
-
},
|
|
119
|
-
[dispatcher, type, payloadKey],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const run = useCallback((): Promise<void> => runFetch(false), [runFetch]);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (!enabled) {
|
|
126
|
-
setLoading(false);
|
|
127
|
-
// skip: query disabled, loading state already cleared above
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
void run();
|
|
131
|
-
return () => {
|
|
132
|
-
activeCtrl.current?.abort();
|
|
133
|
-
fetchInFlight.current = false;
|
|
134
|
-
};
|
|
135
|
-
}, [enabled, run]);
|
|
136
|
-
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
// skip: polling off or disabled query
|
|
139
|
-
if (!enabled || refetchIntervalMs === undefined) return;
|
|
140
|
-
// skip: non-positive or non-finite interval disables polling
|
|
141
|
-
if (!Number.isFinite(refetchIntervalMs) || refetchIntervalMs <= 0) return;
|
|
142
|
-
const timer = setInterval(() => {
|
|
143
|
-
// skip: a fetch is still running, the next tick retries
|
|
144
|
-
if (fetchInFlight.current) return;
|
|
145
|
-
void runFetch(true);
|
|
146
|
-
}, refetchIntervalMs);
|
|
147
|
-
return () => clearInterval(timer);
|
|
148
|
-
}, [enabled, refetchIntervalMs, runFetch]);
|
|
149
|
-
|
|
150
|
-
// Live-mode: auf SSE-Events für die Query-Entity hören und refetchen.
|
|
151
|
-
// Separater Effect, damit eine Änderung an `live` oder `type` das
|
|
152
|
-
// Subscription-Lifecycle genau einmal durchwalzt.
|
|
153
|
-
const subscribeLive = useLiveEvents();
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
// skip: live mode or query disabled, no SSE subscription needed
|
|
156
|
-
if (!live || !enabled) return;
|
|
157
|
-
const entity = entityFromQueryType(type);
|
|
158
|
-
// skip: query type has no mapped entity, nothing to subscribe to
|
|
159
|
-
if (entity === undefined) return;
|
|
160
|
-
return subscribeLive(entity, () => {
|
|
161
|
-
void run();
|
|
162
|
-
});
|
|
163
|
-
}, [live, enabled, type, run, subscribeLive]);
|
|
164
|
-
|
|
165
|
-
return { data, error, loading, refetch: run };
|
|
166
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Tier 2.7e-4: Renderer-Side Eagerload für Reference-Felder.
|
|
2
|
-
//
|
|
3
|
-
// Pro Reference-Spalte einer Liste wird einmal `<feature>:query:
|
|
4
|
-
// <refEntity>:list` (limit:200) gerufen, eine Map<uuid, displayValue>
|
|
5
|
-
// gebaut, und der Renderer nutzt sie als Cell-Display-Renderer.
|
|
6
|
-
//
|
|
7
|
-
// Strategy-Trade-offs:
|
|
8
|
-
// - Server-side Drizzle-Joins wären effizienter (eine Query statt
|
|
9
|
-
// N+1), würden aber die executor-API um lookupTables erweitern
|
|
10
|
-
// müssen. Für das MVP geht der Renderer-Side-Pfad vor.
|
|
11
|
-
// - Limit:200 ist eine harte UX-Grenze: bei mehr Entries in der
|
|
12
|
-
// referenced Entity zeigen die letzten Rows nur noch UUIDs.
|
|
13
|
-
// Searchable-Combobox (Tier 2.1c) + Server-Eagerload mit
|
|
14
|
-
// ID-Whitelist heben das später auf.
|
|
15
|
-
//
|
|
16
|
-
// Dieser Hook ist call-stable: Lookup-Map wird durch useQuery's
|
|
17
|
-
// internen Cache shared zwischen List + Edit-Form für die gleiche
|
|
18
|
-
// Entity, Live-Updates kommen via SSE (use-query-live).
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
REFERENCE_LOOKUP_SOURCES,
|
|
22
|
-
SYSTEM_REFERENCE_LABELS,
|
|
23
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
24
|
-
import { useMemo } from "react";
|
|
25
|
-
import { toKebab } from "../app/qn";
|
|
26
|
-
import { useTranslation } from "../i18n";
|
|
27
|
-
import { REFERENCE_LIST_LOOKUP_LIMIT } from "./reference-limits";
|
|
28
|
-
import { useQuery } from "./use-query";
|
|
29
|
-
|
|
30
|
-
export type ReferenceLookupMap = ReadonlyMap<string, string>;
|
|
31
|
-
|
|
32
|
-
/** Where a reference target's label rows come from: the central override for
|
|
33
|
-
* that `feature:entity` if one exists, else the entity-convention list
|
|
34
|
-
* handler plus the caller's own label field. */
|
|
35
|
-
export function referenceLookupSource(
|
|
36
|
-
refFeature: string,
|
|
37
|
-
refEntity: string,
|
|
38
|
-
labelField: string,
|
|
39
|
-
): { readonly queryQn: string; readonly labelKey: string } {
|
|
40
|
-
const override = REFERENCE_LOOKUP_SOURCES[`${refFeature}:${refEntity}`];
|
|
41
|
-
if (override !== undefined) return override;
|
|
42
|
-
return {
|
|
43
|
-
queryQn: `${toKebab(refFeature)}:query:${toKebab(refEntity)}:list`,
|
|
44
|
-
labelKey: labelField,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Empty while the query loads (or fails), so callers fall back to the raw id.
|
|
49
|
-
* `featureName` is the reference's target feature, not the screen's. */
|
|
50
|
-
export function useReferenceLookup(
|
|
51
|
-
featureName: string,
|
|
52
|
-
refEntity: string,
|
|
53
|
-
labelField: string,
|
|
54
|
-
): { readonly map: ReferenceLookupMap; readonly loading: boolean } {
|
|
55
|
-
const { queryQn, labelKey } = referenceLookupSource(featureName, refEntity, labelField);
|
|
56
|
-
const result = useQuery<{ rows: ReadonlyArray<Record<string, unknown>> }>(queryQn, {
|
|
57
|
-
limit: REFERENCE_LIST_LOOKUP_LIMIT,
|
|
58
|
-
});
|
|
59
|
-
const translate = useTranslation();
|
|
60
|
-
const systemLabel = SYSTEM_REFERENCE_LABELS[`${featureName}:${refEntity}`];
|
|
61
|
-
const map = useMemo(() => {
|
|
62
|
-
const out = new Map<string, string>();
|
|
63
|
-
for (const row of result.data?.rows ?? []) {
|
|
64
|
-
const id = row["id"];
|
|
65
|
-
if (id === undefined || id === null) continue;
|
|
66
|
-
const idStr = String(id);
|
|
67
|
-
const label = row[labelKey] ?? id;
|
|
68
|
-
out.set(idStr, String(label));
|
|
69
|
-
}
|
|
70
|
-
// System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row, so
|
|
71
|
-
// the bulk lookup above never covers them — inject the label directly.
|
|
72
|
-
if (systemLabel !== undefined) out.set(systemLabel.id, translate(systemLabel.labelKey));
|
|
73
|
-
return out;
|
|
74
|
-
}, [result.data, labelKey, systemLabel, translate]);
|
|
75
|
-
return { map, loading: result.loading };
|
|
76
|
-
}
|
package/src/hooks/use-store.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { Store } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
3
|
-
|
|
4
|
-
// React-Bindings für Kumikos Store-Primitive (`createStore` aus
|
|
5
|
-
// @cosmicdrift/kumiko-headless). Beide Hooks sind dünne Wrapper um React's
|
|
6
|
-
// useSyncExternalStore — die ganze Subscribe/Notify-Mechanik lebt im
|
|
7
|
-
// Store selbst, hier wird nur React verdrahtet.
|
|
8
|
-
//
|
|
9
|
-
// useStore: gibt den ganzen Snapshot zurück. Re-rendert wenn der
|
|
10
|
-
// Store seine Identität wechselt. Object.is-Gate im Store selbst
|
|
11
|
-
// verhindert no-op-Updates.
|
|
12
|
-
//
|
|
13
|
-
// useStoreSelector: gibt eine abgeleitete Sicht zurück. Selectors die
|
|
14
|
-
// Object-/Array-Literale zurückgeben (z.B. `s => ({ a, b })`) erzeugen
|
|
15
|
-
// pro Render eine neue Identität — useSyncExternalStore würde das als
|
|
16
|
-
// "Change" lesen und in eine Re-Render-Schleife laufen. Der dritte
|
|
17
|
-
// Arg `equals` dient genau dafür: gleiche Auswahl → cached
|
|
18
|
-
// Identität zurück, kein Re-Render.
|
|
19
|
-
|
|
20
|
-
// @wrapper-known semantic-alias
|
|
21
|
-
export function useStore<T>(store: Store<T>): T {
|
|
22
|
-
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function useStoreSelector<T, S>(
|
|
26
|
-
store: Store<T>,
|
|
27
|
-
select: (snapshot: T) => S,
|
|
28
|
-
equals: (a: S, b: S) => boolean = Object.is,
|
|
29
|
-
): S {
|
|
30
|
-
// Cache der letzten Auswahl. Beim nächsten getSnapshot-Call vergleicht
|
|
31
|
-
// der Wrapper die neu berechnete Auswahl mit dem Cache und gibt — falls
|
|
32
|
-
// gleich — die cached Identität zurück. Das ist der Trick, der
|
|
33
|
-
// Selector-Returns wie `s => ({ a, b })` stabil hält.
|
|
34
|
-
const lastSelected = useRef<S>(undefined as S);
|
|
35
|
-
const initialized = useRef(false);
|
|
36
|
-
|
|
37
|
-
const getSelectedSnapshot = (): S => {
|
|
38
|
-
const next = select(store.getSnapshot());
|
|
39
|
-
if (initialized.current && equals(lastSelected.current, next)) {
|
|
40
|
-
return lastSelected.current;
|
|
41
|
-
}
|
|
42
|
-
lastSelected.current = next;
|
|
43
|
-
initialized.current = true;
|
|
44
|
-
return next;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return useSyncExternalStore(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
48
|
-
}
|