@cosmicdrift/kumiko-renderer 0.377.0 → 0.379.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-shim.js +1 -2
- package/dist/app/action-form-support.js +16 -4
- package/dist/app/kumiko-screen.js +51 -38
- package/dist/app/list-empty-content.d.ts +1 -0
- package/dist/app/list-empty-content.js +5 -0
- package/dist/app/row-actions.d.ts +7 -1
- package/dist/app/row-actions.js +6 -1
- package/dist/app/secret-mint-body.js +3 -2
- package/dist/components/embedded-list-field.js +16 -7
- package/dist/components/file-list-field.js +4 -0
- package/dist/components/form-derivation-patch.d.ts +8 -0
- package/dist/components/form-derivation-patch.js +24 -0
- package/dist/components/form-screen-layout.d.ts +13 -0
- package/dist/components/form-screen-layout.js +13 -0
- package/dist/components/grid-cell-for-field.d.ts +4 -1
- package/dist/components/grid-cell-for-field.js +6 -2
- package/dist/components/list-empty-state.d.ts +4 -1
- package/dist/components/list-empty-state.js +4 -1
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +4 -1
- package/dist/components/render-edit-logic.d.ts +1 -1
- package/dist/components/render-edit-logic.js +8 -2
- package/dist/components/render-edit-types.d.ts +7 -1
- package/dist/components/render-edit.js +38 -6
- package/dist/components/render-field.d.ts +3 -1
- package/dist/components/render-field.js +27 -8
- package/dist/components/render-list.d.ts +3 -1
- package/dist/components/render-list.js +29 -8
- package/dist/components/write-form-section.js +4 -1
- package/dist/field-issue-text.d.ts +4 -0
- package/dist/field-issue-text.js +14 -0
- package/dist/file-ids-of.d.ts +1 -0
- package/dist/file-ids-of.js +7 -0
- package/dist/flow-diagram/icons.d.ts +3 -0
- package/dist/flow-diagram/icons.js +14 -0
- package/dist/flow-diagram/index.d.ts +2 -0
- package/dist/flow-diagram/index.js +1 -0
- package/dist/flow-diagram/render.d.ts +3 -0
- package/dist/flow-diagram/render.js +234 -0
- package/dist/flow-diagram/types.d.ts +18 -0
- package/dist/flow-diagram/types.js +1 -0
- package/dist/headless/dispatcher/index.d.ts +1 -0
- package/dist/headless/dispatcher/index.js +1 -0
- package/dist/headless/dispatcher/request-id.d.ts +1 -0
- package/dist/headless/dispatcher/request-id.js +15 -0
- package/dist/headless/form/form-controller.js +16 -1
- package/dist/headless/form/zod-bridge.js +7 -0
- package/dist/headless/index.d.ts +2 -1
- package/dist/headless/index.js +2 -1
- package/dist/headless/plural-key.d.ts +1 -0
- package/dist/headless/plural-key.js +5 -0
- package/dist/headless/view-model/edit.d.ts +8 -1
- package/dist/headless/view-model/edit.js +23 -7
- package/dist/headless/view-model/types.d.ts +11 -3
- package/dist/hooks/live-refetch-scheduler.d.ts +6 -0
- package/dist/hooks/live-refetch-scheduler.js +45 -0
- package/dist/hooks/use-query.js +8 -4
- package/dist/hooks/use-reference-lookup.d.ts +1 -0
- package/dist/hooks/use-reference-lookup.js +6 -3
- package/dist/i18n-defaults.js +28 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/leave-guard-dialog.d.ts +2 -0
- package/dist/leave-guard-dialog.js +28 -0
- package/dist/leave-guard.d.ts +15 -0
- package/dist/leave-guard.js +39 -0
- package/dist/live/dispatcher-live.js +1 -16
- package/dist/primitives.d.ts +39 -0
- package/dist/sse/live-events.d.ts +3 -0
- package/dist/sse/live-events.js +3 -0
- package/dist/web/app/create-app.js +2 -2
- package/dist/web/app/dashboard-body.js +13 -1
- package/dist/web/app/nav.js +51 -9
- package/dist/web/index.d.ts +2 -2
- package/dist/web/index.js +1 -1
- package/dist/web/layout/nav-tree.js +13 -3
- package/dist/web/layout/shell-header.js +3 -3
- package/dist/web/lib/file-upload-form-data.d.ts +2 -0
- package/dist/web/lib/file-upload-form-data.js +2 -0
- package/dist/web/lib/upload-error-key.d.ts +7 -0
- package/dist/web/lib/upload-error-key.js +23 -0
- package/dist/web/lib/upload-file-ref.d.ts +1 -1
- package/dist/web/primitives/embedded-list-input.js +5 -4
- package/dist/web/primitives/file-thumbnail.d.ts +6 -0
- package/dist/web/primitives/file-thumbnail.js +19 -19
- package/dist/web/primitives/file-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +11 -7
- package/dist/web/primitives/file-value-list.d.ts +3 -0
- package/dist/web/primitives/file-value-list.js +22 -0
- package/dist/web/primitives/files-dropzone.d.ts +16 -0
- package/dist/web/primitives/files-dropzone.js +52 -0
- package/dist/web/primitives/index.js +65 -30
- package/dist/web/primitives/segmented-select.js +8 -2
- package/dist/web/sse/live-events.d.ts +13 -0
- package/dist/web/sse/live-events.js +88 -23
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -0
- package/dist/web/widgets/bounded-load-cache.js +25 -0
- package/dist/web/widgets/drawer.js +37 -2
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.d.ts +10 -0
- package/dist/web/widgets/pdf-preview.js +140 -0
- package/dist/web/widgets/upload-zone.d.ts +11 -2
- package/dist/web/widgets/upload-zone.js +30 -7
- package/package.json +14 -5
- package/src/changes.json +143 -0
- package/src/web/styles.css +13 -9
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
// RenderActionForm-Komponente, die `fields` direkt als Input nimmt.
|
|
15
15
|
// Der Boot-Validator kennt actionForm bereits eigenständig (kein
|
|
16
16
|
// entity-Lookup), also ist der Server-Pfad davon nicht betroffen.
|
|
17
|
-
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
18
|
-
const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
|
|
17
|
+
import { ACTION_FORM_PSEUDO_ENTITY, isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
19
18
|
/** Baut eine minimale EntityDefinition aus den Inline-Fields eines
|
|
20
19
|
* ActionForm- oder SecretMint-Screens. RenderEdit + computeEditViewModel
|
|
21
20
|
* iterieren über entity.fields zur Render-Zeit; alle weiteren
|
|
@@ -40,11 +40,12 @@ function pickDeclaredFields(record, fieldNames) {
|
|
|
40
40
|
/** Loads the initial values of an actionForm declaring `load`; undefined for a
|
|
41
41
|
* form without `load`. */
|
|
42
42
|
export function useActionFormLoad(load, recordId, fieldNames) {
|
|
43
|
-
const
|
|
43
|
+
const isSingleton = load?.singleton === true;
|
|
44
|
+
const query = useQuery(load?.query ?? "", isSingleton ? {} : { [load?.idParam ?? DEFAULT_LOAD_ID_PARAM]: recordId }, { enabled: load !== undefined && (isSingleton || recordId !== undefined) });
|
|
44
45
|
const values = useMemo(() => pickDeclaredFields(query.data, fieldNames), [query.data, fieldNames]);
|
|
45
46
|
if (load === undefined)
|
|
46
47
|
return undefined;
|
|
47
|
-
if (recordId === undefined)
|
|
48
|
+
if (!isSingleton && recordId === undefined)
|
|
48
49
|
return { status: "missing-id" };
|
|
49
50
|
if (query.error !== null)
|
|
50
51
|
return { status: "error", error: query.error };
|
|
@@ -90,7 +91,18 @@ export function resolveSuccessFacts(facts, data, translate, locale) {
|
|
|
90
91
|
});
|
|
91
92
|
return resolved;
|
|
92
93
|
}
|
|
94
|
+
// Lines can repeat (two files with one name), so the position is the only stable key.
|
|
95
|
+
function positionedLines(lines) {
|
|
96
|
+
return lines.map((line, position) => ({ position: String(position), line }));
|
|
97
|
+
}
|
|
93
98
|
export function ActionFormSuccessBanner({ success, }) {
|
|
94
|
-
const { Banner, Link, Text } = usePrimitives();
|
|
95
|
-
|
|
99
|
+
const { Banner, Grid, Link, Text } = usePrimitives();
|
|
100
|
+
// Text renders inline, so the message and each fact sit in a one-column grid to get their own lines.
|
|
101
|
+
return (_jsxs(Banner, { variant: "info", testId: "action-form-success", children: [_jsxs(Grid, { columns: 1, children: [_jsx(Text, { testId: "action-form-success-text", children: success.text }), success.facts.map((fact) => {
|
|
102
|
+
const factTestId = `action-form-success-fact-${fact.key}`;
|
|
103
|
+
if (fact.lines.length === 1) {
|
|
104
|
+
return (_jsxs(Text, { testId: factTestId, children: [fact.label, ": ", fact.lines[0]] }, fact.key));
|
|
105
|
+
}
|
|
106
|
+
return (_jsxs(Grid, { columns: 1, testId: factTestId, children: [_jsxs(Text, { children: [fact.label, ":"] }), positionedLines(fact.lines).map(({ position, line }) => (_jsx(Text, { testId: `${factTestId}-line-${position}`, children: line }, position)))] }, fact.key));
|
|
107
|
+
})] }), success.links.map((link) => (_jsx(Link, { href: link.href, onPress: link.onPress, className: "ml-2 underline", testId: `action-form-success-link-${link.key}`, children: link.label }, link.key)))] }));
|
|
96
108
|
}
|
|
@@ -5,6 +5,7 @@ import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
|
5
5
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
6
6
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
7
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { withDerivedValues } from "../components/form-derivation-patch.js";
|
|
8
9
|
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
9
10
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
10
11
|
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
@@ -20,6 +21,7 @@ import { useQuery } from "../hooks/use-query.js";
|
|
|
20
21
|
import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
|
|
21
22
|
import { useLocale, useOptionalTimeZone, useTranslation } from "../i18n.js";
|
|
22
23
|
import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
|
|
24
|
+
import { requestLeave } from "../leave-guard.js";
|
|
23
25
|
import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
|
|
24
26
|
import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
|
|
25
27
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
@@ -36,7 +38,7 @@ import { FacetCountBridges } from "./facet-count-bridge.js";
|
|
|
36
38
|
import { buildFormSchema } from "./form-schema.js";
|
|
37
39
|
import { HeaderActionsBar } from "./header-actions-bar.js";
|
|
38
40
|
import { hiddenRequiredFieldsFilled, layoutEditFields, layoutFieldNames } from "./layout-fields.js";
|
|
39
|
-
import { isListNarrowed, useListEmptyContent } from "./list-empty-content.js";
|
|
41
|
+
import { isDefaultFilterActive, isListNarrowed, useListEmptyContent, } from "./list-empty-content.js";
|
|
40
42
|
import { buildDateRangePayload, buildEntityFilterFacets, buildFacetCountRequests, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, normalizeDefaultFilters, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
|
|
41
43
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
42
44
|
import { synthesizeProjectionDetailEntity, synthesizeProjectionDetailScreen, } from "./projection-detail-shim.js";
|
|
@@ -93,7 +95,6 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
93
95
|
return _jsx(ReturnHostProvider, { value: ownHost, children: body });
|
|
94
96
|
}
|
|
95
97
|
function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, children, }) {
|
|
96
|
-
const { Banner } = usePrimitives();
|
|
97
98
|
const result = useQuery(visibleWhen.query, {}, { live: true });
|
|
98
99
|
const wasShown = useRef(false);
|
|
99
100
|
const verdict = evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
@@ -104,7 +105,7 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
|
|
|
104
105
|
if (verdict === "visible")
|
|
105
106
|
return children;
|
|
106
107
|
if (verdict === "loading") {
|
|
107
|
-
return
|
|
108
|
+
return _jsx(QueryLoadingBanner, {});
|
|
108
109
|
}
|
|
109
110
|
if (fallback !== undefined) {
|
|
110
111
|
return _jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate });
|
|
@@ -723,7 +724,6 @@ function EntityEditScreen({ schema, screen, translate, entityId, onCopyLink, })
|
|
|
723
724
|
return (_jsx(EntityEditCreateOrDisabled, { schema: schema, screen: screen, entity: entity, ...(translate !== undefined && { translate }) }));
|
|
724
725
|
}
|
|
725
726
|
function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
726
|
-
const { Banner } = usePrimitives();
|
|
727
727
|
const nav = useNav();
|
|
728
728
|
const handoffValues = useInitialValuesHandoff(screen.id);
|
|
729
729
|
const appFeatures = useAppFeatures();
|
|
@@ -799,7 +799,7 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
799
799
|
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
800
800
|
// its real tenant currency is still in flight (fw#2933) — wait instead.
|
|
801
801
|
if (currencyLoading) {
|
|
802
|
-
return
|
|
802
|
+
return _jsx(QueryLoadingBanner, {});
|
|
803
803
|
}
|
|
804
804
|
// Deliberately no `actions` prop here: `screen.actions` targets an
|
|
805
805
|
// EXISTING record (publish/archive/duplicate and friends), which the
|
|
@@ -812,7 +812,6 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
812
812
|
// `<feature>:write:<entity>:update` on submit. `buildPayload` shapes the
|
|
813
813
|
// snapshot into Kumiko's update-event envelope `{ id, version, changes }`.
|
|
814
814
|
function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onCopyLink, autoReloadOnSave, onDeleted, }) {
|
|
815
|
-
const { Banner } = usePrimitives();
|
|
816
815
|
const t = useTranslation();
|
|
817
816
|
const effectiveTranslate = translate ?? t;
|
|
818
817
|
const detailQn = `${toKebab(schema.featureName)}:query:${toKebab(screen.entity)}:detail`;
|
|
@@ -822,11 +821,13 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
822
821
|
const returnTargetParam = useReturnTarget(screen.id);
|
|
823
822
|
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
824
823
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
825
|
-
return
|
|
824
|
+
return _jsx(QueryLoadingBanner, {});
|
|
826
825
|
}
|
|
827
|
-
if (detailQuery.error) {
|
|
828
|
-
return (_jsx(
|
|
826
|
+
if (detailQuery.error && detailQuery.data === null) {
|
|
827
|
+
return (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch }));
|
|
829
828
|
}
|
|
829
|
+
// A failed refetch keeps the last good data on screen.
|
|
830
|
+
const refetchErrorBanner = detailQuery.error ? (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch })) : null;
|
|
830
831
|
const record = detailQuery.data;
|
|
831
832
|
if (!record) {
|
|
832
833
|
const onBack = returnTarget !== undefined || findEntityListScreen(schema, screen.entity) !== undefined
|
|
@@ -841,10 +842,9 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
841
842
|
// weiter die stale version stampen und der Konflikt-Recovery wäre
|
|
842
843
|
// kaputt.
|
|
843
844
|
const recordKey = versionOf(record);
|
|
844
|
-
return (_jsx(EntityEditUpdateForm, { schema: schema, screen: screen, entity: entity, entityId: entityId, record: record, onReload: detailQuery.refetch, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(autoReloadOnSave === true && { onSaved: () => void detailQuery.refetch() }), ...(onDeleted !== undefined && { onDeleted }) }, `${entityId}:${recordKey}`));
|
|
845
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(EntityEditUpdateForm, { schema: schema, screen: screen, entity: entity, entityId: entityId, record: record, onReload: detailQuery.refetch, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(autoReloadOnSave === true && { onSaved: () => void detailQuery.refetch() }), ...(onDeleted !== undefined && { onDeleted }) }, `${entityId}:${recordKey}`)] }));
|
|
845
846
|
}
|
|
846
847
|
function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
|
|
847
|
-
const { Banner } = usePrimitives();
|
|
848
848
|
// Seed the form with the server values for the entity's declared
|
|
849
849
|
// fields; anything else (id, tenant_id, created_at…) stays out of
|
|
850
850
|
// the form and lives in the closure. The record's `version` is
|
|
@@ -1018,7 +1018,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
1018
1018
|
// stored (non-empty) value is unaffected — it never reaches this branch's
|
|
1019
1019
|
// fallback because `initial` above already prefers `record[name]`.
|
|
1020
1020
|
if (currencyLoading) {
|
|
1021
|
-
return
|
|
1021
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1022
1022
|
}
|
|
1023
1023
|
// Evaluated against the stored record only, so typing in a form can never
|
|
1024
1024
|
// flip the screen between editable and view.
|
|
@@ -1044,23 +1044,24 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
1044
1044
|
// existing record via list(limit:1) before deciding create vs update —
|
|
1045
1045
|
// otherwise every visit without an id would create another record.
|
|
1046
1046
|
function EntityEditSingletonBody({ schema, screen, entity, translate, onCopyLink, }) {
|
|
1047
|
-
const { Banner } = usePrimitives();
|
|
1048
1047
|
const t = useTranslation();
|
|
1049
1048
|
const effectiveTranslate = translate ?? t;
|
|
1050
1049
|
const listQn = entityQueryCommand(schema.featureName, screen.entity, "list");
|
|
1051
1050
|
const listQuery = useQuery(listQn, { limit: 1 });
|
|
1052
1051
|
if (listQuery.loading && listQuery.data === null) {
|
|
1053
|
-
return
|
|
1052
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1054
1053
|
}
|
|
1055
|
-
if (listQuery.error) {
|
|
1056
|
-
return (_jsx(
|
|
1054
|
+
if (listQuery.error && listQuery.data === null) {
|
|
1055
|
+
return (_jsx(QueryErrorBanner, { error: listQuery.error, translate: effectiveTranslate, onRetry: listQuery.refetch }));
|
|
1057
1056
|
}
|
|
1057
|
+
// A failed refetch keeps the last good data on screen.
|
|
1058
|
+
const refetchErrorBanner = listQuery.error ? (_jsx(QueryErrorBanner, { error: listQuery.error, translate: effectiveTranslate, onRetry: listQuery.refetch })) : null;
|
|
1058
1059
|
const rawExistingId = listQuery.data?.rows[0]?.["id"];
|
|
1059
1060
|
const existingId = typeof rawExistingId === "string" ? rawExistingId : undefined;
|
|
1060
1061
|
if (existingId !== undefined) {
|
|
1061
|
-
return (_jsx(EntityEditUpdateBody, { schema: schema, screen: screen, entity: entity, entityId: existingId, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), autoReloadOnSave: true, onDeleted: () => void listQuery.refetch() }));
|
|
1062
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(EntityEditUpdateBody, { schema: schema, screen: screen, entity: entity, entityId: existingId, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), autoReloadOnSave: true, onDeleted: () => void listQuery.refetch() })] }));
|
|
1062
1063
|
}
|
|
1063
|
-
return (_jsx(EntityEditCreateOrDisabled, { schema: schema, screen: screen, entity: entity, ...(translate !== undefined && { translate }), onSaved: () => void listQuery.refetch() }));
|
|
1064
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(EntityEditCreateOrDisabled, { schema: schema, screen: screen, entity: entity, ...(translate !== undefined && { translate }), onSaved: () => void listQuery.refetch() })] }));
|
|
1064
1065
|
}
|
|
1065
1066
|
// Shared no-entityId tail for both the plain create path and the
|
|
1066
1067
|
// singleton path's empty-table fallback: `allowCreate: false` blocks
|
|
@@ -1247,7 +1248,6 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1247
1248
|
const featureName = schema.featureName;
|
|
1248
1249
|
const resolvedCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
|
|
1249
1250
|
const onCreate = screen.allowCreate === false ? undefined : resolvedCreate;
|
|
1250
|
-
const { Banner } = usePrimitives();
|
|
1251
1251
|
const queryType = entityQueryCommand(featureName, screen.entity, "list");
|
|
1252
1252
|
const nav = useNav();
|
|
1253
1253
|
const host = useReturnHost();
|
|
@@ -1571,6 +1571,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1571
1571
|
refetch: refreshRowsAfterWrite,
|
|
1572
1572
|
openDrawer,
|
|
1573
1573
|
host,
|
|
1574
|
+
access: { appFeatures, userRoles },
|
|
1574
1575
|
}), [
|
|
1575
1576
|
screen.toolbarActions,
|
|
1576
1577
|
effectiveTranslate,
|
|
@@ -1579,6 +1580,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1579
1580
|
dispatcher,
|
|
1580
1581
|
refreshRowsAfterWrite,
|
|
1581
1582
|
openDrawer,
|
|
1583
|
+
appFeatures,
|
|
1584
|
+
userRoles,
|
|
1582
1585
|
]);
|
|
1583
1586
|
const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
|
|
1584
1587
|
nav,
|
|
@@ -1618,11 +1621,13 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1618
1621
|
invalidateExpansions,
|
|
1619
1622
|
]);
|
|
1620
1623
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1621
|
-
return
|
|
1624
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1622
1625
|
}
|
|
1623
|
-
if (rowsQuery.error) {
|
|
1624
|
-
return (_jsx(
|
|
1626
|
+
if (rowsQuery.error && rowsQuery.data === null) {
|
|
1627
|
+
return (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch }));
|
|
1625
1628
|
}
|
|
1629
|
+
// A failed refetch keeps the last good data on screen.
|
|
1630
|
+
const refetchErrorBanner = rowsQuery.error ? (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch })) : null;
|
|
1626
1631
|
// RenderList's onRowClick is a 1-arg callback; KumikoScreen's
|
|
1627
1632
|
// 2-arg shape (row, entityName) is the public surface — thread the
|
|
1628
1633
|
// screen's entity through here so callers don't have to re-derive
|
|
@@ -1665,7 +1670,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1665
1670
|
// Bei Infinite-Mode: rows kommen aus accumulated (über mehrere
|
|
1666
1671
|
// useQuery-Calls gesammelt), bei pages/false aus dem aktuellen Result.
|
|
1667
1672
|
const renderRows = useInfinite ? accumulated : (rowsQuery.data?.rows ?? []);
|
|
1668
|
-
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, narrowed: isListNarrowed(urlState.q, urlState.filters, defaultFilters), ...(screen.description !== undefined && {
|
|
1673
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _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, narrowed: isListNarrowed(urlState.q, urlState.filters, defaultFilters), defaultFilterActive: isDefaultFilterActive(urlState.q, urlState.filters, defaultFilters), ...(screen.description !== undefined && {
|
|
1669
1674
|
description: effectiveTranslate(screen.description),
|
|
1670
1675
|
}), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1671
1676
|
? { screenPadding: false, chromeless: true }
|
|
@@ -1837,6 +1842,7 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1837
1842
|
refetch: rowsQuery.refetch,
|
|
1838
1843
|
openDrawer,
|
|
1839
1844
|
host,
|
|
1845
|
+
access: { appFeatures, userRoles },
|
|
1840
1846
|
}), [
|
|
1841
1847
|
screen.toolbarActions,
|
|
1842
1848
|
effectiveTranslate,
|
|
@@ -1845,6 +1851,8 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1845
1851
|
dispatcher,
|
|
1846
1852
|
rowsQuery.refetch,
|
|
1847
1853
|
openDrawer,
|
|
1854
|
+
appFeatures,
|
|
1855
|
+
userRoles,
|
|
1848
1856
|
]);
|
|
1849
1857
|
const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
|
|
1850
1858
|
nav,
|
|
@@ -1854,11 +1862,13 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1854
1862
|
onWriteSuccess: rowsQuery.refetch,
|
|
1855
1863
|
});
|
|
1856
1864
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1857
|
-
return
|
|
1865
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1858
1866
|
}
|
|
1859
|
-
if (rowsQuery.error) {
|
|
1860
|
-
return (_jsx(
|
|
1867
|
+
if (rowsQuery.error && rowsQuery.data === null) {
|
|
1868
|
+
return (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch }));
|
|
1861
1869
|
}
|
|
1870
|
+
// A failed refetch keeps the last good data on screen.
|
|
1871
|
+
const refetchErrorBanner = rowsQuery.error ? (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch })) : null;
|
|
1862
1872
|
// rowsQuery.data is typed as PagedRows but the query handler is free to
|
|
1863
1873
|
// return anything at runtime (fw#2216: a bare array silently rendered an
|
|
1864
1874
|
// empty table instead of surfacing the mismatch); a system-boundary cast
|
|
@@ -1889,7 +1899,7 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1889
1899
|
onPageSizeChange: urlState.setPageSize,
|
|
1890
1900
|
}
|
|
1891
1901
|
: undefined;
|
|
1892
|
-
return (_jsxs(_Fragment, { children: [_jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: rowsQuery.data?.rows ?? [], featureName: schema.featureName, searchable: searchable, narrowed: isListNarrowed(activeSearch, urlState.filters), ...(screen.description !== undefined && {
|
|
1902
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleProjectionFacetOptions }), _jsx(RenderList, { screen: listScreen, entity: entity, rows: rowsQuery.data?.rows ?? [], featureName: schema.featureName, searchable: searchable, narrowed: isListNarrowed(activeSearch, urlState.filters), ...(screen.description !== undefined && {
|
|
1893
1903
|
description: effectiveTranslate(screen.description),
|
|
1894
1904
|
}), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: activeSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1895
1905
|
? { screenPadding: false, chromeless: true }
|
|
@@ -2180,13 +2190,13 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2180
2190
|
host,
|
|
2181
2191
|
dispatcher,
|
|
2182
2192
|
openDrawer,
|
|
2183
|
-
onWriteSuccess:
|
|
2193
|
+
onWriteSuccess: reloadDetail,
|
|
2184
2194
|
onRecordLeft: handleRecordLeft,
|
|
2185
2195
|
sameEntityScreenId,
|
|
2186
2196
|
clientActions,
|
|
2187
2197
|
}), [
|
|
2188
2198
|
detailQuery.data,
|
|
2189
|
-
|
|
2199
|
+
reloadDetail,
|
|
2190
2200
|
effectiveTranslate,
|
|
2191
2201
|
nav,
|
|
2192
2202
|
host,
|
|
@@ -2206,7 +2216,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2206
2216
|
host,
|
|
2207
2217
|
dispatcher,
|
|
2208
2218
|
openDrawer,
|
|
2209
|
-
onWriteSuccess:
|
|
2219
|
+
onWriteSuccess: reloadDetail,
|
|
2210
2220
|
onRecordLeft: handleRecordLeft,
|
|
2211
2221
|
sameEntityScreenId,
|
|
2212
2222
|
clientActions,
|
|
@@ -2223,7 +2233,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2223
2233
|
dispatcher,
|
|
2224
2234
|
clientActions,
|
|
2225
2235
|
detailQuery.data,
|
|
2226
|
-
|
|
2236
|
+
reloadDetail,
|
|
2227
2237
|
openDrawer,
|
|
2228
2238
|
handleRecordLeft,
|
|
2229
2239
|
sameEntityScreenId,
|
|
@@ -2248,11 +2258,13 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2248
2258
|
return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-detail-missing-id", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " (projectionDetail) needs a row id in the path \u2014 open it from a row action."] }));
|
|
2249
2259
|
}
|
|
2250
2260
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
2251
|
-
return
|
|
2261
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2252
2262
|
}
|
|
2253
|
-
if (detailQuery.error) {
|
|
2254
|
-
return (_jsx(
|
|
2263
|
+
if (detailQuery.error && detailQuery.data === null) {
|
|
2264
|
+
return (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch }));
|
|
2255
2265
|
}
|
|
2266
|
+
// A failed refetch keeps the last good record on screen.
|
|
2267
|
+
const refetchErrorBanner = detailQuery.error ? (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch })) : null;
|
|
2256
2268
|
const record = detailQuery.data;
|
|
2257
2269
|
if (!record) {
|
|
2258
2270
|
const backListScreen = screen.listScreenId ??
|
|
@@ -2336,8 +2348,8 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2336
2348
|
label: effectiveTranslate(section.title ?? section.id ?? ""),
|
|
2337
2349
|
...(count !== undefined && { count }),
|
|
2338
2350
|
};
|
|
2339
|
-
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
2340
|
-
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { refreshToken: reloadNonce, screen: renderedDetailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
2351
|
+
}), activeId: activeSection.id ?? "", onSelect: (id) => requestLeave(() => nav.setSearchParams({ tab: id })) }))] }));
|
|
2352
|
+
return (_jsxs(_Fragment, { children: [refetchErrorBanner, _jsx(RenderEdit, { refreshToken: reloadNonce, screen: renderedDetailScreen, entity: entity, featureName: schema.featureName, initial: record, entityId: effectiveEntityId, customSubmit: async () => ({ isSuccess: true, validationBlocked: false, data: undefined }), onReload: reloadDetail, onRelatedListDrawerAction: openDrawer, ...(translate !== undefined && { translate }), ...(hasTabs && { hideSectionTitles: true }), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...((hasHeaderCard ||
|
|
2341
2353
|
hasTabs ||
|
|
2342
2354
|
screen.slots?.header !== undefined ||
|
|
2343
2355
|
screen.recordTitleField !== undefined) && {
|
|
@@ -2549,6 +2561,7 @@ function ActionFormBody({ schema, screen, translate, entityId, initialOverrides,
|
|
|
2549
2561
|
handoffValues,
|
|
2550
2562
|
loadedValues,
|
|
2551
2563
|
]);
|
|
2564
|
+
const derivedInitial = useMemo(() => withDerivedValues(initial, screen.derive !== undefined ? screen : undefined), [initial, screen]);
|
|
2552
2565
|
const t = useTranslation();
|
|
2553
2566
|
const effectiveTranslate = translate ?? t;
|
|
2554
2567
|
const summary = useMemo(() => resolveActionFormSummary(screen.summary, initialOverrides, effectiveTranslate, locale), [screen.summary, initialOverrides, effectiveTranslate, locale]);
|
|
@@ -2642,7 +2655,7 @@ function ActionFormBody({ schema, screen, translate, entityId, initialOverrides,
|
|
|
2642
2655
|
// Rendering the form before a tenant-declared currency lands would show the
|
|
2643
2656
|
// fallback and, on a fast click, submit it (fw#2933) — hold instead.
|
|
2644
2657
|
if (currencyLoading) {
|
|
2645
|
-
return
|
|
2658
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2646
2659
|
}
|
|
2647
2660
|
if (currencyUnresolved) {
|
|
2648
2661
|
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-currency-unresolved", children: "The tenant currency could not be loaded, so this form cannot be submitted." }));
|
|
@@ -2650,7 +2663,7 @@ function ActionFormBody({ schema, screen, translate, entityId, initialOverrides,
|
|
|
2650
2663
|
if (loadState !== undefined && loadState.status !== "ready") {
|
|
2651
2664
|
return (_jsx(ActionFormLoadStatus, { state: loadState, screenId: screen.id, translate: effectiveTranslate }));
|
|
2652
2665
|
}
|
|
2653
|
-
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial:
|
|
2666
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: derivedInitial, ...(screen.derive !== undefined && { formDerivation: screen }), actionFormScreenQn: qualifyScreenId(schema.featureName, screen.id),
|
|
2654
2667
|
// Extension-Sections im actionForm sehen die initialen Form-Values
|
|
2655
2668
|
// (inkl. searchParams-Prefill, z.B. ?incidentId=…) als initialValues
|
|
2656
2669
|
// — anders als im entityEdit gibt es hier keinen record, aus dem
|
|
@@ -4,5 +4,6 @@ import type { Translate } from "../headless/index.ts";
|
|
|
4
4
|
import { buildRecordActions } from "./row-actions.ts";
|
|
5
5
|
type ActionBindings = Pick<Parameters<typeof buildRecordActions>[0], "nav" | "host" | "dispatcher" | "openDrawer" | "onWriteSuccess" | "clientActions">;
|
|
6
6
|
export declare function useListEmptyContent(spec: ListEmptyStateSpec | undefined, translate: Translate, bindings: ActionBindings): ListEmptyStateContent | undefined;
|
|
7
|
+
export declare function isDefaultFilterActive(search: string, filters: Readonly<Record<string, readonly string[]>>, defaultFilters: Readonly<Record<string, readonly string[]>>): boolean;
|
|
7
8
|
export declare function isListNarrowed(search: string, filters: Readonly<Record<string, readonly string[]>>, defaultFilters?: Readonly<Record<string, readonly string[]>>): boolean;
|
|
8
9
|
export {};
|
|
@@ -27,6 +27,11 @@ export function useListEmptyContent(spec, translate, bindings) {
|
|
|
27
27
|
};
|
|
28
28
|
}, [spec, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, clientActions]);
|
|
29
29
|
}
|
|
30
|
+
// Empty although the user changed nothing: a default filter hides rows the list would otherwise show.
|
|
31
|
+
export function isDefaultFilterActive(search, filters, defaultFilters) {
|
|
32
|
+
return (!isListNarrowed(search, filters, defaultFilters) &&
|
|
33
|
+
Object.values(defaultFilters).some((values) => values.length > 0));
|
|
34
|
+
}
|
|
30
35
|
// Facets with a default are only "narrowed" once the user moved off that default.
|
|
31
36
|
export function isListNarrowed(search, filters, defaultFilters = {}) {
|
|
32
37
|
if (search.trim() !== "")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityEditScreenDefinition, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { EntityEditScreenDefinition, FeatureSchema, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import type { RenderEditAction } from "../components/render-edit-types.ts";
|
|
3
3
|
import type { ToolbarActionButton } from "../components/render-list.tsx";
|
|
4
4
|
import type { Dispatcher, ListRowViewModel, Translate } from "../headless/index.ts";
|
|
@@ -59,6 +59,12 @@ export declare function buildProjectionToolbarActions(options: {
|
|
|
59
59
|
* entityList/projectionList toolbars have no record and omit this. */
|
|
60
60
|
readonly record?: Readonly<Record<string, unknown>>;
|
|
61
61
|
readonly host: ReturnHost | undefined;
|
|
62
|
+
/** With the user's roles, a navigate action is dropped when the user may not open its target
|
|
63
|
+
* screen. Omitted (embedded or test hosts without a screen registry) keeps every action. */
|
|
64
|
+
readonly access?: {
|
|
65
|
+
readonly appFeatures: readonly FeatureSchema[];
|
|
66
|
+
readonly userRoles: readonly string[] | undefined;
|
|
67
|
+
};
|
|
62
68
|
}): readonly ToolbarActionButton[] | undefined;
|
|
63
69
|
export type DiscardChanges = {
|
|
64
70
|
readonly confirm: string;
|
package/dist/app/row-actions.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
|
3
3
|
import { runClientAction } from "./client-actions.js";
|
|
4
4
|
import { lastSegment } from "./qn.js";
|
|
5
5
|
import { navigateWithReturnTo } from "./return-to.js";
|
|
6
|
+
import { navigateTargetAllows } from "./screen-access.js";
|
|
6
7
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error.js";
|
|
7
8
|
// entityId is explicit: the edit screen may live in another feature than
|
|
8
9
|
// the row source, where the same-feature fallback would miss it.
|
|
@@ -306,7 +307,7 @@ function buildWriteHandlerToolbarAction(action, translate, refetch, dispatcher)
|
|
|
306
307
|
// Shared by entityList, projectionList, and relatedList toolbars so the
|
|
307
308
|
// three call sites can't drift apart.
|
|
308
309
|
export function buildProjectionToolbarActions(options) {
|
|
309
|
-
const { toolbarActions, translate, dispatcher, nav, refetch, openDrawer, navigatePrefill, record, host, } = options;
|
|
310
|
+
const { toolbarActions, translate, dispatcher, nav, refetch, openDrawer, navigatePrefill, record, host, access, } = options;
|
|
310
311
|
if (toolbarActions === undefined)
|
|
311
312
|
return undefined;
|
|
312
313
|
const out = [];
|
|
@@ -317,6 +318,10 @@ export function buildProjectionToolbarActions(options) {
|
|
|
317
318
|
continue;
|
|
318
319
|
}
|
|
319
320
|
if (action.kind === "navigate") {
|
|
321
|
+
if (access !== undefined &&
|
|
322
|
+
!navigateTargetAllows({ screen: action.screen }, access.appFeatures, access.userRoles)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
320
325
|
out.push(buildNavigateToolbarAction(action, translate, nav, host, navigatePrefill, record));
|
|
321
326
|
continue;
|
|
322
327
|
}
|
|
@@ -9,6 +9,7 @@ import { buildInitialValues, mergeSearchParamsIntoInitial, tenantCurrencyMoneyFi
|
|
|
9
9
|
import { hiddenRequiredFieldsFilled, layoutFieldNames } from "./layout-fields.js";
|
|
10
10
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
11
11
|
import { lastSegment } from "./qn.js";
|
|
12
|
+
import { QueryLoadingBanner } from "./query-state-banners.js";
|
|
12
13
|
import { navigateToReturnOr, useReturnTarget } from "./return-to.js";
|
|
13
14
|
function extractRevealValue(data, field) {
|
|
14
15
|
if (typeof data !== "object" || data === null)
|
|
@@ -189,14 +190,14 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
189
190
|
// Hold until a tenant-declared currency lands, else the form would seed (and
|
|
190
191
|
// on a fast click submit) the fallback currency (fw#2933).
|
|
191
192
|
if (confirmCurrencyLoading) {
|
|
192
|
-
return
|
|
193
|
+
return _jsx(QueryLoadingBanner, {});
|
|
193
194
|
}
|
|
194
195
|
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, submitBlocked: acknowledgeBlocked, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
|
|
195
196
|
}
|
|
196
197
|
return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, disabled: acknowledgeBlocked, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
|
|
197
198
|
}
|
|
198
199
|
if (mintCurrencyLoading) {
|
|
199
|
-
return
|
|
200
|
+
return _jsx(QueryLoadingBanner, {});
|
|
200
201
|
}
|
|
201
202
|
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, ...(screen.submitPrefilled === true && {
|
|
202
203
|
submitWithoutChanges: hiddenRequiredFieldsFilled(synthScreen, screen.fields, initial),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { computeDerivedCellValue, currencyDecimals, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "../headless/index.js";
|
|
4
|
+
import { pluralKey } from "../headless/plural-key.js";
|
|
4
5
|
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
5
6
|
import { useQuery } from "../hooks/use-query.js";
|
|
6
7
|
import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
|
|
@@ -158,12 +159,20 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
158
159
|
...(cell.display !== undefined && { display: cell.display }),
|
|
159
160
|
};
|
|
160
161
|
});
|
|
161
|
-
const totals = (field.embeddedListTotals ?? []).map((
|
|
162
|
-
|
|
162
|
+
const totals = (field.embeddedListTotals ?? []).map((entry) => {
|
|
163
|
+
if (typeof entry === "string") {
|
|
164
|
+
const cell = cells.find((c) => c.field === entry);
|
|
165
|
+
return {
|
|
166
|
+
field: entry,
|
|
167
|
+
label: cell?.label ?? entry,
|
|
168
|
+
value: sumEmbeddedListColumn(rows, entry),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
163
171
|
return {
|
|
164
|
-
field:
|
|
165
|
-
label:
|
|
166
|
-
value: sumEmbeddedListColumn(rows,
|
|
172
|
+
field: entry.from.join("+"),
|
|
173
|
+
label: t(entry.label),
|
|
174
|
+
value: entry.from.reduce((sum, name) => sum + sumEmbeddedListColumn(rows, name), 0),
|
|
175
|
+
money: entry.from.every((name) => cells.find((c) => c.field === name)?.type === "money"),
|
|
167
176
|
};
|
|
168
177
|
});
|
|
169
178
|
const { listIssues, rowIssues, cellIssues } = groupEmbeddedListIssues(allIssues, field.field);
|
|
@@ -176,7 +185,7 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
176
185
|
{
|
|
177
186
|
path: field.field,
|
|
178
187
|
code: "paste-rows-truncated",
|
|
179
|
-
i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
|
|
188
|
+
i18nKey: pluralKey("kumiko.field.embedded-list.paste-rows-truncated", pasteWarning.droppedRows),
|
|
180
189
|
params: { count: pasteWarning.droppedRows },
|
|
181
190
|
},
|
|
182
191
|
]
|
|
@@ -186,7 +195,7 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
186
195
|
{
|
|
187
196
|
path: field.field,
|
|
188
197
|
code: "paste-cells-unmatched",
|
|
189
|
-
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
198
|
+
i18nKey: pluralKey("kumiko.field.embedded-list.paste-cells-unmatched", pasteWarning.unmatchedCells),
|
|
190
199
|
params: { count: pasteWarning.unmatchedCells },
|
|
191
200
|
},
|
|
192
201
|
]
|
|
@@ -25,12 +25,16 @@ export function FileListField({ field, id, kind, onChange, hasError, }) {
|
|
|
25
25
|
...(field.accept !== undefined && { accept: field.accept }),
|
|
26
26
|
...(field.maxSize !== undefined && { maxSize: field.maxSize }),
|
|
27
27
|
...(field.entityType !== undefined && { entityType: field.entityType }),
|
|
28
|
+
...(field.uploadActionForm !== undefined && { uploadActionForm: field.uploadActionForm }),
|
|
28
29
|
...(field.entityId !== undefined && { entityId: field.entityId }),
|
|
29
30
|
...(field.fieldName !== undefined && { fieldName: field.fieldName }),
|
|
30
31
|
...(kind === "image" &&
|
|
31
32
|
field.imageVariant !== undefined && { imageVariant: field.imageVariant }),
|
|
32
33
|
...(kind === "image" && field.capture !== undefined && { capture: field.capture }),
|
|
33
34
|
};
|
|
35
|
+
if (field.fileDisplay === "dropzone") {
|
|
36
|
+
return (_jsx(Input, { kind: "files-dropzone", id: id, name: field.field, value: fileIds, onChange: onChange, disabled: disabled, hasError: hasError, testId: `${id}-file-list`, ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.maxCount !== undefined && { maxCount: field.maxCount }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.uploadActionForm !== undefined && { uploadActionForm: field.uploadActionForm }), ...(field.entityId !== undefined && { entityId: field.entityId }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }) }));
|
|
37
|
+
}
|
|
34
38
|
function move(fromIndex, toIndex) {
|
|
35
39
|
const next = [...fileIds];
|
|
36
40
|
const [moved] = next.splice(fromIndex, 1);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FormDerivationScreen } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
/** The derived values that differ from the current form values, or undefined
|
|
3
|
+
* when everything is already up to date. A derived value that no longer
|
|
4
|
+
* resolves is reported as `undefined`. */
|
|
5
|
+
export declare function changedDerivedValues(values: unknown, screen: FormDerivationScreen): Record<string, unknown> | undefined;
|
|
6
|
+
/** `values` with every derived field already resolved, so the form's baseline
|
|
7
|
+
* matches what the live recomputation would produce and starts out clean. */
|
|
8
|
+
export declare function withDerivedValues<TValues extends Record<string, unknown>>(values: TValues, screen: FormDerivationScreen | undefined): TValues;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { applyFormDerivations, isUnchangedValue } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
+
/** The derived values that differ from the current form values, or undefined
|
|
4
|
+
* when everything is already up to date. A derived value that no longer
|
|
5
|
+
* resolves is reported as `undefined`. */
|
|
6
|
+
export function changedDerivedValues(values, screen) {
|
|
7
|
+
const next = applyFormDerivations(values, screen);
|
|
8
|
+
if (!isPlainObject(values) || !isPlainObject(next))
|
|
9
|
+
return undefined;
|
|
10
|
+
const patch = {};
|
|
11
|
+
for (const target of Object.keys(screen.derive ?? {})) {
|
|
12
|
+
if (!isUnchangedValue(values[target], next[target]))
|
|
13
|
+
patch[target] = next[target];
|
|
14
|
+
}
|
|
15
|
+
return Object.keys(patch).length === 0 ? undefined : patch;
|
|
16
|
+
}
|
|
17
|
+
/** `values` with every derived field already resolved, so the form's baseline
|
|
18
|
+
* matches what the live recomputation would produce and starts out clean. */
|
|
19
|
+
export function withDerivedValues(values, screen) {
|
|
20
|
+
if (screen === undefined)
|
|
21
|
+
return values;
|
|
22
|
+
const patch = changedDerivedValues(values, screen);
|
|
23
|
+
return patch === undefined ? values : { ...values, ...patch };
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type FormWidth } from "../primitives.tsx";
|
|
3
|
+
export type FormScreenLayoutProps = {
|
|
4
|
+
readonly children: ReactNode;
|
|
5
|
+
readonly description?: ReactNode;
|
|
6
|
+
readonly actions?: ReactNode;
|
|
7
|
+
readonly secondaryActions?: ReactNode;
|
|
8
|
+
readonly onSubmit?: () => void;
|
|
9
|
+
readonly width?: FormWidth;
|
|
10
|
+
readonly testId?: string;
|
|
11
|
+
};
|
|
12
|
+
/** For custom screens: the standard form screen layout; fill it with `Section` (title, icon, subtitle). */
|
|
13
|
+
export declare function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit, width, testId, }: FormScreenLayoutProps): ReactNode;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives } from "../primitives.js";
|
|
3
|
+
const FORM_SCREEN_LAYOUT_PROPS = {
|
|
4
|
+
screenForm: true,
|
|
5
|
+
fillHeight: true,
|
|
6
|
+
stickyActions: true,
|
|
7
|
+
};
|
|
8
|
+
const NOOP = () => { };
|
|
9
|
+
/** For custom screens: the standard form screen layout; fill it with `Section` (title, icon, subtitle). */
|
|
10
|
+
export function FormScreenLayout({ children, description, actions, secondaryActions, onSubmit = NOOP, width, testId, }) {
|
|
11
|
+
const { Form } = usePrimitives();
|
|
12
|
+
return (_jsx(Form, { onSubmit: onSubmit, ...FORM_SCREEN_LAYOUT_PROPS, ...(description !== undefined && { subtitle: description }), ...(actions !== undefined && { actions }), ...(secondaryActions !== undefined && { secondaryActions }), ...(width !== undefined && { width }), ...(testId !== undefined && { testId }), children: children }));
|
|
13
|
+
}
|
|
@@ -28,6 +28,9 @@ export type GridCellForFieldProps = {
|
|
|
28
28
|
readonly accent?: boolean;
|
|
29
29
|
/** Passed through to RenderField — see RenderFieldProps.fillDisabled. */
|
|
30
30
|
readonly fillDisabled?: boolean;
|
|
31
|
+
/** Title the surrounding section shows. An embedded list whose label equals it
|
|
32
|
+
* keeps its label for screen readers only, so it is not printed twice. */
|
|
33
|
+
readonly sectionTitle?: string;
|
|
31
34
|
};
|
|
32
35
|
export declare function fieldCellWidth(field: EditFieldViewModel): FieldCellWidth;
|
|
33
|
-
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, fillDisabled, }: GridCellForFieldProps): ReactNode;
|
|
36
|
+
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, fillDisabled, sectionTitle, }: GridCellForFieldProps): ReactNode;
|