@cosmicdrift/kumiko-renderer 0.378.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 +49 -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/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 +3 -0
- package/dist/components/render-edit.js +25 -6
- package/dist/components/render-field.d.ts +3 -1
- package/dist/components/render-field.js +19 -7
- 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/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 +4 -0
- package/dist/headless/view-model/edit.js +10 -5
- package/dist/headless/view-model/types.d.ts +10 -2
- 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 +22 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -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 +29 -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/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-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +11 -7
- 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 +62 -29
- 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/upload-zone.d.ts +11 -2
- package/dist/web/widgets/upload-zone.js +30 -7
- package/package.json +5 -5
- package/src/changes.json +104 -0
|
@@ -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
|
}
|
|
@@ -21,6 +21,7 @@ import { useQuery } from "../hooks/use-query.js";
|
|
|
21
21
|
import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
|
|
22
22
|
import { useLocale, useOptionalTimeZone, useTranslation } from "../i18n.js";
|
|
23
23
|
import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
|
|
24
|
+
import { requestLeave } from "../leave-guard.js";
|
|
24
25
|
import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
|
|
25
26
|
import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
|
|
26
27
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
@@ -37,7 +38,7 @@ import { FacetCountBridges } from "./facet-count-bridge.js";
|
|
|
37
38
|
import { buildFormSchema } from "./form-schema.js";
|
|
38
39
|
import { HeaderActionsBar } from "./header-actions-bar.js";
|
|
39
40
|
import { hiddenRequiredFieldsFilled, layoutEditFields, layoutFieldNames } from "./layout-fields.js";
|
|
40
|
-
import { isListNarrowed, useListEmptyContent } from "./list-empty-content.js";
|
|
41
|
+
import { isDefaultFilterActive, isListNarrowed, useListEmptyContent, } from "./list-empty-content.js";
|
|
41
42
|
import { buildDateRangePayload, buildEntityFilterFacets, buildFacetCountRequests, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, normalizeDefaultFilters, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
|
|
42
43
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
43
44
|
import { synthesizeProjectionDetailEntity, synthesizeProjectionDetailScreen, } from "./projection-detail-shim.js";
|
|
@@ -94,7 +95,6 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
94
95
|
return _jsx(ReturnHostProvider, { value: ownHost, children: body });
|
|
95
96
|
}
|
|
96
97
|
function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, children, }) {
|
|
97
|
-
const { Banner } = usePrimitives();
|
|
98
98
|
const result = useQuery(visibleWhen.query, {}, { live: true });
|
|
99
99
|
const wasShown = useRef(false);
|
|
100
100
|
const verdict = evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
@@ -105,7 +105,7 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
|
|
|
105
105
|
if (verdict === "visible")
|
|
106
106
|
return children;
|
|
107
107
|
if (verdict === "loading") {
|
|
108
|
-
return
|
|
108
|
+
return _jsx(QueryLoadingBanner, {});
|
|
109
109
|
}
|
|
110
110
|
if (fallback !== undefined) {
|
|
111
111
|
return _jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate });
|
|
@@ -724,7 +724,6 @@ function EntityEditScreen({ schema, screen, translate, entityId, onCopyLink, })
|
|
|
724
724
|
return (_jsx(EntityEditCreateOrDisabled, { schema: schema, screen: screen, entity: entity, ...(translate !== undefined && { translate }) }));
|
|
725
725
|
}
|
|
726
726
|
function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
727
|
-
const { Banner } = usePrimitives();
|
|
728
727
|
const nav = useNav();
|
|
729
728
|
const handoffValues = useInitialValuesHandoff(screen.id);
|
|
730
729
|
const appFeatures = useAppFeatures();
|
|
@@ -800,7 +799,7 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
800
799
|
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
801
800
|
// its real tenant currency is still in flight (fw#2933) — wait instead.
|
|
802
801
|
if (currencyLoading) {
|
|
803
|
-
return
|
|
802
|
+
return _jsx(QueryLoadingBanner, {});
|
|
804
803
|
}
|
|
805
804
|
// Deliberately no `actions` prop here: `screen.actions` targets an
|
|
806
805
|
// EXISTING record (publish/archive/duplicate and friends), which the
|
|
@@ -813,7 +812,6 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
813
812
|
// `<feature>:write:<entity>:update` on submit. `buildPayload` shapes the
|
|
814
813
|
// snapshot into Kumiko's update-event envelope `{ id, version, changes }`.
|
|
815
814
|
function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onCopyLink, autoReloadOnSave, onDeleted, }) {
|
|
816
|
-
const { Banner } = usePrimitives();
|
|
817
815
|
const t = useTranslation();
|
|
818
816
|
const effectiveTranslate = translate ?? t;
|
|
819
817
|
const detailQn = `${toKebab(schema.featureName)}:query:${toKebab(screen.entity)}:detail`;
|
|
@@ -823,11 +821,13 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
823
821
|
const returnTargetParam = useReturnTarget(screen.id);
|
|
824
822
|
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
825
823
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
826
|
-
return
|
|
824
|
+
return _jsx(QueryLoadingBanner, {});
|
|
827
825
|
}
|
|
828
|
-
if (detailQuery.error) {
|
|
829
|
-
return (_jsx(
|
|
826
|
+
if (detailQuery.error && detailQuery.data === null) {
|
|
827
|
+
return (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch }));
|
|
830
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;
|
|
831
831
|
const record = detailQuery.data;
|
|
832
832
|
if (!record) {
|
|
833
833
|
const onBack = returnTarget !== undefined || findEntityListScreen(schema, screen.entity) !== undefined
|
|
@@ -842,10 +842,9 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
842
842
|
// weiter die stale version stampen und der Konflikt-Recovery wäre
|
|
843
843
|
// kaputt.
|
|
844
844
|
const recordKey = versionOf(record);
|
|
845
|
-
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}`)] }));
|
|
846
846
|
}
|
|
847
847
|
function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
|
|
848
|
-
const { Banner } = usePrimitives();
|
|
849
848
|
// Seed the form with the server values for the entity's declared
|
|
850
849
|
// fields; anything else (id, tenant_id, created_at…) stays out of
|
|
851
850
|
// the form and lives in the closure. The record's `version` is
|
|
@@ -1019,7 +1018,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
1019
1018
|
// stored (non-empty) value is unaffected — it never reaches this branch's
|
|
1020
1019
|
// fallback because `initial` above already prefers `record[name]`.
|
|
1021
1020
|
if (currencyLoading) {
|
|
1022
|
-
return
|
|
1021
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1023
1022
|
}
|
|
1024
1023
|
// Evaluated against the stored record only, so typing in a form can never
|
|
1025
1024
|
// flip the screen between editable and view.
|
|
@@ -1045,23 +1044,24 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
1045
1044
|
// existing record via list(limit:1) before deciding create vs update —
|
|
1046
1045
|
// otherwise every visit without an id would create another record.
|
|
1047
1046
|
function EntityEditSingletonBody({ schema, screen, entity, translate, onCopyLink, }) {
|
|
1048
|
-
const { Banner } = usePrimitives();
|
|
1049
1047
|
const t = useTranslation();
|
|
1050
1048
|
const effectiveTranslate = translate ?? t;
|
|
1051
1049
|
const listQn = entityQueryCommand(schema.featureName, screen.entity, "list");
|
|
1052
1050
|
const listQuery = useQuery(listQn, { limit: 1 });
|
|
1053
1051
|
if (listQuery.loading && listQuery.data === null) {
|
|
1054
|
-
return
|
|
1052
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1055
1053
|
}
|
|
1056
|
-
if (listQuery.error) {
|
|
1057
|
-
return (_jsx(
|
|
1054
|
+
if (listQuery.error && listQuery.data === null) {
|
|
1055
|
+
return (_jsx(QueryErrorBanner, { error: listQuery.error, translate: effectiveTranslate, onRetry: listQuery.refetch }));
|
|
1058
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;
|
|
1059
1059
|
const rawExistingId = listQuery.data?.rows[0]?.["id"];
|
|
1060
1060
|
const existingId = typeof rawExistingId === "string" ? rawExistingId : undefined;
|
|
1061
1061
|
if (existingId !== undefined) {
|
|
1062
|
-
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() })] }));
|
|
1063
1063
|
}
|
|
1064
|
-
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() })] }));
|
|
1065
1065
|
}
|
|
1066
1066
|
// Shared no-entityId tail for both the plain create path and the
|
|
1067
1067
|
// singleton path's empty-table fallback: `allowCreate: false` blocks
|
|
@@ -1248,7 +1248,6 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1248
1248
|
const featureName = schema.featureName;
|
|
1249
1249
|
const resolvedCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
|
|
1250
1250
|
const onCreate = screen.allowCreate === false ? undefined : resolvedCreate;
|
|
1251
|
-
const { Banner } = usePrimitives();
|
|
1252
1251
|
const queryType = entityQueryCommand(featureName, screen.entity, "list");
|
|
1253
1252
|
const nav = useNav();
|
|
1254
1253
|
const host = useReturnHost();
|
|
@@ -1572,6 +1571,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1572
1571
|
refetch: refreshRowsAfterWrite,
|
|
1573
1572
|
openDrawer,
|
|
1574
1573
|
host,
|
|
1574
|
+
access: { appFeatures, userRoles },
|
|
1575
1575
|
}), [
|
|
1576
1576
|
screen.toolbarActions,
|
|
1577
1577
|
effectiveTranslate,
|
|
@@ -1580,6 +1580,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1580
1580
|
dispatcher,
|
|
1581
1581
|
refreshRowsAfterWrite,
|
|
1582
1582
|
openDrawer,
|
|
1583
|
+
appFeatures,
|
|
1584
|
+
userRoles,
|
|
1583
1585
|
]);
|
|
1584
1586
|
const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
|
|
1585
1587
|
nav,
|
|
@@ -1619,11 +1621,13 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1619
1621
|
invalidateExpansions,
|
|
1620
1622
|
]);
|
|
1621
1623
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1622
|
-
return
|
|
1624
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1623
1625
|
}
|
|
1624
|
-
if (rowsQuery.error) {
|
|
1625
|
-
return (_jsx(
|
|
1626
|
+
if (rowsQuery.error && rowsQuery.data === null) {
|
|
1627
|
+
return (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch }));
|
|
1626
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;
|
|
1627
1631
|
// RenderList's onRowClick is a 1-arg callback; KumikoScreen's
|
|
1628
1632
|
// 2-arg shape (row, entityName) is the public surface — thread the
|
|
1629
1633
|
// screen's entity through here so callers don't have to re-derive
|
|
@@ -1666,7 +1670,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1666
1670
|
// Bei Infinite-Mode: rows kommen aus accumulated (über mehrere
|
|
1667
1671
|
// useQuery-Calls gesammelt), bei pages/false aus dem aktuellen Result.
|
|
1668
1672
|
const renderRows = useInfinite ? accumulated : (rowsQuery.data?.rows ?? []);
|
|
1669
|
-
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 && {
|
|
1670
1674
|
description: effectiveTranslate(screen.description),
|
|
1671
1675
|
}), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: effectiveSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1672
1676
|
? { screenPadding: false, chromeless: true }
|
|
@@ -1838,6 +1842,7 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1838
1842
|
refetch: rowsQuery.refetch,
|
|
1839
1843
|
openDrawer,
|
|
1840
1844
|
host,
|
|
1845
|
+
access: { appFeatures, userRoles },
|
|
1841
1846
|
}), [
|
|
1842
1847
|
screen.toolbarActions,
|
|
1843
1848
|
effectiveTranslate,
|
|
@@ -1846,6 +1851,8 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1846
1851
|
dispatcher,
|
|
1847
1852
|
rowsQuery.refetch,
|
|
1848
1853
|
openDrawer,
|
|
1854
|
+
appFeatures,
|
|
1855
|
+
userRoles,
|
|
1849
1856
|
]);
|
|
1850
1857
|
const emptyContent = useListEmptyContent(screen.emptyState, effectiveTranslate, {
|
|
1851
1858
|
nav,
|
|
@@ -1855,11 +1862,13 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1855
1862
|
onWriteSuccess: rowsQuery.refetch,
|
|
1856
1863
|
});
|
|
1857
1864
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1858
|
-
return
|
|
1865
|
+
return _jsx(QueryLoadingBanner, {});
|
|
1859
1866
|
}
|
|
1860
|
-
if (rowsQuery.error) {
|
|
1861
|
-
return (_jsx(
|
|
1867
|
+
if (rowsQuery.error && rowsQuery.data === null) {
|
|
1868
|
+
return (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch }));
|
|
1862
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;
|
|
1863
1872
|
// rowsQuery.data is typed as PagedRows but the query handler is free to
|
|
1864
1873
|
// return anything at runtime (fw#2216: a bare array silently rendered an
|
|
1865
1874
|
// empty table instead of surfacing the mismatch); a system-boundary cast
|
|
@@ -1890,7 +1899,7 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1890
1899
|
onPageSizeChange: urlState.setPageSize,
|
|
1891
1900
|
}
|
|
1892
1901
|
: undefined;
|
|
1893
|
-
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 && {
|
|
1894
1903
|
description: effectiveTranslate(screen.description),
|
|
1895
1904
|
}), ...(emptyContent !== undefined && { emptyContent }), searchValue: urlState.q, onSearchChange: urlState.setQ, sort: activeSort, onSortChange: urlState.setSort, ...(isEmbeddedScreen
|
|
1896
1905
|
? { screenPadding: false, chromeless: true }
|
|
@@ -2181,13 +2190,13 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2181
2190
|
host,
|
|
2182
2191
|
dispatcher,
|
|
2183
2192
|
openDrawer,
|
|
2184
|
-
onWriteSuccess:
|
|
2193
|
+
onWriteSuccess: reloadDetail,
|
|
2185
2194
|
onRecordLeft: handleRecordLeft,
|
|
2186
2195
|
sameEntityScreenId,
|
|
2187
2196
|
clientActions,
|
|
2188
2197
|
}), [
|
|
2189
2198
|
detailQuery.data,
|
|
2190
|
-
|
|
2199
|
+
reloadDetail,
|
|
2191
2200
|
effectiveTranslate,
|
|
2192
2201
|
nav,
|
|
2193
2202
|
host,
|
|
@@ -2207,7 +2216,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2207
2216
|
host,
|
|
2208
2217
|
dispatcher,
|
|
2209
2218
|
openDrawer,
|
|
2210
|
-
onWriteSuccess:
|
|
2219
|
+
onWriteSuccess: reloadDetail,
|
|
2211
2220
|
onRecordLeft: handleRecordLeft,
|
|
2212
2221
|
sameEntityScreenId,
|
|
2213
2222
|
clientActions,
|
|
@@ -2224,7 +2233,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2224
2233
|
dispatcher,
|
|
2225
2234
|
clientActions,
|
|
2226
2235
|
detailQuery.data,
|
|
2227
|
-
|
|
2236
|
+
reloadDetail,
|
|
2228
2237
|
openDrawer,
|
|
2229
2238
|
handleRecordLeft,
|
|
2230
2239
|
sameEntityScreenId,
|
|
@@ -2249,11 +2258,13 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2249
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."] }));
|
|
2250
2259
|
}
|
|
2251
2260
|
if (detailQuery.loading && detailQuery.data === null) {
|
|
2252
|
-
return
|
|
2261
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2253
2262
|
}
|
|
2254
|
-
if (detailQuery.error) {
|
|
2255
|
-
return (_jsx(
|
|
2263
|
+
if (detailQuery.error && detailQuery.data === null) {
|
|
2264
|
+
return (_jsx(QueryErrorBanner, { error: detailQuery.error, translate: effectiveTranslate, onRetry: detailQuery.refetch }));
|
|
2256
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;
|
|
2257
2268
|
const record = detailQuery.data;
|
|
2258
2269
|
if (!record) {
|
|
2259
2270
|
const backListScreen = screen.listScreenId ??
|
|
@@ -2337,8 +2348,8 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2337
2348
|
label: effectiveTranslate(section.title ?? section.id ?? ""),
|
|
2338
2349
|
...(count !== undefined && { count }),
|
|
2339
2350
|
};
|
|
2340
|
-
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
2341
|
-
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 ||
|
|
2342
2353
|
hasTabs ||
|
|
2343
2354
|
screen.slots?.header !== undefined ||
|
|
2344
2355
|
screen.recordTitleField !== undefined) && {
|
|
@@ -2644,7 +2655,7 @@ function ActionFormBody({ schema, screen, translate, entityId, initialOverrides,
|
|
|
2644
2655
|
// Rendering the form before a tenant-declared currency lands would show the
|
|
2645
2656
|
// fallback and, on a fast click, submit it (fw#2933) — hold instead.
|
|
2646
2657
|
if (currencyLoading) {
|
|
2647
|
-
return
|
|
2658
|
+
return _jsx(QueryLoadingBanner, {});
|
|
2648
2659
|
}
|
|
2649
2660
|
if (currencyUnresolved) {
|
|
2650
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." }));
|
|
@@ -2652,7 +2663,7 @@ function ActionFormBody({ schema, screen, translate, entityId, initialOverrides,
|
|
|
2652
2663
|
if (loadState !== undefined && loadState.status !== "ready") {
|
|
2653
2664
|
return (_jsx(ActionFormLoadStatus, { state: loadState, screenId: screen.id, translate: effectiveTranslate }));
|
|
2654
2665
|
}
|
|
2655
|
-
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: derivedInitial, ...(screen.derive !== undefined && { formDerivation: screen }),
|
|
2666
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: derivedInitial, ...(screen.derive !== undefined && { formDerivation: screen }), actionFormScreenQn: qualifyScreenId(schema.featureName, screen.id),
|
|
2656
2667
|
// Extension-Sections im actionForm sehen die initialen Form-Values
|
|
2657
2668
|
// (inkl. searchParams-Prefill, z.B. ?incidentId=…) als initialValues
|
|
2658
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);
|
|
@@ -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;
|
|
@@ -29,10 +29,14 @@ 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, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, fillDisabled, }) {
|
|
32
|
+
export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, flow, layout, accent, fillDisabled, sectionTitle, }) {
|
|
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
|
+
const hideLabel = sectionTitle !== undefined &&
|
|
37
|
+
field.type === "embedded" &&
|
|
38
|
+
field.embeddedListCells !== undefined &&
|
|
39
|
+
field.label === sectionTitle;
|
|
36
40
|
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 }), ...(appendixPlacement !== undefined && { appendixPlacement }), ...(description !== undefined && { description }), ...(status !== undefined && { status }), allIssues: allIssues, valueDisplay: valueDisplay, row: row, ...(changed === true && { changed }), ...(layout !== undefined && { layout }), ...(accent === true && { accent }), ...(fillDisabled === true && { fillDisabled }) }) }));
|
|
41
|
+
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 }), ...(fillDisabled === true && { fillDisabled }), ...(hideLabel && { hideLabel }) }) }));
|
|
38
42
|
}
|
|
@@ -5,10 +5,13 @@ export type ListEmptyStateContent = {
|
|
|
5
5
|
readonly description?: string;
|
|
6
6
|
readonly action?: RenderEditAction;
|
|
7
7
|
};
|
|
8
|
-
export declare function ListEmptyStateBody({ content, narrowed, createButton, translate, }: {
|
|
8
|
+
export declare function ListEmptyStateBody({ content, narrowed, defaultFilterActive, onShowAll, createButton, translate, }: {
|
|
9
9
|
readonly content: ListEmptyStateContent | undefined;
|
|
10
10
|
/** A search term or changed filter produced the empty result, so "create the first one" would mislead. */
|
|
11
11
|
readonly narrowed: boolean;
|
|
12
|
+
/** A default filter is on and nothing else narrows: rows may exist outside it. */
|
|
13
|
+
readonly defaultFilterActive?: boolean;
|
|
14
|
+
readonly onShowAll?: () => void;
|
|
12
15
|
readonly createButton: ReactNode;
|
|
13
16
|
readonly translate: (key: string) => string;
|
|
14
17
|
}): ReactNode;
|
|
@@ -2,11 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { usePrimitives } from "../primitives.js";
|
|
4
4
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
|
-
export function ListEmptyStateBody({ content, narrowed, createButton, translate, }) {
|
|
5
|
+
export function ListEmptyStateBody({ content, narrowed, defaultFilterActive = false, onShowAll, createButton, translate, }) {
|
|
6
6
|
const { Text, Button, Dialog, Banner } = usePrimitives();
|
|
7
7
|
const [actionError, setActionError] = useState(null);
|
|
8
8
|
if (narrowed)
|
|
9
9
|
return _jsx(Text, { children: translate("kumiko.list.empty.filtered") });
|
|
10
|
+
if (defaultFilterActive) {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: translate("kumiko.list.empty.defaultFiltered") }), onShowAll !== undefined && (_jsx(Button, { variant: "secondary", onClick: onShowAll, testId: "render-list-empty-show-all", children: translate("kumiko.list.empty.showAll") }))] }));
|
|
12
|
+
}
|
|
10
13
|
const description = content === undefined ? translate("kumiko.list.empty.hint") : content.description;
|
|
11
14
|
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: content?.title ?? translate("kumiko.list.empty.title") }), description !== undefined && _jsx(Text, { variant: "small", children: description }), content?.action !== undefined && (_jsx(RenderEditActionButton, { action: content.action, Button: Button, Dialog: Dialog, onError: setActionError })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-list-empty-state-action-error", children: actionError })), createButton] }));
|
|
12
15
|
}
|
|
@@ -48,5 +48,5 @@ export function ReferenceCreateDialog({ open, onClose, onCreated, featureName, s
|
|
|
48
48
|
};
|
|
49
49
|
if (!open)
|
|
50
50
|
return null;
|
|
51
|
-
return (_jsx(Modal, { open: open, onOpenChange: (next) => !next && onClose(), title: t("kumiko.actions.create"), children: currencyLoading ? (_jsx(Banner, { padded: true, variant: "loading", testId: "reference-create-dialog-loading", children: "
|
|
51
|
+
return (_jsx(Modal, { open: open, onOpenChange: (next) => !next && onClose(), title: t("kumiko.actions.create"), children: currencyLoading ? (_jsx(Banner, { padded: true, variant: "loading", testId: "reference-create-dialog-loading", children: t("kumiko.widget.loading") })) : (_jsx(RenderEdit, { screen: screen, entity: entity, featureName: featureName, initial: initial, writeCommand: writeCommand, onSubmit: handleSubmitted, onCancel: onClose, ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) })) }));
|
|
52
52
|
}
|