@cosmicdrift/kumiko-renderer 0.330.0 → 0.331.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/entity-list-expanded-row.d.ts +15 -0
- package/dist/app/entity-list-expanded-row.js +49 -0
- package/dist/app/kumiko-screen.js +87 -29
- package/dist/components/grid-cell-for-field.js +1 -1
- package/dist/components/query-options-select.d.ts +12 -0
- package/dist/components/query-options-select.js +21 -0
- package/dist/components/related-list-section.d.ts +9 -1
- package/dist/components/related-list-section.js +48 -21
- package/dist/components/render-edit.d.ts +2 -0
- package/dist/components/render-edit.js +22 -16
- package/dist/components/render-field.js +2 -1
- package/dist/components/render-list.d.ts +5 -0
- package/dist/components/render-list.js +5 -4
- package/dist/components/write-form-section.js +3 -4
- package/dist/i18n-defaults.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +7 -0
- package/dist/primitives.d.ts +17 -1
- package/package.json +5 -5
- package/src/changes.json +26 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { EditRelatedListSectionViewModel, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export declare function EntityListExpandedRow({ section, row, featureName, translate, expansionNonce, openDrawer, onAfterWrite, onSubListInvalidated, }: {
|
|
5
|
+
readonly section: EditRelatedListSectionViewModel;
|
|
6
|
+
readonly row: ListRowViewModel;
|
|
7
|
+
readonly featureName: string;
|
|
8
|
+
readonly translate?: Translate;
|
|
9
|
+
readonly expansionNonce: number;
|
|
10
|
+
readonly openDrawer: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
11
|
+
/** Reloads the parent list after a write in the sub-list. */
|
|
12
|
+
readonly onAfterWrite: () => void | Promise<void>;
|
|
13
|
+
/** Asks the host to remount every open sub-list. */
|
|
14
|
+
readonly onSubListInvalidated: () => void;
|
|
15
|
+
}): ReactNode;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { RelatedListSection } from "../components/related-list-section.js";
|
|
4
|
+
import { RenderEditActionButton } from "../components/render-edit-action-button.js";
|
|
5
|
+
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
6
|
+
import { useTranslation } from "../i18n.js";
|
|
7
|
+
import { usePrimitives } from "../primitives.js";
|
|
8
|
+
import { useNav } from "./nav.js";
|
|
9
|
+
import { useReturnHost } from "./return-to.js";
|
|
10
|
+
import { buildRecordActions } from "./row-actions.js";
|
|
11
|
+
// Content of an entityList row's expansion area: the row's record is the
|
|
12
|
+
// parent of a relatedList. `expansionNonce` in the key remounts the sub-list
|
|
13
|
+
// after a write that ran outside of it (drawer submit, title-row action).
|
|
14
|
+
export function EntityListExpandedRow({ section, row, featureName, translate, expansionNonce, openDrawer, onAfterWrite, onSubListInvalidated, }) {
|
|
15
|
+
const { Banner, Button, Dialog } = usePrimitives();
|
|
16
|
+
const t = useTranslation();
|
|
17
|
+
const effectiveTranslate = translate ?? t;
|
|
18
|
+
const nav = useNav();
|
|
19
|
+
const host = useReturnHost();
|
|
20
|
+
const dispatcher = useOptionalDispatcher();
|
|
21
|
+
const [actionError, setActionError] = useState(null);
|
|
22
|
+
const onTitleActionWritten = useCallback(async () => {
|
|
23
|
+
onSubListInvalidated();
|
|
24
|
+
await onAfterWrite();
|
|
25
|
+
}, [onSubListInvalidated, onAfterWrite]);
|
|
26
|
+
const titleActions = useMemo(() => section.actions !== undefined && section.actions.length > 0
|
|
27
|
+
? buildRecordActions({
|
|
28
|
+
actions: section.actions,
|
|
29
|
+
record: row.values,
|
|
30
|
+
translate: effectiveTranslate,
|
|
31
|
+
nav,
|
|
32
|
+
host,
|
|
33
|
+
dispatcher,
|
|
34
|
+
openDrawer,
|
|
35
|
+
onWriteSuccess: onTitleActionWritten,
|
|
36
|
+
})
|
|
37
|
+
: undefined, [
|
|
38
|
+
section.actions,
|
|
39
|
+
row.values,
|
|
40
|
+
effectiveTranslate,
|
|
41
|
+
nav,
|
|
42
|
+
host,
|
|
43
|
+
dispatcher,
|
|
44
|
+
openDrawer,
|
|
45
|
+
onTitleActionWritten,
|
|
46
|
+
]);
|
|
47
|
+
const titleActionButtons = titleActions?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: setActionError }, action.id)));
|
|
48
|
+
return (_jsxs(_Fragment, { children: [actionError !== null && (_jsx(Banner, { variant: "error", testId: `row-${row.id}-expansion-action-error`, children: actionError })), _jsx(RelatedListSection, { embedded: true, section: section, parentId: String(row.values["id"] ?? row.id), record: row.values, featureName: featureName, translate: effectiveTranslate, onOpenDrawer: openDrawer, onAfterWrite: onAfterWrite, ...(titleActionButtons !== undefined && { actions: titleActionButtons }) }, expansionNonce)] }));
|
|
49
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { evalFieldCondition, isWriteFormEditSection, metricField, parseRefTarget, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import { fieldLabelKey, fieldOptionLabelKey, isSafeHref } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { computeRelatedListSectionViewModel, fieldLabelKey, fieldOptionLabelKey, isSafeHref, } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
5
5
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
8
|
-
import { RenderEdit, } from "../components/render-edit.js";
|
|
8
|
+
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
9
9
|
import { needsActionConfirm, RenderEditActionButton, RenderEditActionConfirmDialog, } from "../components/render-edit-action-button.js";
|
|
10
10
|
import { RenderList } from "../components/render-list.js";
|
|
11
11
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
@@ -14,7 +14,7 @@ import { PAGE_SIZE_OPTIONS, useListUrlState } from "../hooks/use-list-url-state.
|
|
|
14
14
|
import { useQuery } from "../hooks/use-query.js";
|
|
15
15
|
import { useLocale, useOptionalTimeZone, useTranslation } from "../i18n.js";
|
|
16
16
|
import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
|
|
17
|
-
import { PageHeaderSlotAvailableProvider, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
|
|
17
|
+
import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
|
|
18
18
|
import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
|
|
19
19
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
20
20
|
import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim.js";
|
|
@@ -22,6 +22,7 @@ import { useAppFeatures } from "./app-features-context.js";
|
|
|
22
22
|
import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "./config-edit-shim.js";
|
|
23
23
|
import { useCustomScreenComponent } from "./custom-screens.js";
|
|
24
24
|
import { useDashboardBody } from "./dashboard-body.js";
|
|
25
|
+
import { EntityListExpandedRow } from "./entity-list-expanded-row.js";
|
|
25
26
|
import { buildFormSchema } from "./form-schema.js";
|
|
26
27
|
import { layoutFieldNames } from "./layout-fields.js";
|
|
27
28
|
import { buildDateRangePayload, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
|
|
@@ -1281,6 +1282,36 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1281
1282
|
refreshRowsAfterWrite,
|
|
1282
1283
|
openDrawer,
|
|
1283
1284
|
]);
|
|
1285
|
+
// Expandable rows: open state is keyed by row id, so it survives refetch,
|
|
1286
|
+
// sort and page changes. Several rows can be open at once.
|
|
1287
|
+
const expandableRow = screen.expandableRow;
|
|
1288
|
+
const expandableSection = useMemo(() => expandableRow !== undefined
|
|
1289
|
+
? computeRelatedListSectionViewModel(expandableRow, effectiveTranslate)
|
|
1290
|
+
: undefined, [expandableRow, effectiveTranslate]);
|
|
1291
|
+
const [expandedRowIds, setExpandedRowIds] = useState(() => new Set());
|
|
1292
|
+
const toggleRowExpanded = useCallback((rowId) => {
|
|
1293
|
+
setExpandedRowIds((prev) => {
|
|
1294
|
+
const next = new Set(prev);
|
|
1295
|
+
if (next.has(rowId))
|
|
1296
|
+
next.delete(rowId);
|
|
1297
|
+
else
|
|
1298
|
+
next.add(rowId);
|
|
1299
|
+
return next;
|
|
1300
|
+
});
|
|
1301
|
+
}, []);
|
|
1302
|
+
// Bumped after a write that ran outside the sub-list (drawer submit,
|
|
1303
|
+
// title-row action) so open sub-lists remount and reload.
|
|
1304
|
+
const [expansionNonce, setExpansionNonce] = useState(0);
|
|
1305
|
+
const invalidateExpansions = useCallback(() => setExpansionNonce((n) => n + 1), []);
|
|
1306
|
+
const renderExpandedRow = useCallback((row) => expandableSection === undefined ? null : (_jsx(EntityListExpandedRow, { section: expandableSection, row: row, featureName: featureName, ...(translate !== undefined && { translate }), expansionNonce: expansionNonce, openDrawer: openDrawer, onAfterWrite: refreshRowsAfterWrite, onSubListInvalidated: invalidateExpansions })), [
|
|
1307
|
+
expandableSection,
|
|
1308
|
+
featureName,
|
|
1309
|
+
translate,
|
|
1310
|
+
expansionNonce,
|
|
1311
|
+
openDrawer,
|
|
1312
|
+
refreshRowsAfterWrite,
|
|
1313
|
+
invalidateExpansions,
|
|
1314
|
+
]);
|
|
1284
1315
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1285
1316
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
1286
1317
|
}
|
|
@@ -1336,8 +1367,13 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1336
1367
|
filterValues: urlState.filters,
|
|
1337
1368
|
onFilterChange: urlState.setFilter,
|
|
1338
1369
|
onFilterReset: urlState.clearFilters,
|
|
1370
|
+
}), ...(expandableSection !== undefined && {
|
|
1371
|
+
expandedRowIds,
|
|
1372
|
+
onToggleRowExpanded: toggleRowExpanded,
|
|
1373
|
+
renderExpandedRow,
|
|
1339
1374
|
}) }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
1340
1375
|
closeDrawer();
|
|
1376
|
+
invalidateExpansions();
|
|
1341
1377
|
void rowsQuery.refetch();
|
|
1342
1378
|
} })] }));
|
|
1343
1379
|
}
|
|
@@ -1623,7 +1659,7 @@ function primaryHeaderAction(actions) {
|
|
|
1623
1659
|
// state for menu items: RenderEditActionButton already owns a per-button
|
|
1624
1660
|
// confirm dialog for the always-visible primary action, but a menu item has
|
|
1625
1661
|
// no button of its own to carry one.
|
|
1626
|
-
function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu, onError, collapseAfterPrimary = false, }) {
|
|
1662
|
+
function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu, onError, collapseAfterPrimary = false, compact = false, PageHeader, }) {
|
|
1627
1663
|
const t = useTranslation();
|
|
1628
1664
|
const [pendingAction, setPendingAction] = useState(null);
|
|
1629
1665
|
const trigger = async (action) => {
|
|
@@ -1635,38 +1671,51 @@ function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu, onError
|
|
|
1635
1671
|
onError(e instanceof Error ? e.message : String(e));
|
|
1636
1672
|
}
|
|
1637
1673
|
};
|
|
1674
|
+
const toMenuItem = (action) => ({
|
|
1675
|
+
id: action.id,
|
|
1676
|
+
label: action.label,
|
|
1677
|
+
...(action.icon !== undefined && { icon: action.icon }),
|
|
1678
|
+
variant: action.style === "danger" ? "danger" : "default",
|
|
1679
|
+
onSelect: () => {
|
|
1680
|
+
if (needsActionConfirm(action)) {
|
|
1681
|
+
setPendingAction(action);
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
void trigger(action);
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
});
|
|
1688
|
+
const pendingActionDialog = pendingAction !== null && (_jsx(RenderEditActionConfirmDialog, { action: pendingAction, open: true, onOpenChange: (open) => {
|
|
1689
|
+
if (!open)
|
|
1690
|
+
setPendingAction(null);
|
|
1691
|
+
}, onConfirm: async () => {
|
|
1692
|
+
const action = pendingAction;
|
|
1693
|
+
setPendingAction(null);
|
|
1694
|
+
await trigger(action);
|
|
1695
|
+
}, Dialog: Dialog }));
|
|
1696
|
+
if (compact && PageHeader !== undefined) {
|
|
1697
|
+
const primary = primaryHeaderAction(actions);
|
|
1698
|
+
// A bare icon for a destructive or confirm-gated action (e.g. "x" for terminate)
|
|
1699
|
+
// reads as "close"; such actions go to the labelled menu instead.
|
|
1700
|
+
const iconPrimary = primary?.icon !== undefined && primary.style !== "danger" && !needsActionConfirm(primary)
|
|
1701
|
+
? primary
|
|
1702
|
+
: undefined;
|
|
1703
|
+
const overflowActions = actions.filter((a) => a !== iconPrimary);
|
|
1704
|
+
return (_jsxs(_Fragment, { children: [iconPrimary !== undefined && (_jsx(RenderEditActionButton, { action: iconPrimary, Button: Button, Dialog: Dialog, iconOnly: true, onError: onError }, iconPrimary.id)), overflowActions.length > 0 && (_jsx(PageHeader, { overflowItems: overflowActions.map(toMenuItem) })), pendingActionDialog] }));
|
|
1705
|
+
}
|
|
1638
1706
|
const maxPlainButtons = collapseAfterPrimary ? 1 : 2;
|
|
1639
1707
|
if (actions.length <= maxPlainButtons || ActionOverflowMenu === undefined) {
|
|
1640
1708
|
return (_jsx(_Fragment, { children: actions.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, hideIcon: true, onError: onError }, action.id))) }));
|
|
1641
1709
|
}
|
|
1642
1710
|
const primary = primaryHeaderAction(actions);
|
|
1643
1711
|
const rest = actions.filter((a) => a.id !== primary?.id);
|
|
1644
|
-
return (_jsxs(_Fragment, { children: [primary !== undefined && (_jsx(RenderEditActionButton, { action: primary, Button: Button, Dialog: Dialog, hideIcon: true, onError: onError }, primary.id)), _jsx(ActionOverflowMenu, { label: t("kumiko.list.row-actions.more"), testId: "kumiko-screen-projection-detail-actions-overflow", items: rest.map(
|
|
1645
|
-
id: action.id,
|
|
1646
|
-
label: action.label,
|
|
1647
|
-
...(action.icon !== undefined && { icon: action.icon }),
|
|
1648
|
-
variant: action.style === "danger" ? "danger" : "default",
|
|
1649
|
-
onSelect: () => {
|
|
1650
|
-
if (needsActionConfirm(action)) {
|
|
1651
|
-
setPendingAction(action);
|
|
1652
|
-
}
|
|
1653
|
-
else {
|
|
1654
|
-
void trigger(action);
|
|
1655
|
-
}
|
|
1656
|
-
},
|
|
1657
|
-
})) }), pendingAction !== null && (_jsx(RenderEditActionConfirmDialog, { action: pendingAction, open: true, onOpenChange: (open) => {
|
|
1658
|
-
if (!open)
|
|
1659
|
-
setPendingAction(null);
|
|
1660
|
-
}, onConfirm: async () => {
|
|
1661
|
-
const action = pendingAction;
|
|
1662
|
-
setPendingAction(null);
|
|
1663
|
-
await trigger(action);
|
|
1664
|
-
}, Dialog: Dialog }))] }));
|
|
1712
|
+
return (_jsxs(_Fragment, { children: [primary !== undefined && (_jsx(RenderEditActionButton, { action: primary, Button: Button, Dialog: Dialog, hideIcon: true, onError: onError }, primary.id)), _jsx(ActionOverflowMenu, { label: t("kumiko.list.row-actions.more"), testId: "kumiko-screen-projection-detail-actions-overflow", items: rest.map(toMenuItem) }), pendingActionDialog] }));
|
|
1665
1713
|
}
|
|
1666
1714
|
function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
1667
1715
|
const { Banner, Button, Dialog, Text, Grid, GridCell, Card, Tabs, StatusBadge, Metric, MetricBand, PageHeader, Link, ActionOverflowMenu, } = usePrimitives();
|
|
1668
1716
|
const t = useTranslation();
|
|
1669
1717
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
1718
|
+
const pageHeaderCompactContext = usePageHeaderCompact();
|
|
1670
1719
|
const effectiveTranslate = translate ?? t;
|
|
1671
1720
|
const nav = useNav();
|
|
1672
1721
|
const idParam = screen.idParam ?? "id";
|
|
@@ -1843,6 +1892,9 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1843
1892
|
// this shares the same page padding/width as the record's edit card below
|
|
1844
1893
|
// it, instead of sitting flush against the screen edge (fw record-screen
|
|
1845
1894
|
// header polish).
|
|
1895
|
+
const recordTitle = screen.recordTitleField !== undefined
|
|
1896
|
+
? resolveRecordTitle(record[screen.recordTitleField])
|
|
1897
|
+
: undefined;
|
|
1846
1898
|
const header = screen.header;
|
|
1847
1899
|
const subtitleHref = resolveSubtitleHref(header, record);
|
|
1848
1900
|
// Plain Text, not Heading — the Card title slot already renders as its own
|
|
@@ -1866,16 +1918,19 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1866
1918
|
// Metrics are a dl band when the primitive exists; the old Grid stays as the fallback.
|
|
1867
1919
|
const metricsBlock = (subtitle, lead) => MetricBand !== undefined ? (_jsx(MetricBand, { testId: "kumiko-screen-projection-detail-metrics", subtitle: subtitle, ...(lead !== undefined && { lead }), children: metricItems })) : (_jsx(Grid, { columns: screen.metrics?.length ?? 1, testId: "kumiko-screen-projection-detail-metrics", children: metricItems }));
|
|
1868
1920
|
const usesPageHeaderSlot = pageHeaderSlotAvailable && PageHeader !== undefined;
|
|
1921
|
+
const pageHeaderCompact = pageHeaderCompactContext && usesPageHeaderSlot;
|
|
1869
1922
|
const actionErrorBanner = actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError }));
|
|
1870
|
-
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header?.status !== undefined &&
|
|
1923
|
+
const renderHeaderContent = (headerSlot) => (_jsxs(_Fragment, { children: [usesPageHeaderSlot ? (_jsxs(_Fragment, { children: [_jsx(PageHeader, { ...(header !== undefined && { title: String(record[header.title] ?? "") }), ...(header === undefined && recordTitle !== undefined && { title: recordTitle }), ...(header?.status !== undefined &&
|
|
1871
1924
|
StatusBadge !== undefined && {
|
|
1872
1925
|
status: (_jsx(StatusBadge, { value: String(record[header.status] ?? ""), tone: headerStatusTone(header, record), testId: "kumiko-screen-projection-detail-status" })),
|
|
1873
1926
|
}), ...(hasHeaderActions && {
|
|
1874
|
-
actions: (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: true }) })),
|
|
1927
|
+
actions: (_jsx(Grid, { columns: "auto", testId: "kumiko-screen-projection-detail-actions", children: _jsx(HeaderActionsBar, { actions: headerActionsList, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: true, compact: pageHeaderCompact, PageHeader: PageHeader }) })),
|
|
1875
1928
|
}) }), MetricBand === undefined && headerSlot, (hasMetrics || headerSubtitleSlot !== undefined || headerSlot !== undefined) &&
|
|
1876
1929
|
(MetricBand !== undefined || hasMetrics) &&
|
|
1877
|
-
metricsBlock(headerSubtitleSlot, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined) && (_jsxs(Card, { slots: {
|
|
1930
|
+
metricsBlock(headerSubtitleSlot, MetricBand !== undefined ? headerSlot : undefined), actionErrorBanner] })) : ((hasHeaderCard || headerSlot !== undefined || recordTitle !== undefined) && (_jsxs(Card, { slots: {
|
|
1878
1931
|
...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
|
|
1932
|
+
...(headerTitleSlot === undefined &&
|
|
1933
|
+
recordTitle !== undefined && { title: recordTitle }),
|
|
1879
1934
|
...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
|
|
1880
1935
|
...(headerSlot !== undefined && { headerContent: headerSlot }),
|
|
1881
1936
|
...(hasHeaderActions && { headerActions: headerActionsContent }),
|
|
@@ -1891,7 +1946,10 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
1891
1946
|
...(count !== undefined && { count }),
|
|
1892
1947
|
};
|
|
1893
1948
|
}), activeId: activeSection.id ?? "", onSelect: (id) => nav.setSearchParams({ tab: id }) }))] }));
|
|
1894
|
-
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen: detailScreen, 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 ||
|
|
1949
|
+
return (_jsxs(_Fragment, { children: [_jsx(RenderEdit, { screen: detailScreen, 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 ||
|
|
1950
|
+
hasTabs ||
|
|
1951
|
+
screen.slots?.header !== undefined ||
|
|
1952
|
+
screen.recordTitleField !== undefined) && {
|
|
1895
1953
|
headerRegion: renderHeaderContent,
|
|
1896
1954
|
}), buildSectionActions: buildSectionActions, valueDisplay: screen.valueDisplay ?? "text" }, `${entityId}:${reloadNonce}`), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
1897
1955
|
closeDrawer();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
type QueryOptionsSelectProps = {
|
|
4
|
+
readonly field: EditFieldViewModel;
|
|
5
|
+
readonly query: string;
|
|
6
|
+
readonly payload: Readonly<Record<string, string | number | boolean>>;
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly hasError: boolean;
|
|
9
|
+
readonly onChange: (value: unknown) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare function QueryOptionsSelect({ field, query, payload, id, hasError, onChange, }: QueryOptionsSelectProps): ReactNode;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
3
|
+
import { usePrimitives } from "../primitives.js";
|
|
4
|
+
// Select whose options come from a query (SelectFieldDef.optionsQuery), so it
|
|
5
|
+
// has to be a mounted component for useQuery. useQuery re-fetches on the
|
|
6
|
+
// serialized payload, hence no payload memoization here.
|
|
7
|
+
export function QueryOptionsSelect({ field, query, payload, id, hasError, onChange, }) {
|
|
8
|
+
const { Input } = usePrimitives();
|
|
9
|
+
const { data, loading } = useQuery(query, payload);
|
|
10
|
+
const current = typeof field.value === "string" ? field.value : "";
|
|
11
|
+
const rows = data?.rows ?? [];
|
|
12
|
+
// A stored value missing from the result (still loading, or dropped from the
|
|
13
|
+
// catalog) must stay visible instead of silently turning into "nothing selected".
|
|
14
|
+
const options = current !== "" && !rows.some((row) => row.value === current)
|
|
15
|
+
? [{ value: current, label: current }, ...rows]
|
|
16
|
+
: rows;
|
|
17
|
+
// Keeps the presentation stable: without it the empty loading state would pick
|
|
18
|
+
// radio/dropdown by option count and jump once the rows arrive.
|
|
19
|
+
const display = field.display === "radio" || field.display === "dropdown" ? field.display : "dropdown";
|
|
20
|
+
return (_jsx(Input, { kind: "select", id: id, name: field.field, disabled: field.readOnly || loading, required: field.required, hasError: hasError, value: current, onChange: (v) => onChange(v), options: options, display: display }));
|
|
21
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import type { EditRelatedListSectionViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import { type ReactNode } from "react";
|
|
4
|
-
export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, }: {
|
|
4
|
+
export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, }: {
|
|
5
5
|
readonly section: EditRelatedListSectionViewModel;
|
|
6
6
|
readonly parentId: string;
|
|
7
7
|
/** The enclosing projectionDetail's own record (the "Akte") — evaluates
|
|
@@ -22,4 +22,12 @@ export declare function RelatedListSection({ section, parentId, record, featureN
|
|
|
22
22
|
* (non-`hideTitle`) branch below — `hideTitle` uses `FillContainer`, which
|
|
23
23
|
* has no title row to render actions into (see the comment on that branch). */
|
|
24
24
|
readonly actions?: ReactNode;
|
|
25
|
+
/** Mounted inside a host row (entityList `expandableRow`): unframed, in
|
|
26
|
+
* document flow (no Section card, no scrollBody), with its own title row
|
|
27
|
+
* carrying `actions`. */
|
|
28
|
+
readonly embedded?: boolean;
|
|
29
|
+
/** Runs after this section's own refetch once a write action (row action,
|
|
30
|
+
* toolbar action, emptyState action) succeeded — lets a host reload data
|
|
31
|
+
* the write also changed. */
|
|
32
|
+
readonly onAfterWrite?: () => void | Promise<void>;
|
|
25
33
|
}): ReactNode;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { normalizeListColumn, parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { useCallback, useMemo, useState } from "react";
|
|
4
4
|
import { useAppFeatures } from "../app/app-features-context.js";
|
|
5
5
|
import { buildFilterFacets, buildFilterPayload, mergeReferenceFacetOptions, resolveProjectionFacetSpecs, } from "../app/list-facets.js";
|
|
@@ -22,17 +22,22 @@ const RELATED_LIST_PSEUDO_ENTITY = "__related-list__";
|
|
|
22
22
|
// Minimal EntityDefinition from the section's own columns — same shape as
|
|
23
23
|
// projection-list-shim's synthesizeProjectionEntity, but sortable is read
|
|
24
24
|
// per-column (ListColumnSpec.sortable) instead of screen-wide, since a
|
|
25
|
-
// relatedList section has no single Zod schema to derive it from.
|
|
26
|
-
|
|
25
|
+
// relatedList section has no single Zod schema to derive it from. With a
|
|
26
|
+
// declared `entity`, a column naming one of its fields takes that field's
|
|
27
|
+
// definition, so RenderList formats it exactly like an entityList column;
|
|
28
|
+
// only `sortable` stays per-column because the sort runs locally.
|
|
29
|
+
function synthesizeRelatedListEntity(columns, sourceEntity) {
|
|
27
30
|
const fields = {};
|
|
28
31
|
for (const col of columns) {
|
|
29
32
|
const normalized = normalizeListColumn(col);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const sortable = normalized.sortable === true;
|
|
34
|
+
const sourceField = sourceEntity?.fields[normalized.field];
|
|
35
|
+
fields[normalized.field] =
|
|
36
|
+
sourceField !== undefined
|
|
37
|
+
? { ...sourceField, sortable }
|
|
38
|
+
: { type: normalized.valueType ?? "text", sortable };
|
|
34
39
|
}
|
|
35
|
-
return { fields };
|
|
40
|
+
return { ...sourceEntity, fields };
|
|
36
41
|
}
|
|
37
42
|
// Mirrors row-actions.ts's own warnDrawerActionDropped (module-private
|
|
38
43
|
// there, so not reusable directly) — a drawer-kind emptyState.action with no
|
|
@@ -42,8 +47,8 @@ function warnRelatedListEmptyStateDrawerDropped(actionId) {
|
|
|
42
47
|
// biome-ignore lint/suspicious/noConsole: dev-time warning, same pattern as row-actions.ts
|
|
43
48
|
console.warn(`[kumiko] relatedList emptyState.action "${actionId}" is kind: "drawer" but no onOpenDrawer was supplied — dropped.`);
|
|
44
49
|
}
|
|
45
|
-
export function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, }) {
|
|
46
|
-
const { Banner, Section, FillContainer, Text, Button, Dialog } = usePrimitives();
|
|
50
|
+
export function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, embedded, onAfterWrite, }) {
|
|
51
|
+
const { Banner, Section, Card, FillContainer, Text, Button, Dialog } = usePrimitives();
|
|
47
52
|
const [emptyStateActionError, setEmptyStateActionError] = useState(null);
|
|
48
53
|
const t = useTranslation();
|
|
49
54
|
const effectiveTranslate = translate ?? t;
|
|
@@ -59,7 +64,15 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
59
64
|
return findEditScreenFor(targetEntity, appFeatures, userRoles);
|
|
60
65
|
}, [appFeatures, section.rowClick, userRoles]);
|
|
61
66
|
const defaultEditRowAction = useMemo(() => buildDefaultEditRowAction(defaultEditScreen, section.rowClick?.idColumn ?? "id"), [defaultEditScreen, section.rowClick]);
|
|
62
|
-
|
|
67
|
+
// RenderList derives header and option-label keys from (featureName,
|
|
68
|
+
// screen.entity), so a declared entity also supplies both.
|
|
69
|
+
const sourceTarget = useMemo(() => (section.entity !== undefined ? parseRefTarget(section.entity, featureName) : undefined), [section.entity, featureName]);
|
|
70
|
+
const sourceEntity = sourceTarget !== undefined
|
|
71
|
+
? appFeatures.find((f) => f.featureName === sourceTarget.featureName)?.entities[sourceTarget.entityName]
|
|
72
|
+
: undefined;
|
|
73
|
+
const listFeatureName = sourceEntity !== undefined ? sourceTarget?.featureName : undefined;
|
|
74
|
+
const listEntityName = sourceEntity !== undefined ? sourceTarget?.entityName : undefined;
|
|
75
|
+
const entity = useMemo(() => synthesizeRelatedListEntity(section.columns, sourceEntity), [section.columns, sourceEntity]);
|
|
63
76
|
const listScreen = useMemo(() => ({
|
|
64
77
|
// Empty id → RenderList's own toolbarTitle resolves to "" (its
|
|
65
78
|
// `screen:${id}.title` lookup misses and falls back to `id`) — this
|
|
@@ -67,9 +80,9 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
67
80
|
// so RenderList's toolbar carries none.
|
|
68
81
|
id: "",
|
|
69
82
|
type: "entityList",
|
|
70
|
-
entity: RELATED_LIST_PSEUDO_ENTITY,
|
|
83
|
+
entity: listEntityName ?? RELATED_LIST_PSEUDO_ENTITY,
|
|
71
84
|
columns: section.columns,
|
|
72
|
-
}), [section.columns]);
|
|
85
|
+
}), [section.columns, listEntityName]);
|
|
73
86
|
// Local state, not URL state: a section `id` is optional, so there is no
|
|
74
87
|
// stable URL key to namespace against — the section's sort is local for the
|
|
75
88
|
// same reason. Consequence: search/filters reset on reload.
|
|
@@ -107,6 +120,11 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
107
120
|
filterPayload,
|
|
108
121
|
]);
|
|
109
122
|
const rowsQuery = useQuery(section.query, payload);
|
|
123
|
+
const ownRefetch = rowsQuery.refetch;
|
|
124
|
+
const refetchSelfAndHost = useCallback(async () => {
|
|
125
|
+
await ownRefetch();
|
|
126
|
+
await onAfterWrite?.();
|
|
127
|
+
}, [ownRefetch, onAfterWrite]);
|
|
110
128
|
const onFilterChange = useCallback((field, values) => setFilters((prev) => ({ ...prev, [field]: values })), []);
|
|
111
129
|
const onFilterReset = useCallback(() => setFilters({}), []);
|
|
112
130
|
// Sorted client-side over the already-loaded rows — this section has no
|
|
@@ -150,7 +168,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
150
168
|
translate: effectiveTranslate,
|
|
151
169
|
dispatcher,
|
|
152
170
|
nav,
|
|
153
|
-
refetch:
|
|
171
|
+
refetch: refetchSelfAndHost,
|
|
154
172
|
openDrawer: onOpenDrawer,
|
|
155
173
|
defaultEditRowAction,
|
|
156
174
|
host,
|
|
@@ -159,7 +177,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
159
177
|
effectiveTranslate,
|
|
160
178
|
dispatcher,
|
|
161
179
|
nav,
|
|
162
|
-
|
|
180
|
+
refetchSelfAndHost,
|
|
163
181
|
onOpenDrawer,
|
|
164
182
|
defaultEditRowAction,
|
|
165
183
|
host,
|
|
@@ -171,7 +189,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
171
189
|
translate: effectiveTranslate,
|
|
172
190
|
dispatcher,
|
|
173
191
|
nav,
|
|
174
|
-
refetch:
|
|
192
|
+
refetch: refetchSelfAndHost,
|
|
175
193
|
navigatePrefill: section.parentFilter !== undefined
|
|
176
194
|
? { [section.parentFilter.field]: parentId }
|
|
177
195
|
: { [section.parentParam ?? "id"]: parentId },
|
|
@@ -190,7 +208,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
190
208
|
dispatcher,
|
|
191
209
|
nav,
|
|
192
210
|
host,
|
|
193
|
-
|
|
211
|
+
refetchSelfAndHost,
|
|
194
212
|
onOpenDrawer,
|
|
195
213
|
]);
|
|
196
214
|
// emptyState.action is a RowAction — the same record-bound action shape
|
|
@@ -216,7 +234,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
216
234
|
host,
|
|
217
235
|
dispatcher,
|
|
218
236
|
openDrawer: onOpenDrawer ?? (() => { }),
|
|
219
|
-
onWriteSuccess:
|
|
237
|
+
onWriteSuccess: refetchSelfAndHost,
|
|
220
238
|
})?.[0];
|
|
221
239
|
}, [
|
|
222
240
|
section.emptyState,
|
|
@@ -226,7 +244,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
226
244
|
host,
|
|
227
245
|
dispatcher,
|
|
228
246
|
onOpenDrawer,
|
|
229
|
-
|
|
247
|
+
refetchSelfAndHost,
|
|
230
248
|
]);
|
|
231
249
|
const emptyStateContent = section.emptyState !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: section.emptyState.title }), section.emptyState.description !== undefined && (_jsx(Text, { variant: "small", children: section.emptyState.description })), emptyStateActionButton !== undefined && (_jsx(RenderEditActionButton, { action: emptyStateActionButton, Button: Button, Dialog: Dialog, onError: setEmptyStateActionError }))] })) : undefined;
|
|
232
250
|
// A truncated fetch means `sortedRows` is a sort of a partial set, not of
|
|
@@ -242,7 +260,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
242
260
|
(rowsQuery.data.total === undefined || rowsQuery.data.total > rowsQuery.data.rows.length);
|
|
243
261
|
const content = rowsQuery.loading && rowsQuery.data === null ? (_jsx(Banner, { padded: true, variant: "loading", testId: "related-list-loading", children: "Loading\u2026" })) : rowsQuery.error ? (_jsx(Banner, { padded: true, variant: "error", testId: "related-list-error", children: dispatcherErrorText(rowsQuery.error, effectiveTranslate) })) : (_jsxs(_Fragment, { children: [truncated && (_jsx(Banner, { variant: "info", testId: "related-list-truncated", children: effectiveTranslate("kumiko.list.related-list-truncated", {
|
|
244
262
|
count: sortedRows.length,
|
|
245
|
-
}) })), emptyStateActionError !== null && (_jsx(Banner, { variant: "error", testId: "related-list-empty-state-action-error", children: emptyStateActionError })), _jsx(PageHeaderSlotAvailableProvider, { value: false, children: _jsx(RenderList, { screen: listScreen, entity: entity, rows: sortedRows, featureName: featureName, translate: effectiveTranslate, ...(section.description !== undefined && { description: section.description }), ...(section.itemNoun !== undefined && { itemNounKey: section.itemNoun }), sort: sort, onSortChange: setSort, ...(section.searchable === true && {
|
|
263
|
+
}) })), emptyStateActionError !== null && (_jsx(Banner, { variant: "error", testId: "related-list-empty-state-action-error", children: emptyStateActionError })), _jsx(PageHeaderSlotAvailableProvider, { value: false, children: _jsx(RenderList, { screen: listScreen, entity: entity, rows: sortedRows, featureName: listFeatureName ?? featureName, translate: effectiveTranslate, ...(section.description !== undefined && { description: section.description }), ...(section.itemNoun !== undefined && { itemNounKey: section.itemNoun }), sort: sort, onSortChange: setSort, ...(section.searchable === true && {
|
|
246
264
|
searchable: true,
|
|
247
265
|
searchValue: search,
|
|
248
266
|
onSearchChange: setSearch,
|
|
@@ -255,7 +273,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
255
273
|
scrollBody: true,
|
|
256
274
|
screenPadding: false,
|
|
257
275
|
chromeless: true,
|
|
258
|
-
}) }) })] }));
|
|
276
|
+
}), ...(embedded === true && { screenPadding: false, chromeless: true }) }) })] }));
|
|
259
277
|
// hideTitle (tabs mode) → the caller (render-edit.tsx) already wraps this
|
|
260
278
|
// component's own return value in the same Card frame every other tab
|
|
261
279
|
// kind gets — no card here, or the two would nest. `scrollBody` caps the
|
|
@@ -266,6 +284,15 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
266
284
|
// Section card and document-flow height since they render a visible title
|
|
267
285
|
// and aren't confined to a tab panel.
|
|
268
286
|
const bridges = _jsx(ReferenceFacetBridges, { specs: facetSpecs, onOptions: handleFacetOptions });
|
|
287
|
+
if (embedded === true) {
|
|
288
|
+
const countSuffix = rowsQuery.data !== null ? ` · ${sortedRows.length}` : "";
|
|
289
|
+
return (_jsxs(_Fragment, { children: [bridges, _jsx(Card, { options: { framed: false, padded: false },
|
|
290
|
+
// Trim the default header inset: the host row already pads the area.
|
|
291
|
+
className: "[&>div:first-child]:px-0 [&>div:first-child]:pt-1 [&>div:first-child]:pb-2", slots: {
|
|
292
|
+
title: `${section.title}${countSuffix}`,
|
|
293
|
+
...(actions !== undefined && { headerActions: actions }),
|
|
294
|
+
}, testId: `related-list-${section.title}-${parentId}`, children: content })] }));
|
|
295
|
+
}
|
|
269
296
|
if (hideTitle) {
|
|
270
297
|
return (_jsxs(_Fragment, { children: [bridges, FillContainer !== undefined ? (_jsx(FillContainer, { ...(grow === true && { grow: true }), children: content })) : (content)] }));
|
|
271
298
|
}
|
|
@@ -3,4 +3,6 @@ import type { RenderEditProps } from "./render-edit-types.js";
|
|
|
3
3
|
export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./render-edit-types.js";
|
|
4
4
|
import { type ReactNode } from "react";
|
|
5
5
|
export declare const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
|
|
6
|
+
/** A record title is a non-blank string; anything else leaves the screen title alone. */
|
|
7
|
+
export declare function resolveRecordTitle(value: unknown): string | undefined;
|
|
6
8
|
export declare function RenderEdit<TValues extends FormValues, TCtx = unknown>(props: RenderEditProps<TValues, TCtx>): ReactNode;
|
|
@@ -3,7 +3,7 @@ import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION
|
|
|
3
3
|
import { computeEditViewModel } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
5
|
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
6
|
-
import {
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { ExtensionFormRegistryProvider, useExtensionFormHost, } from "../app/extension-form-submit.js";
|
|
8
8
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
9
9
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
@@ -12,7 +12,7 @@ import { formatWhen } from "../format-when.js";
|
|
|
12
12
|
import { useForm } from "../hooks/use-form.js";
|
|
13
13
|
import { useTranslation } from "../i18n.js";
|
|
14
14
|
import { useInsideDrawer } from "../inside-drawer.js";
|
|
15
|
-
import { usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
15
|
+
import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
16
16
|
import { STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
|
|
17
17
|
import { GridCellForField } from "./grid-cell-for-field.js";
|
|
18
18
|
import { RelatedListSection } from "./related-list-section.js";
|
|
@@ -141,6 +141,10 @@ function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values,
|
|
|
141
141
|
function formSectionDomId(sectionIndex) {
|
|
142
142
|
return `form-section-${sectionIndex}`;
|
|
143
143
|
}
|
|
144
|
+
/** A record title is a non-blank string; anything else leaves the screen title alone. */
|
|
145
|
+
export function resolveRecordTitle(value) {
|
|
146
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
147
|
+
}
|
|
144
148
|
export function RenderEdit(props) {
|
|
145
149
|
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, summary, labelAppendix, fieldAppendix, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, headerRegion, buildSectionActions, } = props;
|
|
146
150
|
const { customSubmit } = props;
|
|
@@ -208,6 +212,7 @@ export function RenderEdit(props) {
|
|
|
208
212
|
});
|
|
209
213
|
const { Button, Banner, Card, Dialog, Form, Section, Grid, GridCell, Text, Progress, StepBar, WizardStepGroup, Tabs, PageHeader, ActionOverflowMenu, } = usePrimitives();
|
|
210
214
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
215
|
+
const pageHeaderCompact = usePageHeaderCompact();
|
|
211
216
|
const insideDrawer = useInsideDrawer();
|
|
212
217
|
// Both stepped layouts show one section at a time and keep the rest mounted
|
|
213
218
|
// but inert. A tabs layout on a host without the Tabs primitive has no strip
|
|
@@ -957,11 +962,8 @@ export function RenderEdit(props) {
|
|
|
957
962
|
return id == null || id === "";
|
|
958
963
|
})();
|
|
959
964
|
const formMode = isCreate ? "create" : "edit";
|
|
960
|
-
const
|
|
961
|
-
? initial[screen.recordTitleField]
|
|
962
|
-
: undefined;
|
|
963
|
-
const recordTitle = typeof recordTitleValue === "string" && recordTitleValue.trim() !== ""
|
|
964
|
-
? recordTitleValue
|
|
965
|
+
const recordTitle = !isCreate && screen.recordTitleField !== undefined
|
|
966
|
+
? resolveRecordTitle(initial[screen.recordTitleField])
|
|
965
967
|
: undefined;
|
|
966
968
|
const resolveScreenText = (suffix) => {
|
|
967
969
|
for (const key of [
|
|
@@ -1013,6 +1015,8 @@ export function RenderEdit(props) {
|
|
|
1013
1015
|
: translate("kumiko.wizard.step", { current: currentStep + 1, total: lastStepIndex + 1 });
|
|
1014
1016
|
})();
|
|
1015
1017
|
return (_jsxs(ExtensionFormRegistryProvider, { value: extensionFormRegistry, children: [titleInShell && (_jsx(PageHeader, { title: formTitle, ...(recordTitle !== undefined && { recordTitle }), ...(headerMenuItems.length > 0 &&
|
|
1018
|
+
pageHeaderCompact && { overflowItems: headerMenuItems }), ...(headerMenuItems.length > 0 &&
|
|
1019
|
+
!pageHeaderCompact &&
|
|
1016
1020
|
ActionOverflowMenu !== undefined && {
|
|
1017
1021
|
actions: (_jsx(ActionOverflowMenu, { label: translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
|
|
1018
1022
|
}) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true && !titleInShell && { title: formTitle }), ...(formSubtitle !== undefined && !stepRail && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount > 0 && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
|
|
@@ -1084,12 +1088,14 @@ export function RenderEdit(props) {
|
|
|
1084
1088
|
// over TValues; controller is mount-lifetime-stable, see onControlsReady above.
|
|
1085
1089
|
patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(hideSectionTitles !== true &&
|
|
1086
1090
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
|
|
1087
|
-
// Tabs mode: unframed
|
|
1088
|
-
//
|
|
1089
|
-
// testId stays only on the inner
|
|
1090
|
-
//
|
|
1091
|
+
// Tabs mode: an unframed padded panel (the tab panel is the
|
|
1092
|
+
// surface), so extension content neither sits flush at the shell
|
|
1093
|
+
// edge nor touches the tab strip. testId stays only on the inner
|
|
1094
|
+
// Section — giving the wrapper the same one would register two
|
|
1091
1095
|
// elements under one id.
|
|
1092
|
-
const wrapped = hideSectionTitles !== true ? (mount) :
|
|
1096
|
+
const wrapped = hideSectionTitles !== true ? (mount) : (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
1097
|
+
slots: { headerActions: sectionActionsEl },
|
|
1098
|
+
}), children: mount }, section.title));
|
|
1093
1099
|
return wrapWizardStep(section.title, wrapped);
|
|
1094
1100
|
}
|
|
1095
1101
|
if (section.kind === "relatedList") {
|
|
@@ -1126,8 +1132,8 @@ export function RenderEdit(props) {
|
|
|
1126
1132
|
}), ...(fieldAppendix !== undefined && {
|
|
1127
1133
|
fieldAppendix: fieldAppendix(field.field),
|
|
1128
1134
|
}), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...(!isCreate && field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
|
|
1129
|
-
// Tabs mode
|
|
1130
|
-
//
|
|
1135
|
+
// Tabs mode: the tab panel is the surface, so field sections render
|
|
1136
|
+
// as unframed padded panels; group cards inside stay framed.
|
|
1131
1137
|
// Stays `Card`, not `Section` — Section always flattens to a
|
|
1132
1138
|
// borderless divider when rendered inside this component's own
|
|
1133
1139
|
// `<Form>` (InsideFormContext), even with `chromeless` set, so it
|
|
@@ -1140,13 +1146,13 @@ export function RenderEdit(props) {
|
|
|
1140
1146
|
// group cards (Card-in-Card) — the boot-validator rejects that
|
|
1141
1147
|
// combination in tabs mode, so `sectionActionsEl` is always
|
|
1142
1148
|
// undefined here at runtime.
|
|
1143
|
-
return wrapWizardStep(sectionKey, groupsGrid);
|
|
1149
|
+
return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
|
|
1144
1150
|
}
|
|
1145
1151
|
// No section title here (fw#3218) — the Tab strip right above
|
|
1146
1152
|
// already names this panel, so a repeated Card title would just
|
|
1147
1153
|
// duplicate it. The title row only exists to carry actions, and
|
|
1148
1154
|
// then it carries only the actions, never the title text.
|
|
1149
|
-
const cardEl = (_jsx(Card, { ...(sectionActionsEl !== undefined && {
|
|
1155
|
+
const cardEl = (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
1150
1156
|
slots: { headerActions: sectionActionsEl },
|
|
1151
1157
|
}), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`) }, sectionKey));
|
|
1152
1158
|
return wrapWizardStep(sectionKey, cardEl);
|
|
@@ -15,6 +15,7 @@ import { useLocale, useTranslation } from "../i18n.js";
|
|
|
15
15
|
import { usePrimitives } from "../primitives.js";
|
|
16
16
|
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
17
17
|
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
18
|
+
import { QueryOptionsSelect } from "./query-options-select.js";
|
|
18
19
|
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
19
20
|
const AllFieldsRequiredContext = createContext(false);
|
|
20
21
|
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
@@ -41,7 +42,7 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
41
42
|
// Reference-Field rendert eine eigene Component — sie nutzt
|
|
42
43
|
// useQuery() für den Live-Lookup, also muss sie als React-
|
|
43
44
|
// Komponente gemountet werden (nicht als pure render-Call).
|
|
44
|
-
const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
|
|
45
|
+
const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
|
|
45
46
|
field,
|
|
46
47
|
id,
|
|
47
48
|
hasError,
|
|
@@ -98,6 +98,11 @@ export type RenderListProps = {
|
|
|
98
98
|
* fw#3234). Omitted: legacy `p-6` fallback for a standalone table with no
|
|
99
99
|
* padded host. */
|
|
100
100
|
readonly screenPadding?: boolean;
|
|
101
|
+
/** Forwarded to `DataTableProps.expandedRowIds` / `onToggleRowExpanded` /
|
|
102
|
+
* `renderExpandedRow` (entityList `expandableRow`). */
|
|
103
|
+
readonly expandedRowIds?: ReadonlySet<string>;
|
|
104
|
+
readonly onToggleRowExpanded?: (rowId: string) => void;
|
|
105
|
+
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
101
106
|
};
|
|
102
107
|
export type ToolbarActionButton = {
|
|
103
108
|
readonly id: string;
|
|
@@ -4,17 +4,18 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
5
5
|
import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
|
|
6
6
|
import { useTranslation } from "../i18n.js";
|
|
7
|
-
import { usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
7
|
+
import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
8
8
|
import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
|
|
9
9
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
10
10
|
export function RenderList(props) {
|
|
11
|
-
const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, } = props;
|
|
11
|
+
const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, } = props;
|
|
12
12
|
// Wie RenderEdit: Translate-Fallback aus dem i18next-Context, sonst
|
|
13
13
|
// wären Column-Header raw i18n-Keys.
|
|
14
14
|
const t = useTranslation();
|
|
15
15
|
const translate = translateProp ?? t;
|
|
16
16
|
const { DataTable, Button, Dialog, Input, Text, Banner, PageHeader } = usePrimitives();
|
|
17
17
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
18
|
+
const pageHeaderCompact = usePageHeaderCompact();
|
|
18
19
|
const createInPageHeader = onCreate !== undefined && PageHeader !== undefined && pageHeaderSlotAvailable;
|
|
19
20
|
// Local search buffer + debounce. External changes (browser back,
|
|
20
21
|
// cross-component reset) are mirrored back; typing only fires
|
|
@@ -131,7 +132,7 @@ export function RenderList(props) {
|
|
|
131
132
|
const hasToolbarActions = toolbarActions !== undefined && toolbarActions.length > 0;
|
|
132
133
|
const hasHeaderSlot = screen.slots?.header !== undefined;
|
|
133
134
|
const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
|
|
134
|
-
const createButton = onCreate !== undefined ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-create", className: "px-3.5", children: effectiveCreateLabel })) : undefined;
|
|
135
|
+
const createButton = onCreate !== undefined && createInPageHeader && pageHeaderCompact ? (_jsx(Button, { variant: "primary", icon: "plus", size: "icon", ariaLabel: effectiveCreateLabel, title: effectiveCreateLabel, onClick: onCreate, testId: "render-list-create" })) : onCreate !== undefined ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-create", className: "px-3.5", children: effectiveCreateLabel })) : undefined;
|
|
135
136
|
const toolbarEnd = hasHeaderSlot || hasToolbarActions || (onCreate !== undefined && !createInPageHeader) ? (_jsxs(_Fragment, { children: [hasHeaderSlot && _jsx(ListHeaderSlotMount, { screen: screen }), hasToolbarActions &&
|
|
136
137
|
toolbarActions.map((a) => (_jsx(ToolbarActionView, { action: a, iconOnly: toolbarIconOnly, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), !createInPageHeader && createButton] })) : undefined;
|
|
137
138
|
// Empty-State: Default zeigt Heading + Description + optional CTA-
|
|
@@ -148,7 +149,7 @@ export function RenderList(props) {
|
|
|
148
149
|
const resolvedTitle = translate(titleKey);
|
|
149
150
|
const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
|
|
150
151
|
// ListSort = DataTableSort (use-list-url-state aliased) — kein Cast nötig.
|
|
151
|
-
return (_jsxs(_Fragment, { children: [createInPageHeader && PageHeader !== undefined && _jsx(PageHeader, { actions: createButton }), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && !searchable && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), testId: "render-list-table" })] }));
|
|
152
|
+
return (_jsxs(_Fragment, { children: [createInPageHeader && PageHeader !== undefined && _jsx(PageHeader, { actions: createButton }), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && !searchable && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), testId: "render-list-table" })] }));
|
|
152
153
|
}
|
|
153
154
|
// Header-Slot in der Listen-Toolbar (toolbarEnd): `screen.slots.header` ist
|
|
154
155
|
// eine PlatformComponent (`{ react: { __component: "X" } }`), aufgelöst über
|
|
@@ -88,11 +88,10 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
88
88
|
const submitButton = (_jsx(Button, { type: "button", variant: "primary", disabled: isSubmitting, loading: isSubmitting, onClick: () => void handleSubmit(), testId: "write-form-section-submit", children: section.submitLabel ?? effectiveTranslate("kumiko.actions.save") }));
|
|
89
89
|
const titleRowActions = actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
|
|
90
90
|
// Section always flattens to a borderless divider when rendered inside
|
|
91
|
-
// RenderEdit's own <Form> in tabs mode (hideTitle) —
|
|
92
|
-
//
|
|
93
|
-
// branches stay on Card.
|
|
91
|
+
// RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
|
|
92
|
+
// stands in, like render-edit.tsx's tabs-mode fields/extension branches.
|
|
94
93
|
if (hideTitle) {
|
|
95
|
-
return (_jsx(Card, { slots: {
|
|
94
|
+
return (_jsx(Card, { options: { framed: false }, slots: {
|
|
96
95
|
...(section.description !== undefined && { subtitle: section.description }),
|
|
97
96
|
headerActions: titleRowActions,
|
|
98
97
|
}, testId: `write-form-${section.title ?? "section"}`, children: content }));
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -68,6 +68,8 @@ export const kumikoDefaultTranslations = {
|
|
|
68
68
|
"kumiko.list.count.one": "1 entry",
|
|
69
69
|
"kumiko.list.count.other": "{count} entries",
|
|
70
70
|
"kumiko.list.row-actions.more": "More actions",
|
|
71
|
+
"kumiko.list.row.collapse": "Collapse {title}",
|
|
72
|
+
"kumiko.list.row.expand": "Expand {title}",
|
|
71
73
|
"kumiko.list.sort.by": "Sorted by {column}",
|
|
72
74
|
"kumiko.form.changed": "changed",
|
|
73
75
|
"kumiko.form.on-this-page": "On this page",
|
package/dist/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export type { LocaleProviderProps, TranslationBundle, TranslationsByKey, Transla
|
|
|
66
66
|
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
67
67
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
68
68
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
69
|
-
export { PageHeaderSlotAvailableProvider, usePageHeaderSlotAvailable } from "./page-header-slot.js";
|
|
69
|
+
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
70
70
|
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, WizardStepGroupProps, } from "./primitives.js";
|
|
71
71
|
export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
72
72
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ export { useStreamHandler } from "./hooks/use-stream-handler.js";
|
|
|
47
47
|
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
48
48
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
49
49
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
50
|
-
export { PageHeaderSlotAvailableProvider, usePageHeaderSlotAvailable } from "./page-header-slot.js";
|
|
50
|
+
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
51
51
|
export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
52
52
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
53
53
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare const PageHeaderSlotAvailableProvider: import("react").Provider<boolean>;
|
|
2
2
|
export declare function usePageHeaderSlotAvailable(): boolean;
|
|
3
|
+
export declare const PageHeaderCompactProvider: import("react").Provider<boolean>;
|
|
4
|
+
export declare function usePageHeaderCompact(): boolean;
|
package/dist/page-header-slot.js
CHANGED
|
@@ -7,3 +7,10 @@ export const PageHeaderSlotAvailableProvider = PageHeaderSlotAvailableContext.Pr
|
|
|
7
7
|
export function usePageHeaderSlotAvailable() {
|
|
8
8
|
return useContext(PageHeaderSlotAvailableContext);
|
|
9
9
|
}
|
|
10
|
+
// True while a shell with page-header slots renders at phone width: screens
|
|
11
|
+
// then move secondary header actions into the shell's overflow menu.
|
|
12
|
+
const PageHeaderCompactContext = createContext(false);
|
|
13
|
+
export const PageHeaderCompactProvider = PageHeaderCompactContext.Provider;
|
|
14
|
+
export function usePageHeaderCompact() {
|
|
15
|
+
return useContext(PageHeaderCompactContext);
|
|
16
|
+
}
|
package/dist/primitives.d.ts
CHANGED
|
@@ -642,6 +642,17 @@ export type DataTableProps = {
|
|
|
642
642
|
* `scrollBody`'s flex-fill height budget.
|
|
643
643
|
* Default false: unchanged symmetric inset for an embedded table. */
|
|
644
644
|
readonly screenPadding?: boolean;
|
|
645
|
+
/** Ids of the rows whose expansion area is currently open (controlled by
|
|
646
|
+
* the host; several rows can be open at once). Only meaningful together
|
|
647
|
+
* with `renderExpandedRow` + `onToggleRowExpanded`. */
|
|
648
|
+
readonly expandedRowIds?: ReadonlySet<string>;
|
|
649
|
+
/** User toggled a row's expand control. With `renderExpandedRow` this makes
|
|
650
|
+
* every row expandable: the renderer adds a toggle (button with
|
|
651
|
+
* `aria-expanded`) that does not trigger `onRowClick`. */
|
|
652
|
+
readonly onToggleRowExpanded?: (rowId: string) => void;
|
|
653
|
+
/** Content of the area under an expanded row. Native renderers implement
|
|
654
|
+
* the same three props. */
|
|
655
|
+
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
645
656
|
};
|
|
646
657
|
/** Cell type for one column of an embedded-list field. Mirrors
|
|
647
658
|
* EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-headless`. */
|
|
@@ -876,7 +887,7 @@ export type GridProps = {
|
|
|
876
887
|
readonly flow?: boolean;
|
|
877
888
|
};
|
|
878
889
|
/** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
|
|
879
|
-
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "
|
|
890
|
+
export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "toggle";
|
|
880
891
|
/** Span-Wrapper für ein Kind innerhalb eines Grid. Web: `style={{gridColumn: span N}}`,
|
|
881
892
|
* Native: eigenes Width-Rechnen. */
|
|
882
893
|
export type GridCellProps = {
|
|
@@ -1029,6 +1040,9 @@ export type CardOptions = {
|
|
|
1029
1040
|
* relatedList table sizing to the panel instead of the page) instead of
|
|
1030
1041
|
* normal document-flow height. Default false. */
|
|
1031
1042
|
readonly fillHeight?: boolean;
|
|
1043
|
+
/** false drops border, background, shadow and radius but keeps padding and
|
|
1044
|
+
* header slots — a panel inside a host surface such as a tab panel. Default true. */
|
|
1045
|
+
readonly framed?: boolean;
|
|
1032
1046
|
};
|
|
1033
1047
|
/** Generische Card-Chrome (border/bg/radius/shadow + Header/Body/Footer) als
|
|
1034
1048
|
* Primitive — eine Quelle für alle Card-Flächen, statt pro App nachgebaut.
|
|
@@ -1159,6 +1173,8 @@ export type PageHeaderProps = {
|
|
|
1159
1173
|
readonly recordTitle?: string;
|
|
1160
1174
|
readonly status?: ReactNode;
|
|
1161
1175
|
readonly actions?: ReactNode;
|
|
1176
|
+
/** Land in the shell header's overflow menu at phone width. */
|
|
1177
|
+
readonly overflowItems?: readonly ActionMenuItemSpec[];
|
|
1162
1178
|
};
|
|
1163
1179
|
/** One item in an `ActionOverflowMenu` (A7: header/row actions beyond the
|
|
1164
1180
|
* primary one collapse into this menu instead of a wall of buttons). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.331.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.331.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.331.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.331.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.331.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.331.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "entityList rows can expand into a related list with its own row actions (expandableRow)",
|
|
6
|
+
"detail": "`expandableRow` on an entityList declares a related list under each row, with the same fields as a projectionDetail `relatedList` section (query, `parentFilter` or `parentParam`, columns, row and toolbar actions, emptyState). The parent id is the row's `id`. An arrow button at the start of the row opens and closes the area, carries `aria-expanded`, and works by keyboard. Several rows can be open at once. A successful write from the area reloads both the related list and the parent list, so counters on the parent row update. The boot validator and the role projection check the area like a relatedList section. `DataTableProps` gains `expandedRowIds`, `onToggleRowExpanded` and `renderExpandedRow` for custom DataTable primitives."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.331.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "relatedList columns format select, date and other field types like entityList columns (entity)",
|
|
12
|
+
"detail": "A relatedList section (projectionDetail tab or entityList `expandableRow`) takes an optional `entity`: an entity name or `feature:entity`. Columns that name a field of that entity render through the same cell formatter as an entityList column. A select shows as a status badge with its translated option label, dates are locale-formatted, and the header defaults to the field's label key. A column's own `sortable` still controls the header sort. The boot validator rejects an `entity` that does not resolve. Without `entity`, columns render as before."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.330.1",
|
|
16
|
+
"type": "breaking",
|
|
17
|
+
"title": "Boolean fields in flow forms use FieldCellWidth \"toggle\" instead of \"auto\"",
|
|
18
|
+
"detail": "In flow forms (screen forms and drawers) a boolean field's label now shares the top line with its neighbours' labels and the switch sits on the input line. The `FieldCellWidth` union loses `\"auto\"` and gains `\"toggle\"`; the default web Grid maps it to a fixed minimum width.",
|
|
19
|
+
"migration": "Custom Grid/GridCell primitives keyed by FieldCellWidth: rename `auto` to `toggle`."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.330.1",
|
|
23
|
+
"type": "improvement",
|
|
24
|
+
"title": "projectionDetail tab panels get page padding, recordTitleField on projectionDetail",
|
|
25
|
+
"detail": "Tab panels on a projectionDetail with `layout.mode: \"tabs\"` now pad their content like the rest of the page: extension, field-section, groups and writeForm tabs render as an unframed padded panel with space below the tab strip. relatedList tabs stay flush. This fixes extension tabs sitting flush against the shell edge since 0.330.0. `CardOptions.framed` (default true) drops the card frame and keeps the padding. A projectionDetail without `header` can set `recordTitleField`: the query output field then titles the page header (breadcrumb \"list > record\"). The boot check requires the field to be in the query's outputSchema and rejects it next to `header`.",
|
|
26
|
+
"migration": "Extension tab panels get their padding back, so extension components that added their own padding to make up for the flush panel in 0.330.0 should drop it. Set `recordTitleField: \"<field>\"` on a projectionDetail without `header` to show the record name in the breadcrumb."
|
|
27
|
+
},
|
|
2
28
|
{
|
|
3
29
|
"version": "0.330.0",
|
|
4
30
|
"type": "fix",
|