@cosmicdrift/kumiko-renderer 0.333.0 → 0.335.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/config-edit-shim.d.ts +7 -0
- package/dist/app/config-edit-shim.js +39 -0
- package/dist/app/content-preview.js +7 -9
- package/dist/app/extension-sections.d.ts +6 -0
- package/dist/app/form-schema.d.ts +2 -1
- package/dist/app/form-schema.js +11 -2
- package/dist/app/kumiko-screen.d.ts +5 -0
- package/dist/app/kumiko-screen.js +259 -101
- package/dist/app/nav.d.ts +5 -0
- package/dist/app/nav.js +17 -4
- package/dist/app/query-state-banners.d.ts +8 -0
- package/dist/app/query-state-banners.js +14 -0
- package/dist/app/reference-facet-bridge.js +8 -1
- package/dist/app/row-actions.d.ts +12 -0
- package/dist/app/row-actions.js +24 -7
- package/dist/app/screen-access.js +13 -9
- package/dist/app/secret-mint-body.js +41 -7
- package/dist/app/secrets-edit-body.js +89 -33
- package/dist/components/embedded-list-field.js +2 -4
- package/dist/components/grid-cell-for-field.d.ts +7 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/multi-select-checkboxes.js +4 -1
- package/dist/components/query-options-select.d.ts +12 -3
- package/dist/components/query-options-select.js +45 -2
- package/dist/components/reference-create-dialog.js +9 -4
- package/dist/components/related-list-section.js +8 -10
- package/dist/components/render-edit-action-button.d.ts +1 -1
- package/dist/components/render-edit-action-button.js +5 -1
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +8 -5
- package/dist/components/render-edit-types.d.ts +30 -1
- package/dist/components/render-edit.js +248 -76
- package/dist/components/render-field.d.ts +7 -1
- package/dist/components/render-field.js +40 -15
- package/dist/components/render-list.js +6 -6
- package/dist/components/write-form-section.d.ts +5 -1
- package/dist/components/write-form-section.js +29 -25
- package/dist/context/embedded-screen-context.d.ts +5 -0
- package/dist/context/embedded-screen-context.js +12 -0
- package/dist/hooks/use-reference-lookup.d.ts +7 -0
- package/dist/hooks/use-reference-lookup.js +10 -0
- package/dist/i18n-defaults.js +30 -5
- package/dist/i18n.js +8 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +70 -19
- package/dist/session/session-ended.d.ts +16 -0
- package/dist/session/session-ended.js +73 -0
- package/dist/sort-by-accessor.d.ts +6 -2
- package/dist/sort-by-accessor.js +13 -3
- package/package.json +5 -5
- package/src/changes.json +40 -0
package/dist/app/nav.js
CHANGED
|
@@ -9,8 +9,21 @@ import { lastSegment } from "./qn.js";
|
|
|
9
9
|
// Alles nach den ersten 2/3 Segmenten wird ignoriert — kumikos
|
|
10
10
|
// Navigation-Grammatik nested nicht weiter. Nested-Routes wäre eine
|
|
11
11
|
// Spec-Änderung, nicht ein URL-Shape-Unfall.
|
|
12
|
+
function decodeSegments(segments) {
|
|
13
|
+
try {
|
|
14
|
+
return segments.map((segment) => decodeURIComponent(segment));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Malformed percent-escape ("%zz") — treat the URL as no route.
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
export function parsePath(pathname, hasWorkspaces) {
|
|
13
|
-
const
|
|
22
|
+
const rawParts = pathname.split("/").filter((p) => p !== "");
|
|
23
|
+
// Only the segments the grammar uses are decoded; ignored trailing ones may be junk.
|
|
24
|
+
const parts = decodeSegments(rawParts.slice(0, hasWorkspaces === true ? 3 : 2));
|
|
25
|
+
if (parts === undefined)
|
|
26
|
+
return undefined;
|
|
14
27
|
if (hasWorkspaces === true) {
|
|
15
28
|
const [workspaceId, screenId, entityId] = parts;
|
|
16
29
|
if (workspaceId === undefined || workspaceId === "")
|
|
@@ -41,17 +54,17 @@ export function formatPath(target) {
|
|
|
41
54
|
segments.push(target.screenId);
|
|
42
55
|
if (target.entityId !== undefined)
|
|
43
56
|
segments.push(target.entityId);
|
|
44
|
-
return `/${segments.join("/")}`;
|
|
57
|
+
return `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
45
58
|
}
|
|
46
59
|
// Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
|
|
47
60
|
// branch and hasDetailScreen (create-app's row-click default, fw#2164) both
|
|
48
61
|
// need "is there a detail screen for this entity", one to resolve it, the
|
|
49
62
|
// other to just check before falling back to a different default.
|
|
50
|
-
function findDetailForScreen(features, entity) {
|
|
63
|
+
export function findDetailForScreen(features, entity) {
|
|
51
64
|
for (const feature of features) {
|
|
52
65
|
for (const screen of feature.screens) {
|
|
53
66
|
if (screen.detailFor === entity)
|
|
54
|
-
return { featureName: feature.featureName, screenId: screen.id };
|
|
67
|
+
return { featureName: feature.featureName, screenId: screen.id, screen };
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
return undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DispatcherError, Translate } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare function QueryLoadingBanner(): ReactNode;
|
|
4
|
+
export declare function QueryErrorBanner({ error, translate, onRetry, }: {
|
|
5
|
+
readonly error: DispatcherError;
|
|
6
|
+
readonly translate: Translate;
|
|
7
|
+
readonly onRetry: () => void | Promise<void>;
|
|
8
|
+
}): ReactNode;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "../i18n.js";
|
|
3
|
+
import { usePrimitives } from "../primitives.js";
|
|
4
|
+
import { dispatcherErrorText } from "./write-failed-error.js";
|
|
5
|
+
export function QueryLoadingBanner() {
|
|
6
|
+
const { Banner } = usePrimitives();
|
|
7
|
+
const t = useTranslation();
|
|
8
|
+
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: t("kumiko.widget.loading") }));
|
|
9
|
+
}
|
|
10
|
+
export function QueryErrorBanner({ error, translate, onRetry, }) {
|
|
11
|
+
const { Banner, Button } = usePrimitives();
|
|
12
|
+
const t = useTranslation();
|
|
13
|
+
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-error", actions: _jsx(Button, { variant: "secondary", size: "sm", onClick: onRetry, testId: "kumiko-screen-retry", children: t("kumiko.actions.retry") }), children: dispatcherErrorText(error, translate) }));
|
|
14
|
+
}
|
|
@@ -2,14 +2,21 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
// One bridge component per reference facet, mirroring ReferenceLookupBridge —
|
|
3
3
|
// facet count varies per screen, so the hook can't be called in a loop.
|
|
4
4
|
import { useEffect } from "react";
|
|
5
|
+
import { REFERENCE_LIST_LOOKUP_LIMIT } from "../hooks/reference-limits.js";
|
|
5
6
|
import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
|
|
6
7
|
function ReferenceFacetOptionsBridge({ field, refEntity, refFeature, labelField, onOptions, }) {
|
|
7
8
|
const lookup = useReferenceLookup(refFeature, refEntity, labelField);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
if (lookup.loading)
|
|
10
11
|
return;
|
|
12
|
+
// The lookup is one capped list fetch; a full page means the facet may be
|
|
13
|
+
// missing values the user wants to filter by.
|
|
14
|
+
if (lookup.map.size >= REFERENCE_LIST_LOOKUP_LIMIT) {
|
|
15
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning, facet options are silently truncated
|
|
16
|
+
console.warn(`[kumiko] reference facet "${field}" shows only the first ${REFERENCE_LIST_LOOKUP_LIMIT} ${refEntity} rows; further values cannot be filtered by`);
|
|
17
|
+
}
|
|
11
18
|
onOptions(field, [...lookup.map.entries()].map(([value, label]) => ({ value, label })));
|
|
12
|
-
}, [lookup.loading, lookup.map, field, onOptions]);
|
|
19
|
+
}, [lookup.loading, lookup.map, field, refEntity, onOptions]);
|
|
13
20
|
return null;
|
|
14
21
|
}
|
|
15
22
|
// One invisible bridge per "reference" facet spec, each publishing its
|
|
@@ -15,6 +15,7 @@ export declare function runProjectionRowNavigate(nav: NavApi, action: RowActionN
|
|
|
15
15
|
type OpenDrawer = (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
16
16
|
/** @internal test-only */
|
|
17
17
|
export declare function resetDrawerActionWarningsForTests(): void;
|
|
18
|
+
export declare function buildDrawerRowAction(action: RowActionDrawer, translate: Translate, openDrawer: OpenDrawer): DataTableRowAction;
|
|
18
19
|
export declare function buildProjectionRowActions(options: {
|
|
19
20
|
readonly rowActions: readonly RowAction[] | undefined;
|
|
20
21
|
readonly translate: Translate;
|
|
@@ -52,6 +53,8 @@ export declare function buildProjectionToolbarActions(options: {
|
|
|
52
53
|
readonly record?: Readonly<Record<string, unknown>>;
|
|
53
54
|
readonly host: ReturnHost | undefined;
|
|
54
55
|
}): readonly ToolbarActionButton[] | undefined;
|
|
56
|
+
export declare function deletesShownRecord(action: RowActionWriteHandler, payload: Readonly<Record<string, unknown>>, shownRecordId: unknown): boolean;
|
|
57
|
+
export type RecordLeftHandler = (action: RowActionWriteHandler, resultData: unknown, deletedShownRecord: boolean) => void;
|
|
55
58
|
export declare function buildRecordActions(options: {
|
|
56
59
|
readonly actions: readonly RowAction[];
|
|
57
60
|
readonly record: Readonly<Record<string, unknown>>;
|
|
@@ -62,6 +65,11 @@ export declare function buildRecordActions(options: {
|
|
|
62
65
|
readonly openDrawer: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
63
66
|
/** Called after a writeHandler action succeeds (refetch/reload). */
|
|
64
67
|
readonly onWriteSuccess: () => void | Promise<void>;
|
|
68
|
+
/** Called INSTEAD of `onWriteSuccess` when a writeHandler action succeeds
|
|
69
|
+
* and leaves the shown record (`redirect` set, or it deletes the record).
|
|
70
|
+
* Callers whose actions run on the shown record pass it; callers whose
|
|
71
|
+
* records are list rows omit it. */
|
|
72
|
+
readonly onRecordLeft?: RecordLeftHandler;
|
|
65
73
|
/** Screen-target navigate default entityId when the action declares no
|
|
66
74
|
* explicit `entityId` field-source. A caller editing a fixed entity can
|
|
67
75
|
* pass its own `entityId` here directly (the record it already is). A
|
|
@@ -73,5 +81,9 @@ export declare function buildRecordActions(options: {
|
|
|
73
81
|
* belongs to (cross-feature lookup) — undefined otherwise. Omitted by
|
|
74
82
|
* callers with no such cross-screen lookup. */
|
|
75
83
|
readonly sameEntityScreenId?: (targetScreen: string) => string | undefined;
|
|
84
|
+
/** Default `{ id }` for a writeHandler action declaring no `payload`. A
|
|
85
|
+
* caller editing a fixed entity passes its route id so the payload does
|
|
86
|
+
* not depend on the loaded record carrying an `id` field. */
|
|
87
|
+
readonly defaultWritePayloadId?: string;
|
|
76
88
|
}): readonly RenderEditAction[] | undefined;
|
|
77
89
|
export {};
|
package/dist/app/row-actions.js
CHANGED
|
@@ -119,7 +119,7 @@ function warnDrawerActionDropped(actionKind, actionId) {
|
|
|
119
119
|
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
120
120
|
console.warn(`[kumiko] ${actionKind} "${actionId}" is kind:"drawer", but the host did not wire openDrawer (RelatedListSection: pass onOpenDrawer) — it will not render.`);
|
|
121
121
|
}
|
|
122
|
-
function buildDrawerRowAction(action, translate, openDrawer) {
|
|
122
|
+
export function buildDrawerRowAction(action, translate, openDrawer) {
|
|
123
123
|
const { visible, params } = action;
|
|
124
124
|
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
125
125
|
return {
|
|
@@ -269,9 +269,9 @@ export function buildProjectionToolbarActions(options) {
|
|
|
269
269
|
return undefined;
|
|
270
270
|
const out = [];
|
|
271
271
|
for (const action of toolbarActions) {
|
|
272
|
+
// Fail closed: a conditional action without a record to evaluate against stays hidden.
|
|
272
273
|
if (action.visible !== undefined &&
|
|
273
|
-
record
|
|
274
|
-
!evalFieldCondition(action.visible, record)) {
|
|
274
|
+
(record === undefined || !evalFieldCondition(action.visible, record))) {
|
|
275
275
|
continue;
|
|
276
276
|
}
|
|
277
277
|
if (action.kind === "navigate") {
|
|
@@ -356,8 +356,17 @@ function buildDrawerRecordAction(action, options) {
|
|
|
356
356
|
},
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
|
+
// Entity delete handler QN: `<feature>:write:<entity>:delete` (see
|
|
360
|
+
// entityWriteCommand in kumiko-screen.tsx).
|
|
361
|
+
const ENTITY_DELETE_HANDLER_PATTERN = /:write:[^:]+:delete$/;
|
|
362
|
+
export function deletesShownRecord(action, payload, shownRecordId) {
|
|
363
|
+
return (ENTITY_DELETE_HANDLER_PATTERN.test(action.handler) &&
|
|
364
|
+
payload["id"] !== undefined &&
|
|
365
|
+
payload["id"] === shownRecordId);
|
|
366
|
+
}
|
|
359
367
|
function buildWriteHandlerRecordAction(action, options) {
|
|
360
|
-
const { record, translate, actionIcon, dispatcher, onWriteSuccess } = options;
|
|
368
|
+
const { record, translate, actionIcon, dispatcher, onWriteSuccess, defaultWritePayloadId, onRecordLeft, } = options;
|
|
369
|
+
const shownRecordId = defaultWritePayloadId ?? record["id"];
|
|
361
370
|
return {
|
|
362
371
|
id: action.id,
|
|
363
372
|
label: translate(action.label),
|
|
@@ -371,12 +380,18 @@ function buildWriteHandlerRecordAction(action, options) {
|
|
|
371
380
|
onPress: async () => {
|
|
372
381
|
const payload = action.payload !== undefined
|
|
373
382
|
? evalRowExtractor(action.payload, record)
|
|
374
|
-
: { id:
|
|
383
|
+
: { id: shownRecordId };
|
|
375
384
|
const result = await dispatcher.write(action.handler, payload);
|
|
376
385
|
if (!result.isSuccess) {
|
|
377
386
|
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
378
387
|
}
|
|
379
|
-
|
|
388
|
+
const deletedShownRecord = deletesShownRecord(action, payload, shownRecordId);
|
|
389
|
+
if (onRecordLeft !== undefined && (action.redirect !== undefined || deletedShownRecord)) {
|
|
390
|
+
onRecordLeft(action, result.data, deletedShownRecord);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
await onWriteSuccess();
|
|
394
|
+
}
|
|
380
395
|
},
|
|
381
396
|
};
|
|
382
397
|
}
|
|
@@ -387,7 +402,7 @@ function buildWriteHandlerRecordAction(action, options) {
|
|
|
387
402
|
// resolved against the record currently being viewed/edited, independent of
|
|
388
403
|
// any table row.
|
|
389
404
|
export function buildRecordActions(options) {
|
|
390
|
-
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, defaultScreenTargetEntityId, sameEntityScreenId, } = options;
|
|
405
|
+
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, onRecordLeft, defaultScreenTargetEntityId, sameEntityScreenId, defaultWritePayloadId, } = options;
|
|
391
406
|
const out = [];
|
|
392
407
|
for (const action of actions) {
|
|
393
408
|
if (action.visible !== undefined && !evalFieldCondition(action.visible, record))
|
|
@@ -420,6 +435,8 @@ export function buildRecordActions(options) {
|
|
|
420
435
|
actionIcon,
|
|
421
436
|
dispatcher,
|
|
422
437
|
onWriteSuccess,
|
|
438
|
+
defaultWritePayloadId,
|
|
439
|
+
onRecordLeft,
|
|
423
440
|
}));
|
|
424
441
|
}
|
|
425
442
|
return out.length > 0 ? out : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { findDetailForScreen } from "./nav.js";
|
|
2
3
|
import { lastSegment } from "./qn.js";
|
|
3
4
|
// Shared UI-visibility predicate for the screen-render path (#1203 — nav
|
|
4
5
|
// filtering via filterByAccess in workspace-shell.tsx hid role-gated
|
|
@@ -20,15 +21,18 @@ export function navigateTargetAllows(navigate, appFeatures, userRoles) {
|
|
|
20
21
|
// forbidden ones.
|
|
21
22
|
if (appFeatures.length === 0)
|
|
22
23
|
return true;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
// Entity targets share nav's lookup so the gate and resolveTarget can't
|
|
25
|
+
// pick different screens. Short ids are globally unique
|
|
26
|
+
// (validateScreenShortIdCollisions), so the first screen hit is the one.
|
|
27
|
+
if (entity !== undefined) {
|
|
28
|
+
const found = findDetailForScreen(appFeatures, entity);
|
|
29
|
+
if (found !== undefined)
|
|
30
|
+
return isUiAccessGranted(found.screen.access, userRoles);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const feature of appFeatures) {
|
|
34
|
+
const candidate = feature.screens.find((s) => lastSegment(s.id) === screen);
|
|
35
|
+
if (candidate !== undefined)
|
|
32
36
|
return isUiAccessGranted(candidate.access, userRoles);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { RenderEdit } from "../components/render-edit.js";
|
|
4
4
|
import { useTranslation } from "../i18n.js";
|
|
@@ -44,7 +44,7 @@ function isBlank(value) {
|
|
|
44
44
|
export function SecretMintBody({ schema, screen, translate }) {
|
|
45
45
|
const nav = useNav();
|
|
46
46
|
const returnTarget = useReturnTarget(screen.id);
|
|
47
|
-
const { Card, Heading, Banner, Button, Text, Grid, GridCell, SecretReveal } = usePrimitives();
|
|
47
|
+
const { Card, Heading, Banner, Button, Text, Grid, GridCell, Section, SecretReveal } = usePrimitives();
|
|
48
48
|
const t = useTranslation();
|
|
49
49
|
const effectiveTranslate = translate ?? t;
|
|
50
50
|
const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
|
|
@@ -63,6 +63,9 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
63
63
|
}), [screen.fields, screen.urlPrefillFields, nav.searchParams, synthScreen, handoffValues]);
|
|
64
64
|
const [revealed, setRevealed] = useState(null);
|
|
65
65
|
const [done, setDone] = useState(false);
|
|
66
|
+
// The secret was minted server-side but none of the declared reveal fields
|
|
67
|
+
// came back; the mint form must not return (a re-mint would invalidate it).
|
|
68
|
+
const [revealMissing, setRevealMissing] = useState(false);
|
|
66
69
|
// Never rendered, never merged into `revealed` or the confirm form's
|
|
67
70
|
// initial values — read only from `buildPayload` at confirm-submit time.
|
|
68
71
|
const carriedRef = useRef({});
|
|
@@ -82,16 +85,36 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
82
85
|
continue;
|
|
83
86
|
values[revealField.field] = value;
|
|
84
87
|
}
|
|
88
|
+
if (Object.values(values).every(isBlank)) {
|
|
89
|
+
// biome-ignore lint/suspicious/noConsole: the user-facing banner carries no detail; this names the screen and fields for the developer
|
|
90
|
+
console.error(`secretMint "${screen.id}": write result carried no value for reveal fields [${screen.reveal.fields
|
|
91
|
+
.map((revealField) => revealField.field)
|
|
92
|
+
.join(", ")}]`);
|
|
93
|
+
setRevealMissing(true);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
85
96
|
setRevealed(values);
|
|
86
97
|
carriedRef.current =
|
|
87
98
|
confirm?.carry !== undefined ? extractCarriedValues(result.data, confirm.carry) : {};
|
|
88
|
-
}, [screen.reveal.fields, confirm?.carry]);
|
|
99
|
+
}, [screen.id, screen.reveal.fields, confirm?.carry]);
|
|
89
100
|
const handleCancel = useMemo(() => {
|
|
90
101
|
const target = screen.cancelTarget ?? screen.redirect;
|
|
91
102
|
if (target === undefined || target === false)
|
|
92
103
|
return undefined;
|
|
93
|
-
return () =>
|
|
104
|
+
return () => {
|
|
105
|
+
// A native stack keeps this screen mounted after navigate; drop the secret first.
|
|
106
|
+
setRevealed(null);
|
|
107
|
+
carriedRef.current = {};
|
|
108
|
+
navigateToReturnOr(nav, returnTarget, () => nav.navigate({ screenId: lastSegment(target) }));
|
|
109
|
+
};
|
|
94
110
|
}, [nav, screen.redirect, screen.cancelTarget, returnTarget]);
|
|
111
|
+
// The confirm step must always offer a way out: a failed confirm (e.g. an
|
|
112
|
+
// expired setup token) otherwise leaves a form that can only keep failing.
|
|
113
|
+
// Restarting re-mints, which issues a fresh secret and invalidates this one.
|
|
114
|
+
const restartMint = useCallback(() => {
|
|
115
|
+
setRevealed(null);
|
|
116
|
+
carriedRef.current = {};
|
|
117
|
+
}, []);
|
|
95
118
|
// Ends the reveal phase for both paths (the bare acknowledge button, and a
|
|
96
119
|
// successful confirm submit): clears the secret and the carried values, then
|
|
97
120
|
// either navigates (screen.redirect) or shows a done-state — never falls
|
|
@@ -113,7 +136,10 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
113
136
|
finishMint();
|
|
114
137
|
}, [finishMint]);
|
|
115
138
|
if (done) {
|
|
116
|
-
return (_jsx(Card, { children: _jsx(Banner, { variant: "info", testId: "kumiko-screen-secret-mint-done", children: effectiveTranslate(confirm?.doneMessage ?? "kumiko.secretMint.done") }) }));
|
|
139
|
+
return (_jsx(Card, { options: { screenBody: true }, children: _jsx(Banner, { variant: "info", testId: "kumiko-screen-secret-mint-done", children: effectiveTranslate(confirm?.doneMessage ?? "kumiko.secretMint.done") }) }));
|
|
140
|
+
}
|
|
141
|
+
if (revealMissing) {
|
|
142
|
+
return (_jsx(Card, { options: { screenBody: true }, children: _jsx(Banner, { variant: "error", testId: "kumiko-screen-secret-mint-missing", children: effectiveTranslate("kumiko.form.error.generic") }) }));
|
|
117
143
|
}
|
|
118
144
|
if (revealed !== null) {
|
|
119
145
|
const values = screen.reveal.fields.flatMap((revealField) => {
|
|
@@ -134,7 +160,15 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
134
160
|
},
|
|
135
161
|
];
|
|
136
162
|
});
|
|
137
|
-
|
|
163
|
+
const revealContent = (_jsxs(_Fragment, { children: [_jsx(Heading, { variant: "page", children: effectiveTranslate(screen.reveal.title ?? "kumiko.secretMint.title") }), _jsx(Banner, { variant: "warning", testId: "kumiko-screen-secret-mint-warning", children: effectiveTranslate(screen.reveal.warning ?? "kumiko.secretMint.warning") }), SecretReveal !== undefined ? (_jsx(SecretReveal, { values: values, copyLabel: effectiveTranslate("kumiko.secretMint.copy"), copiedLabel: effectiveTranslate("kumiko.secretMint.copied"), testId: "kumiko-screen-secret-mint-reveal" })) : (_jsx(Grid, { columns: 1, testId: "kumiko-screen-secret-mint-reveal", children: values.map((v) => (_jsxs(GridCell, { children: [_jsx(Text, { children: v.label }), _jsx(Text, { variant: "code", children: v.value })] }, v.label))) }))] }));
|
|
164
|
+
// One screen form like the mint phase: the reveal leads the form column
|
|
165
|
+
// instead of the confirm form nesting a second card inside a card. The
|
|
166
|
+
// keys stop the mint and confirm forms sharing one instance (and its
|
|
167
|
+
// submit state) across phases.
|
|
168
|
+
if (confirm !== undefined && confirmEntity !== undefined && confirmScreen !== undefined) {
|
|
169
|
+
return (_jsx(RenderEdit, { screen: confirmScreen, i18nScreenId: screen.id, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, onCancel: handleCancel ?? restartMint, fillScreenHeight: true, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
|
|
170
|
+
}
|
|
171
|
+
return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
|
|
138
172
|
}
|
|
139
|
-
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }));
|
|
173
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, fillScreenHeight: true, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }, "mint"));
|
|
140
174
|
}
|
|
@@ -3,13 +3,23 @@ import { useCallback, useMemo, useState } from "react";
|
|
|
3
3
|
import { useDispatcher } from "../context/dispatcher-context.js";
|
|
4
4
|
import { useQuery } from "../hooks/use-query.js";
|
|
5
5
|
import { useTranslation } from "../i18n.js";
|
|
6
|
-
import { usePrimitives } from "../primitives.js";
|
|
6
|
+
import { STICKY_PRIMARY_ACTION_PROP, usePrimitives } from "../primitives.js";
|
|
7
|
+
import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
|
|
7
8
|
import { dispatcherErrorText } from "./write-failed-error.js";
|
|
9
|
+
function groupSectionsByTitle(sections) {
|
|
10
|
+
const byTitle = new Map();
|
|
11
|
+
for (const section of sections) {
|
|
12
|
+
const title = section.title ?? "config.secrets.section";
|
|
13
|
+
// kumiko-lint-ignore section-fields-raw secretsEdit section fields are field-id strings, not EditFieldSpec
|
|
14
|
+
byTitle.set(title, [...(byTitle.get(title) ?? []), ...section.fields]);
|
|
15
|
+
}
|
|
16
|
+
return [...byTitle].map(([title, fields]) => ({ title, fields }));
|
|
17
|
+
}
|
|
8
18
|
// secrets:query:list never returns plaintext (only a redacted preview), so
|
|
9
19
|
// unlike ConfigEditBody there is no server value to pre-fill a draft with —
|
|
10
20
|
// every input starts at "" and stays that way unless the user types into it.
|
|
11
21
|
export function SecretsEditBody({ screen, translate }) {
|
|
12
|
-
const { Banner, Form, Section, Field, Input, Button, Text } = usePrimitives();
|
|
22
|
+
const { Banner, Dialog, Form, Section, Field, Input, Button, Text, Grid } = usePrimitives();
|
|
13
23
|
const t = useTranslation();
|
|
14
24
|
const effectiveTranslate = translate ?? t;
|
|
15
25
|
const dispatcher = useDispatcher();
|
|
@@ -17,6 +27,8 @@ export function SecretsEditBody({ screen, translate }) {
|
|
|
17
27
|
const [drafts, setDrafts] = useState({});
|
|
18
28
|
const [submitting, setSubmitting] = useState(false);
|
|
19
29
|
const [submitError, setSubmitError] = useState(null);
|
|
30
|
+
const [pendingDeleteKey, setPendingDeleteKey] = useState(null);
|
|
31
|
+
const [deleting, setDeleting] = useState(false);
|
|
20
32
|
const rowsByQualifiedKey = useMemo(() => {
|
|
21
33
|
const out = new Map();
|
|
22
34
|
for (const row of listQuery.data ?? [])
|
|
@@ -35,42 +47,86 @@ export function SecretsEditBody({ screen, translate }) {
|
|
|
35
47
|
return;
|
|
36
48
|
setSubmitting(true);
|
|
37
49
|
setSubmitError(null);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
try {
|
|
51
|
+
const result = await dispatcher.batch(commands);
|
|
52
|
+
if (!result.isSuccess) {
|
|
53
|
+
setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setDrafts({});
|
|
57
|
+
await listQuery.refetch();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Transport failures reject instead of returning a result; drafts stay
|
|
61
|
+
// so the user can retry without retyping the secrets.
|
|
62
|
+
setSubmitError(effectiveTranslate("kumiko.form.error.generic"));
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setSubmitting(false);
|
|
43
66
|
}
|
|
44
|
-
setDrafts({});
|
|
45
|
-
await listQuery.refetch();
|
|
46
67
|
}, [dispatcher, drafts, screen.secretKeys, listQuery.refetch, effectiveTranslate]);
|
|
47
68
|
const handleDelete = useCallback(async (qualified) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
setDeleting(true);
|
|
70
|
+
try {
|
|
71
|
+
const result = await dispatcher.write("secrets:write:delete", { key: qualified });
|
|
72
|
+
if (!result.isSuccess) {
|
|
73
|
+
setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setSubmitError(null);
|
|
77
|
+
await listQuery.refetch();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
setSubmitError(effectiveTranslate("kumiko.form.error.generic"));
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
setDeleting(false);
|
|
52
84
|
}
|
|
53
|
-
await listQuery.refetch();
|
|
54
85
|
}, [dispatcher, listQuery.refetch, effectiveTranslate]);
|
|
55
|
-
if (listQuery.loading && listQuery.data === null)
|
|
56
|
-
return
|
|
57
|
-
}
|
|
86
|
+
if (listQuery.loading && listQuery.data === null)
|
|
87
|
+
return _jsx(QueryLoadingBanner, {});
|
|
58
88
|
if (listQuery.error) {
|
|
59
|
-
return (_jsx(
|
|
89
|
+
return (_jsx(QueryErrorBanner, { error: listQuery.error, translate: effectiveTranslate, onRetry: listQuery.refetch }));
|
|
60
90
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
// The generator emits one section per feature; features without a declared
|
|
92
|
+
// title all fall under the generic "Secrets" heading and share one band.
|
|
93
|
+
const sectionGroups = groupSectionsByTitle(screen.sections);
|
|
94
|
+
const unsavedCount = Object.values(drafts).filter((value) => value.trim() !== "").length;
|
|
95
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Form, { onSubmit: () => {
|
|
96
|
+
void handleSubmit();
|
|
97
|
+
}, testId: "secrets-edit-form", width: "full", fillHeight: true, stickyActions: true, screenForm: true, unsavedCount: unsavedCount, ...(screen.description !== undefined && {
|
|
98
|
+
subtitle: effectiveTranslate(screen.description),
|
|
99
|
+
}), actions: _jsxs(_Fragment, { children: [unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: submitting, onClick: () => setDrafts({}), testId: "secrets-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: effectiveTranslate("kumiko.form.discard") })), _jsx(Button, { type: "submit", variant: "primary", loading: submitting, disabled: submitting || unsavedCount === 0, testId: "secrets-edit-submit", children: effectiveTranslate("kumiko.form.saveChanges") })] }), children: [submitError !== null && (_jsx(Banner, { variant: "error", testId: "secrets-edit-error", children: submitError })), sectionGroups.map((section) => (_jsx(Section, { layout: "settings-list", title: effectiveTranslate(section.title), subtitle: effectiveTranslate("config.secrets.description"), children: _jsx(Grid, { columns: 1, list: true, children: section.fields.map((fieldId) => {
|
|
100
|
+
const qualified = screen.secretKeys[fieldId];
|
|
101
|
+
if (qualified === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
const row = rowsByQualifiedKey.get(qualified);
|
|
104
|
+
const hintKey = screen.fieldHints?.[fieldId];
|
|
105
|
+
const isRequired = screen.requiredFields?.includes(fieldId) ?? false;
|
|
106
|
+
return (_jsx(Field, { id: fieldId, layout: "row", label: effectiveTranslate(screen.fieldLabels[fieldId] ?? fieldId), required: isRequired, testId: `field-${fieldId}`, status: row !== undefined
|
|
107
|
+
? {
|
|
108
|
+
tone: "ok",
|
|
109
|
+
label: effectiveTranslate("config.secrets.saved"),
|
|
110
|
+
testId: `secret-saved-${fieldId}`,
|
|
111
|
+
}
|
|
112
|
+
: {
|
|
113
|
+
tone: isRequired ? "bad" : "muted",
|
|
114
|
+
label: effectiveTranslate("config.secrets.notSet"),
|
|
115
|
+
testId: `secret-not-set-${fieldId}`,
|
|
116
|
+
}, ...(hintKey !== undefined && { description: effectiveTranslate(hintKey) }), ...(row !== undefined && {
|
|
117
|
+
fieldAppendix: (_jsxs(Grid, { columns: "auto", children: [_jsx(Text, { variant: "muted", testId: `secret-preview-${fieldId}`, children: row.redactedPreview !== null
|
|
118
|
+
? effectiveTranslate("config.secrets.stored", {
|
|
119
|
+
preview: row.redactedPreview,
|
|
120
|
+
})
|
|
121
|
+
: effectiveTranslate("config.secrets.set") }), _jsx(Button, { type: "button", variant: "danger-ghost", size: "sm", disabled: submitting || deleting, onClick: () => setPendingDeleteKey(qualified), testId: `secret-delete-${fieldId}`, children: effectiveTranslate("config.secrets.delete") })] })),
|
|
122
|
+
}), children: _jsx(Input, { kind: "password", id: fieldId, name: fieldId, value: drafts[fieldId] ?? "", onChange: (v) => setDraft(fieldId, v), placeholder: effectiveTranslate(row !== undefined
|
|
123
|
+
? "config.secrets.replacePlaceholder"
|
|
124
|
+
: "config.secrets.placeholder"), autoComplete: "new-password", disabled: submitting, testId: `secret-input-${fieldId}` }) }, fieldId));
|
|
125
|
+
}) }) }, section.title)))] }), _jsx(Dialog, { open: pendingDeleteKey !== null, onOpenChange: (open) => {
|
|
126
|
+
if (!open)
|
|
127
|
+
setPendingDeleteKey(null);
|
|
128
|
+
}, title: effectiveTranslate("config.secrets.delete"), description: effectiveTranslate("config.secrets.deleteConfirm"), confirmLabel: effectiveTranslate("config.secrets.delete"), variant: "danger", initialFocus: "cancel", onConfirm: async () => {
|
|
129
|
+
if (pendingDeleteKey !== null)
|
|
130
|
+
await handleDelete(pendingDeleteKey);
|
|
131
|
+
}, testId: "secrets-delete-dialog" })] }));
|
|
76
132
|
}
|
|
@@ -3,7 +3,7 @@ import { computeDerivedCellValue, currencyDecimals, groupEmbeddedListIssues, rou
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
5
5
|
import { useQuery } from "../hooks/use-query.js";
|
|
6
|
-
import {
|
|
6
|
+
import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
|
|
7
7
|
import { useTranslation } from "../i18n.js";
|
|
8
8
|
import { usePrimitives } from "../primitives.js";
|
|
9
9
|
function withRecomputedDerived(row, derived, cells) {
|
|
@@ -107,9 +107,7 @@ export function EmbeddedListField({ field, id, onChange, allIssues, featureName,
|
|
|
107
107
|
const rows = Array.isArray(field.value) ? field.value : [];
|
|
108
108
|
const derived = field.embeddedListDerived;
|
|
109
109
|
const referenceCells = cells.filter((c) => c.type === "reference");
|
|
110
|
-
const referenceSources = referenceCells.map((cell) => cell.
|
|
111
|
-
? { queryQn: cell.refOptionsQuery, labelKey: "label" }
|
|
112
|
-
: referenceLookupSource(cell.refFeature ?? featureName, cell.refEntity ?? "", cell.refLabelField ?? "id"));
|
|
110
|
+
const referenceSources = referenceCells.map((cell) => referenceOptionSource(cell, cell.refFeature ?? featureName, cell.refEntity ?? ""));
|
|
113
111
|
const referenceQueries = referenceSources.map((source) =>
|
|
114
112
|
// biome-ignore lint/correctness/useHookAtTopLevel: referenceCells comes from the entity-schema definition — fixed for the screen's lifetime, not a real conditional-hook risk.
|
|
115
113
|
useQuery(source.queryQn, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import type { FieldCellWidth, usePrimitives } from "../primitives.js";
|
|
3
|
+
import type { FieldCellWidth, FieldProps, usePrimitives } from "../primitives.js";
|
|
4
4
|
export type GridCellForFieldProps = {
|
|
5
5
|
readonly field: EditFieldViewModel;
|
|
6
6
|
readonly columns: number;
|
|
@@ -12,6 +12,9 @@ export type GridCellForFieldProps = {
|
|
|
12
12
|
readonly featureName: string;
|
|
13
13
|
readonly labelAppendix?: ReactNode;
|
|
14
14
|
readonly fieldAppendix?: ReactNode;
|
|
15
|
+
readonly appendixPlacement?: FieldProps["appendixPlacement"];
|
|
16
|
+
readonly description?: ReactNode;
|
|
17
|
+
readonly status?: FieldProps["status"];
|
|
15
18
|
/** Full issues-by-path map (FormSnapshot.errors) — passed through for
|
|
16
19
|
* embedded-list fields, which bucket row-/cell-level issues themselves. */
|
|
17
20
|
readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
@@ -21,6 +24,8 @@ export type GridCellForFieldProps = {
|
|
|
21
24
|
readonly row: Readonly<Record<string, unknown>>;
|
|
22
25
|
readonly changed?: boolean;
|
|
23
26
|
readonly flow?: boolean;
|
|
27
|
+
readonly layout?: FieldProps["layout"];
|
|
28
|
+
readonly accent?: boolean;
|
|
24
29
|
};
|
|
25
30
|
export declare function fieldCellWidth(field: EditFieldViewModel): FieldCellWidth;
|
|
26
|
-
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }: GridCellForFieldProps): ReactNode;
|
|
31
|
+
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, }: GridCellForFieldProps): ReactNode;
|
|
@@ -29,10 +29,10 @@ export function fieldCellWidth(field) {
|
|
|
29
29
|
return "full";
|
|
30
30
|
return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
|
|
31
31
|
}
|
|
32
|
-
export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }) {
|
|
32
|
+
export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, }) {
|
|
33
33
|
// RenderField renders nothing for a hidden field, but the GridCell around it still claims the row.
|
|
34
34
|
if (!field.visible)
|
|
35
35
|
return null;
|
|
36
36
|
const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
|
|
37
|
-
return (_jsx(GridCell, { span: effectiveSpan, ...(flow === true && { width: fieldCellWidth(field) }), children: _jsx(RenderField, { field: field, ...(issues !== undefined && { issues }), onChange: onChange, featureName: featureName, ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), allIssues: allIssues, valueDisplay: valueDisplay, row: row, ...(changed === true && { changed }) }) }));
|
|
37
|
+
return (_jsx(GridCell, { span: effectiveSpan, ...(flow === true && { width: fieldCellWidth(field) }), children: _jsx(RenderField, { field: field, ...(issues !== undefined && { issues }), onChange: onChange, featureName: featureName, ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(appendixPlacement !== undefined && { appendixPlacement }), ...(description !== undefined && { description }), ...(status !== undefined && { status }), allIssues: allIssues, valueDisplay: valueDisplay, row: row, ...(changed === true && { changed }), ...(layout !== undefined && { layout }), ...(accent === true && { accent }) }) }));
|
|
38
38
|
}
|
|
@@ -32,5 +32,8 @@ export function MultiSelectCheckboxes({ field, id, options, value, onChange, })
|
|
|
32
32
|
};
|
|
33
33
|
return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: disabled, onClick: toggleAll, testId: `${id}-select-all`, children: allSelected
|
|
34
34
|
? t("kumiko.field.multiSelect.deselect-all")
|
|
35
|
-
: t("kumiko.field.multiSelect.select-all") }), _jsx(Grid, { columns: field.columns ?? 2, testId: `${id}-checkboxes`,
|
|
35
|
+
: t("kumiko.field.multiSelect.select-all") }), _jsx(Grid, { columns: field.columns ?? 2, testId: `${id}-checkboxes`,
|
|
36
|
+
// Field's label targets `id`, which no checkbox carries here; point the
|
|
37
|
+
// group at Field's `${id}-label` element instead.
|
|
38
|
+
ariaLabelledBy: `${id}-label`, ...(field.maxRows !== undefined && { maxRows: field.maxRows }), children: options.map((opt) => (_jsx(GridCell, { children: _jsx(Field, { id: `${id}-${opt.value}`, label: opt.label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${id}-${opt.value}`, name: `${id}-${opt.value}`, value: selected.has(opt.value), onChange: (checked) => toggleOption(opt.value, checked), disabled: disabled }) }) }, opt.value))) })] }));
|
|
36
39
|
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import type
|
|
2
|
+
import { type OptionsQueryPayload } from "@cosmicdrift/kumiko-types/fields";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
type ResolvedOptionsQueryPayload = Readonly<Record<string, string | number | boolean>>;
|
|
5
|
+
export declare function resolveOptionsQueryPayload(payload: OptionsQueryPayload, row: Readonly<Record<string, unknown>>): ResolvedOptionsQueryPayload;
|
|
3
6
|
type QueryOptionsSelectProps = {
|
|
4
7
|
readonly field: EditFieldViewModel;
|
|
5
8
|
readonly query: string;
|
|
6
|
-
readonly payload:
|
|
9
|
+
readonly payload: OptionsQueryPayload;
|
|
10
|
+
readonly row: Readonly<Record<string, unknown>>;
|
|
7
11
|
readonly id: string;
|
|
8
12
|
readonly hasError: boolean;
|
|
9
13
|
readonly onChange: (value: unknown) => void;
|
|
10
14
|
};
|
|
11
|
-
export declare function QueryOptionsSelect({ field, query, payload, id, hasError, onChange, }: QueryOptionsSelectProps): ReactNode;
|
|
15
|
+
export declare function QueryOptionsSelect({ field, query, payload, row, id, hasError, onChange, }: QueryOptionsSelectProps): ReactNode;
|
|
16
|
+
export declare function QueryOptionLabel({ query, payload, value, }: {
|
|
17
|
+
readonly query: string;
|
|
18
|
+
readonly payload: ResolvedOptionsQueryPayload;
|
|
19
|
+
readonly value: string | number | boolean;
|
|
20
|
+
}): ReactNode;
|
|
12
21
|
export {};
|