@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityDefinition,
|
|
3
|
-
EntityListScreenDefinition,
|
|
4
|
-
RowActionDrawer,
|
|
5
|
-
RowActionNavigate,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
-
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
8
|
-
import type {
|
|
9
|
-
EditRelatedListSectionViewModel,
|
|
10
|
-
ListRowViewModel,
|
|
11
|
-
Translate,
|
|
12
|
-
} from "@cosmicdrift/kumiko-headless";
|
|
13
|
-
import { type ReactNode, useCallback, useMemo, useState } from "react";
|
|
14
|
-
import { useAppFeatures } from "../app/app-features-context";
|
|
15
|
-
import {
|
|
16
|
-
buildFilterFacets,
|
|
17
|
-
buildFilterPayload,
|
|
18
|
-
mergeReferenceFacetOptions,
|
|
19
|
-
resolveProjectionFacetSpecs,
|
|
20
|
-
} from "../app/list-facets";
|
|
21
|
-
import { useNav } from "../app/nav";
|
|
22
|
-
import { ReferenceFacetBridges, type ReferenceFacetOption } from "../app/reference-facet-bridge";
|
|
23
|
-
import { navigateWithReturnTo, useReturnHost } from "../app/return-to";
|
|
24
|
-
import {
|
|
25
|
-
buildDefaultEditRowAction,
|
|
26
|
-
buildProjectionRowActions,
|
|
27
|
-
buildProjectionToolbarActions,
|
|
28
|
-
buildRecordActions,
|
|
29
|
-
runProjectionRowNavigate,
|
|
30
|
-
} from "../app/row-actions";
|
|
31
|
-
import { findEditScreenFor } from "../app/screen-access";
|
|
32
|
-
import { dispatcherErrorText } from "../app/write-failed-error";
|
|
33
|
-
import { useOptionalDispatcher } from "../context/dispatcher-context";
|
|
34
|
-
import { useUserRoles } from "../context/user-roles-context";
|
|
35
|
-
import type { ListSort } from "../hooks/use-list-url-state";
|
|
36
|
-
import { useQuery } from "../hooks/use-query";
|
|
37
|
-
import { useTranslation } from "../i18n";
|
|
38
|
-
import { PageHeaderSlotAvailableProvider } from "../page-header-slot";
|
|
39
|
-
import { type DataTableFacet, usePrimitives } from "../primitives";
|
|
40
|
-
import { sortByAccessor } from "../sort-by-accessor";
|
|
41
|
-
import { RenderEditActionButton } from "./render-edit-action-button";
|
|
42
|
-
import { RenderList } from "./render-list";
|
|
43
|
-
|
|
44
|
-
const RELATED_LIST_PSEUDO_ENTITY = "__related-list__";
|
|
45
|
-
|
|
46
|
-
// Same paged envelope as ProjectionListBody's PagedRows (kumiko-screen.tsx) —
|
|
47
|
-
// duplicated locally because that type isn't exported (projectionList and
|
|
48
|
-
// relatedList are independent query call-sites, not a shared abstraction).
|
|
49
|
-
type PagedRows = {
|
|
50
|
-
readonly rows: Readonly<Record<string, unknown>>[];
|
|
51
|
-
readonly nextCursor: string | null;
|
|
52
|
-
readonly total?: number;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// Minimal EntityDefinition from the section's own columns — same shape as
|
|
56
|
-
// projection-list-shim's synthesizeProjectionEntity, but sortable is read
|
|
57
|
-
// per-column (ListColumnSpec.sortable) instead of screen-wide, since a
|
|
58
|
-
// relatedList section has no single Zod schema to derive it from.
|
|
59
|
-
function synthesizeRelatedListEntity(
|
|
60
|
-
columns: EditRelatedListSectionViewModel["columns"],
|
|
61
|
-
): EntityDefinition {
|
|
62
|
-
const fields: Record<string, { type: string; sortable: boolean }> = {};
|
|
63
|
-
for (const col of columns) {
|
|
64
|
-
const normalized = normalizeListColumn(col);
|
|
65
|
-
fields[normalized.field] = {
|
|
66
|
-
type: normalized.valueType ?? "text",
|
|
67
|
-
sortable: normalized.sortable === true,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return { fields } as unknown as EntityDefinition;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Mirrors row-actions.ts's own warnDrawerActionDropped (module-private
|
|
74
|
-
// there, so not reusable directly) — a drawer-kind emptyState.action with no
|
|
75
|
-
// onOpenDrawer wired is dropped (not rendered) with a dev warning, same as a
|
|
76
|
-
// drawer-kind rowAction/toolbarAction without one.
|
|
77
|
-
function warnRelatedListEmptyStateDrawerDropped(actionId: string): void {
|
|
78
|
-
// biome-ignore lint/suspicious/noConsole: dev-time warning, same pattern as row-actions.ts
|
|
79
|
-
console.warn(
|
|
80
|
-
`[kumiko] relatedList emptyState.action "${actionId}" is kind: "drawer" but no onOpenDrawer was supplied — dropped.`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function RelatedListSection({
|
|
85
|
-
section,
|
|
86
|
-
parentId,
|
|
87
|
-
record,
|
|
88
|
-
featureName,
|
|
89
|
-
translate,
|
|
90
|
-
hideTitle,
|
|
91
|
-
grow,
|
|
92
|
-
onOpenDrawer,
|
|
93
|
-
actions,
|
|
94
|
-
}: {
|
|
95
|
-
readonly section: EditRelatedListSectionViewModel;
|
|
96
|
-
readonly parentId: string;
|
|
97
|
-
/** The enclosing projectionDetail's own record (the "Akte") — evaluates
|
|
98
|
-
* toolbarActions' `visible`/`params` (RelatedListToolbarAction), the same
|
|
99
|
-
* record header actions already use. */
|
|
100
|
-
readonly record: Readonly<Record<string, unknown>>;
|
|
101
|
-
readonly featureName: string;
|
|
102
|
-
readonly translate?: Translate;
|
|
103
|
-
readonly hideTitle?: boolean;
|
|
104
|
-
/** Under a fixed-height screen the tab panel chain stretches so the count footer sits at the bottom. */
|
|
105
|
-
readonly grow?: boolean;
|
|
106
|
-
/** Opens a drawer-kind rowAction (fw#2710). Supplied by the parent
|
|
107
|
-
* (ProjectionDetailBody), which owns schema + the actual Drawer render —
|
|
108
|
-
* this component only ever invokes the callback. */
|
|
109
|
-
readonly onOpenDrawer?: (
|
|
110
|
-
action: RowActionDrawer,
|
|
111
|
-
initialValues: Readonly<Record<string, unknown>> | undefined,
|
|
112
|
-
) => void;
|
|
113
|
-
/** section.actions, already resolved into buttons by the caller —
|
|
114
|
-
* rendered in the Section's title row. Only reachable in the standalone
|
|
115
|
-
* (non-`hideTitle`) branch below — `hideTitle` uses `FillContainer`, which
|
|
116
|
-
* has no title row to render actions into (see the comment on that branch). */
|
|
117
|
-
readonly actions?: ReactNode;
|
|
118
|
-
}): ReactNode {
|
|
119
|
-
const { Banner, Section, FillContainer, Text, Button, Dialog } = usePrimitives();
|
|
120
|
-
const [emptyStateActionError, setEmptyStateActionError] = useState<string | null>(null);
|
|
121
|
-
const t = useTranslation();
|
|
122
|
-
const effectiveTranslate = translate ?? t;
|
|
123
|
-
const nav = useNav();
|
|
124
|
-
const host = useReturnHost();
|
|
125
|
-
const dispatcher = useOptionalDispatcher();
|
|
126
|
-
const appFeatures = useAppFeatures();
|
|
127
|
-
const userRoles = useUserRoles();
|
|
128
|
-
const defaultEditScreen = useMemo(() => {
|
|
129
|
-
const targetEntity = section.rowClick?.entity;
|
|
130
|
-
if (targetEntity === undefined) return undefined;
|
|
131
|
-
return findEditScreenFor(targetEntity, appFeatures, userRoles);
|
|
132
|
-
}, [appFeatures, section.rowClick, userRoles]);
|
|
133
|
-
const defaultEditRowAction = useMemo(
|
|
134
|
-
() => buildDefaultEditRowAction(defaultEditScreen, section.rowClick?.idColumn ?? "id"),
|
|
135
|
-
[defaultEditScreen, section.rowClick],
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const entity = useMemo(() => synthesizeRelatedListEntity(section.columns), [section.columns]);
|
|
139
|
-
const listScreen = useMemo(
|
|
140
|
-
(): EntityListScreenDefinition => ({
|
|
141
|
-
// Empty id → RenderList's own toolbarTitle resolves to "" (its
|
|
142
|
-
// `screen:${id}.title` lookup misses and falls back to `id`) — this
|
|
143
|
-
// component renders the visible heading itself via `Section` below,
|
|
144
|
-
// so RenderList's toolbar carries none.
|
|
145
|
-
id: "",
|
|
146
|
-
type: "entityList",
|
|
147
|
-
entity: RELATED_LIST_PSEUDO_ENTITY,
|
|
148
|
-
columns: section.columns,
|
|
149
|
-
}),
|
|
150
|
-
[section.columns],
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Local state, not URL state: a section `id` is optional, so there is no
|
|
154
|
-
// stable URL key to namespace against — the section's sort is local for the
|
|
155
|
-
// same reason. Consequence: search/filters reset on reload.
|
|
156
|
-
const [search, setSearch] = useState("");
|
|
157
|
-
const [filters, setFilters] = useState<Readonly<Record<string, readonly string[]>>>({});
|
|
158
|
-
|
|
159
|
-
const facetSpecs = useMemo(
|
|
160
|
-
() => resolveProjectionFacetSpecs(section.facets, effectiveTranslate, featureName),
|
|
161
|
-
[section.facets, effectiveTranslate, featureName],
|
|
162
|
-
);
|
|
163
|
-
const [referenceFacetOptions, setReferenceFacetOptions] = useState<
|
|
164
|
-
Record<string, readonly ReferenceFacetOption[]>
|
|
165
|
-
>({});
|
|
166
|
-
const handleFacetOptions = useCallback(
|
|
167
|
-
(field: string, options: readonly ReferenceFacetOption[]) =>
|
|
168
|
-
setReferenceFacetOptions((prev) =>
|
|
169
|
-
prev[field] === options ? prev : { ...prev, [field]: options },
|
|
170
|
-
),
|
|
171
|
-
[],
|
|
172
|
-
);
|
|
173
|
-
const resolvedFacetSpecs = useMemo(
|
|
174
|
-
() => mergeReferenceFacetOptions(facetSpecs, referenceFacetOptions),
|
|
175
|
-
[facetSpecs, referenceFacetOptions],
|
|
176
|
-
);
|
|
177
|
-
const filterPayload = useMemo(
|
|
178
|
-
() =>
|
|
179
|
-
buildFilterPayload(
|
|
180
|
-
filters,
|
|
181
|
-
(field) => resolvedFacetSpecs.find((spec) => spec.field === field)?.type,
|
|
182
|
-
),
|
|
183
|
-
[filters, resolvedFacetSpecs],
|
|
184
|
-
);
|
|
185
|
-
const filterFacets = useMemo<DataTableFacet[]>(
|
|
186
|
-
() => buildFilterFacets(resolvedFacetSpecs),
|
|
187
|
-
[resolvedFacetSpecs],
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const payload = useMemo(
|
|
191
|
-
() => ({
|
|
192
|
-
// `parentFilter` sends the parent id as a server-side `filter` clause
|
|
193
|
-
// instead of a bespoke top-level key — the generic `<entity>:list`
|
|
194
|
-
// query understands `filter`, not an arbitrary `parentParam` key. Kept
|
|
195
|
-
// separate from `filterPayload` (user facets) below so a facet
|
|
196
|
-
// selection can never clear or overwrite it.
|
|
197
|
-
...(section.parentFilter !== undefined
|
|
198
|
-
? { filter: { field: section.parentFilter.field, op: "eq" as const, value: parentId } }
|
|
199
|
-
: { [section.parentParam ?? "id"]: parentId }),
|
|
200
|
-
...(section.pageSize !== undefined && { limit: section.pageSize }),
|
|
201
|
-
// Gated on the declared capability, not just on state carrying a value —
|
|
202
|
-
// same rule as ProjectionListBody: a param the bound query's Zod schema
|
|
203
|
-
// doesn't accept would 422 the whole section.
|
|
204
|
-
...(section.searchable === true && search !== "" && { search }),
|
|
205
|
-
...(section.facets !== undefined && filterPayload.length > 0 && { filters: filterPayload }),
|
|
206
|
-
}),
|
|
207
|
-
[
|
|
208
|
-
section.parentFilter,
|
|
209
|
-
section.parentParam,
|
|
210
|
-
section.pageSize,
|
|
211
|
-
section.searchable,
|
|
212
|
-
section.facets,
|
|
213
|
-
parentId,
|
|
214
|
-
search,
|
|
215
|
-
filterPayload,
|
|
216
|
-
],
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const rowsQuery = useQuery<PagedRows>(section.query, payload);
|
|
220
|
-
|
|
221
|
-
const onFilterChange = useCallback(
|
|
222
|
-
(field: string, values: readonly string[]) =>
|
|
223
|
-
setFilters((prev) => ({ ...prev, [field]: values })),
|
|
224
|
-
[],
|
|
225
|
-
);
|
|
226
|
-
const onFilterReset = useCallback(() => setFilters({}), []);
|
|
227
|
-
|
|
228
|
-
// Sorted client-side over the already-loaded rows — this section has no
|
|
229
|
-
// pager (see `payload` above: a one-shot fetch, no cursor/offset), so the
|
|
230
|
-
// loaded set already IS the full display set and there is no "other page"
|
|
231
|
-
// a client-side sort could misleadingly hide (fw#2722).
|
|
232
|
-
const [sort, setSort] = useState<ListSort | null>(section.defaultSort ?? null);
|
|
233
|
-
const sortAccessors = useMemo(() => {
|
|
234
|
-
const accessors: Record<string, (row: Readonly<Record<string, unknown>>) => string | number> =
|
|
235
|
-
{};
|
|
236
|
-
for (const col of section.columns) {
|
|
237
|
-
const field = normalizeListColumn(col).field;
|
|
238
|
-
accessors[field] = (row) => {
|
|
239
|
-
const value = row[field];
|
|
240
|
-
return typeof value === "number" ? value : String(value ?? "");
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
return accessors;
|
|
244
|
-
}, [section.columns]);
|
|
245
|
-
const sortedRows = useMemo(
|
|
246
|
-
() => sortByAccessor(rowsQuery.data?.rows ?? [], sort, sortAccessors),
|
|
247
|
-
[rowsQuery.data, sort, sortAccessors],
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
const rowClick = section.rowClick;
|
|
251
|
-
// A row-body click target comes from EITHER the legacy `rowClick` field OR
|
|
252
|
-
// a `rowActions` entry marked rowClick:true — the boot-validator rejects
|
|
253
|
-
// both being set, so this order is just a fallback, not a precedence rule.
|
|
254
|
-
const rowClickAction = section.rowActions?.find(
|
|
255
|
-
(a): a is RowActionNavigate => a.kind === "navigate" && a.rowClick === true,
|
|
256
|
-
);
|
|
257
|
-
const onRowClick =
|
|
258
|
-
rowClick !== undefined
|
|
259
|
-
? (row: ListRowViewModel) => {
|
|
260
|
-
const id = String(row.values[rowClick.idColumn ?? "id"] ?? "");
|
|
261
|
-
if (id === "") return;
|
|
262
|
-
navigateWithReturnTo(nav, { entity: rowClick.entity, id }, host);
|
|
263
|
-
}
|
|
264
|
-
: rowClickAction !== undefined
|
|
265
|
-
? (row: ListRowViewModel) => runProjectionRowNavigate(nav, rowClickAction, row, host)
|
|
266
|
-
: undefined;
|
|
267
|
-
|
|
268
|
-
// Same execution path as projectionList's rowActions (kumiko-screen.tsx) —
|
|
269
|
-
// a relatedList row has the identical "no guaranteed id field" shape a
|
|
270
|
-
// query-projection row has, so navigate/writeHandler dispatch is shared
|
|
271
|
-
// rather than a second implementation (fw editable-detail-screens).
|
|
272
|
-
const rowActions = useMemo(
|
|
273
|
-
() =>
|
|
274
|
-
buildProjectionRowActions({
|
|
275
|
-
rowActions: section.rowActions,
|
|
276
|
-
translate: effectiveTranslate,
|
|
277
|
-
dispatcher,
|
|
278
|
-
nav,
|
|
279
|
-
refetch: rowsQuery.refetch,
|
|
280
|
-
openDrawer: onOpenDrawer,
|
|
281
|
-
defaultEditRowAction,
|
|
282
|
-
host,
|
|
283
|
-
}),
|
|
284
|
-
[
|
|
285
|
-
section.rowActions,
|
|
286
|
-
effectiveTranslate,
|
|
287
|
-
dispatcher,
|
|
288
|
-
nav,
|
|
289
|
-
rowsQuery.refetch,
|
|
290
|
-
onOpenDrawer,
|
|
291
|
-
defaultEditRowAction,
|
|
292
|
-
host,
|
|
293
|
-
],
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
// Toolbar actions (e.g. "+ Create" above the tab table) — same schema
|
|
297
|
-
// and dispatch semantics as entityList/projectionList.
|
|
298
|
-
const toolbarActionButtons = useMemo(
|
|
299
|
-
() =>
|
|
300
|
-
buildProjectionToolbarActions({
|
|
301
|
-
toolbarActions: section.toolbarActions,
|
|
302
|
-
translate: effectiveTranslate,
|
|
303
|
-
dispatcher,
|
|
304
|
-
nav,
|
|
305
|
-
refetch: rowsQuery.refetch,
|
|
306
|
-
navigatePrefill:
|
|
307
|
-
section.parentFilter !== undefined
|
|
308
|
-
? { [section.parentFilter.field]: parentId }
|
|
309
|
-
: { [section.parentParam ?? "id"]: parentId },
|
|
310
|
-
record,
|
|
311
|
-
host,
|
|
312
|
-
...(onOpenDrawer !== undefined && {
|
|
313
|
-
openDrawer: (action) => onOpenDrawer(action, undefined),
|
|
314
|
-
}),
|
|
315
|
-
}),
|
|
316
|
-
[
|
|
317
|
-
section.toolbarActions,
|
|
318
|
-
section.parentParam,
|
|
319
|
-
section.parentFilter,
|
|
320
|
-
parentId,
|
|
321
|
-
record,
|
|
322
|
-
effectiveTranslate,
|
|
323
|
-
dispatcher,
|
|
324
|
-
nav,
|
|
325
|
-
host,
|
|
326
|
-
rowsQuery.refetch,
|
|
327
|
-
onOpenDrawer,
|
|
328
|
-
],
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// emptyState.action is a RowAction — the same record-bound action shape
|
|
332
|
-
// (and evalRowExtractor payload/params evaluation) screen-level and
|
|
333
|
-
// section-level actions already use via buildRecordActions, NOT the
|
|
334
|
-
// toolbar-action builder (whose `payload` is a literal Record, not a
|
|
335
|
-
// RowFieldExtractor — casting a RowAction into it would silently skip the
|
|
336
|
-
// declared `pick`/`map` evaluation). "Row" here is the parent record: a
|
|
337
|
-
// relatedList's emptyState has no row of its own.
|
|
338
|
-
const emptyStateActionButton = useMemo(() => {
|
|
339
|
-
const action = section.emptyState?.action;
|
|
340
|
-
if (action === undefined) return undefined;
|
|
341
|
-
if (action.kind === "drawer" && onOpenDrawer === undefined) {
|
|
342
|
-
warnRelatedListEmptyStateDrawerDropped(action.id);
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
return buildRecordActions({
|
|
346
|
-
actions: [action],
|
|
347
|
-
record,
|
|
348
|
-
translate: effectiveTranslate,
|
|
349
|
-
nav,
|
|
350
|
-
host,
|
|
351
|
-
dispatcher,
|
|
352
|
-
openDrawer: onOpenDrawer ?? (() => {}),
|
|
353
|
-
onWriteSuccess: rowsQuery.refetch,
|
|
354
|
-
})?.[0];
|
|
355
|
-
}, [
|
|
356
|
-
section.emptyState,
|
|
357
|
-
record,
|
|
358
|
-
effectiveTranslate,
|
|
359
|
-
nav,
|
|
360
|
-
host,
|
|
361
|
-
dispatcher,
|
|
362
|
-
onOpenDrawer,
|
|
363
|
-
rowsQuery.refetch,
|
|
364
|
-
]);
|
|
365
|
-
const emptyStateContent =
|
|
366
|
-
section.emptyState !== undefined ? (
|
|
367
|
-
<>
|
|
368
|
-
<Text>{section.emptyState.title}</Text>
|
|
369
|
-
{section.emptyState.description !== undefined && (
|
|
370
|
-
<Text variant="small">{section.emptyState.description}</Text>
|
|
371
|
-
)}
|
|
372
|
-
{emptyStateActionButton !== undefined && (
|
|
373
|
-
<RenderEditActionButton
|
|
374
|
-
action={emptyStateActionButton}
|
|
375
|
-
Button={Button}
|
|
376
|
-
Dialog={Dialog}
|
|
377
|
-
onError={setEmptyStateActionError}
|
|
378
|
-
/>
|
|
379
|
-
)}
|
|
380
|
-
</>
|
|
381
|
-
) : undefined;
|
|
382
|
-
|
|
383
|
-
// A truncated fetch means `sortedRows` is a sort of a partial set, not of
|
|
384
|
-
// the full related-row set — the client-side sort above (or even plain
|
|
385
|
-
// unsorted display) would silently claim "these are the top N" when they
|
|
386
|
-
// are really just "the first N in the server's own order" (fw#2722 review).
|
|
387
|
-
// `nextCursor` is exactly how the paged envelope marks that: non-null means
|
|
388
|
-
// more rows exist server-side beyond what was fetched.
|
|
389
|
-
// A handler that omits `nextCursor` (or reports a `total` the fetched rows
|
|
390
|
-
// already cover) has no further rows — only a real cursor marks truncation.
|
|
391
|
-
const truncated =
|
|
392
|
-
rowsQuery.data !== null &&
|
|
393
|
-
typeof rowsQuery.data.nextCursor === "string" &&
|
|
394
|
-
(rowsQuery.data.total === undefined || rowsQuery.data.total > rowsQuery.data.rows.length);
|
|
395
|
-
|
|
396
|
-
const content =
|
|
397
|
-
rowsQuery.loading && rowsQuery.data === null ? (
|
|
398
|
-
<Banner padded variant="loading" testId="related-list-loading">
|
|
399
|
-
Loading…
|
|
400
|
-
</Banner>
|
|
401
|
-
) : rowsQuery.error ? (
|
|
402
|
-
<Banner padded variant="error" testId="related-list-error">
|
|
403
|
-
{dispatcherErrorText(rowsQuery.error, effectiveTranslate)}
|
|
404
|
-
</Banner>
|
|
405
|
-
) : (
|
|
406
|
-
<>
|
|
407
|
-
{truncated && (
|
|
408
|
-
<Banner variant="info" testId="related-list-truncated">
|
|
409
|
-
{effectiveTranslate("kumiko.list.related-list-truncated", {
|
|
410
|
-
count: sortedRows.length,
|
|
411
|
-
})}
|
|
412
|
-
</Banner>
|
|
413
|
-
)}
|
|
414
|
-
{emptyStateActionError !== null && (
|
|
415
|
-
<Banner variant="error" testId="related-list-empty-state-action-error">
|
|
416
|
-
{emptyStateActionError}
|
|
417
|
-
</Banner>
|
|
418
|
-
)}
|
|
419
|
-
<PageHeaderSlotAvailableProvider value={false}>
|
|
420
|
-
<RenderList
|
|
421
|
-
screen={listScreen}
|
|
422
|
-
entity={entity}
|
|
423
|
-
rows={sortedRows}
|
|
424
|
-
featureName={featureName}
|
|
425
|
-
translate={effectiveTranslate}
|
|
426
|
-
{...(section.description !== undefined && { description: section.description })}
|
|
427
|
-
{...(section.itemNoun !== undefined && { itemNounKey: section.itemNoun })}
|
|
428
|
-
sort={sort}
|
|
429
|
-
onSortChange={setSort}
|
|
430
|
-
{...(section.searchable === true && {
|
|
431
|
-
searchable: true,
|
|
432
|
-
searchValue: search,
|
|
433
|
-
onSearchChange: setSearch,
|
|
434
|
-
})}
|
|
435
|
-
{...(filterFacets.length > 0 && {
|
|
436
|
-
filterFacets,
|
|
437
|
-
filterValues: filters,
|
|
438
|
-
onFilterChange,
|
|
439
|
-
onFilterReset,
|
|
440
|
-
})}
|
|
441
|
-
{...(onRowClick !== undefined && { onRowClick })}
|
|
442
|
-
{...(rowActions !== undefined && { rowActions })}
|
|
443
|
-
{...(toolbarActionButtons !== undefined && { toolbarActions: toolbarActionButtons })}
|
|
444
|
-
{...(emptyStateContent !== undefined && { emptyState: emptyStateContent })}
|
|
445
|
-
{...(hideTitle === true && {
|
|
446
|
-
scrollBody: true,
|
|
447
|
-
screenPadding: false,
|
|
448
|
-
chromeless: true,
|
|
449
|
-
})}
|
|
450
|
-
/>
|
|
451
|
-
</PageHeaderSlotAvailableProvider>
|
|
452
|
-
</>
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
// hideTitle (tabs mode) → the caller (render-edit.tsx) already wraps this
|
|
456
|
-
// component's own return value in the same Card frame every other tab
|
|
457
|
-
// kind gets — no card here, or the two would nest. `scrollBody` caps the
|
|
458
|
-
// table to the panel height; `FillContainer` is this section's link in the
|
|
459
|
-
// fillHeight flex chain that Card's `options.fillHeight` continues (see
|
|
460
|
-
// render-edit.tsx). A platform primitive (not a raw `<div>`) because
|
|
461
|
-
// `renderer` stays DOM-free. Stacked (non-tabs) sections keep their own
|
|
462
|
-
// Section card and document-flow height since they render a visible title
|
|
463
|
-
// and aren't confined to a tab panel.
|
|
464
|
-
const bridges = <ReferenceFacetBridges specs={facetSpecs} onOptions={handleFacetOptions} />;
|
|
465
|
-
|
|
466
|
-
if (hideTitle) {
|
|
467
|
-
return (
|
|
468
|
-
<>
|
|
469
|
-
{bridges}
|
|
470
|
-
{FillContainer !== undefined ? (
|
|
471
|
-
<FillContainer {...(grow === true && { grow: true })}>{content}</FillContainer>
|
|
472
|
-
) : (
|
|
473
|
-
content
|
|
474
|
-
)}
|
|
475
|
-
</>
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return (
|
|
480
|
-
<>
|
|
481
|
-
{bridges}
|
|
482
|
-
<Section
|
|
483
|
-
title={section.title}
|
|
484
|
-
{...(actions !== undefined && { actions })}
|
|
485
|
-
testId={`related-list-${section.title}`}
|
|
486
|
-
>
|
|
487
|
-
{content}
|
|
488
|
-
</Section>
|
|
489
|
-
</>
|
|
490
|
-
);
|
|
491
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import type { usePrimitives } from "../primitives";
|
|
4
|
-
import type { RenderEditAction } from "./render-edit-types";
|
|
5
|
-
|
|
6
|
-
// Same rule as RowActionWriteHandler: "danger" forces a confirm even
|
|
7
|
-
// without an explicit confirm key — unless `confirmRequired` overrides it
|
|
8
|
-
// (schema-driven navigate/drawer actions, where the target form is itself
|
|
9
|
-
// the confirmation).
|
|
10
|
-
export function needsActionConfirm(action: RenderEditAction): boolean {
|
|
11
|
-
return action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function RenderEditActionConfirmDialog({
|
|
15
|
-
action,
|
|
16
|
-
open,
|
|
17
|
-
onOpenChange,
|
|
18
|
-
onConfirm,
|
|
19
|
-
Dialog,
|
|
20
|
-
}: {
|
|
21
|
-
readonly action: RenderEditAction;
|
|
22
|
-
readonly open: boolean;
|
|
23
|
-
readonly onOpenChange: (open: boolean) => void;
|
|
24
|
-
readonly onConfirm: () => void | Promise<void>;
|
|
25
|
-
readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
|
|
26
|
-
}): ReactNode {
|
|
27
|
-
return (
|
|
28
|
-
<Dialog
|
|
29
|
-
open={open}
|
|
30
|
-
onOpenChange={onOpenChange}
|
|
31
|
-
title={action.label}
|
|
32
|
-
{...(action.confirm !== undefined && { description: action.confirm })}
|
|
33
|
-
confirmLabel={action.confirmLabel ?? action.label}
|
|
34
|
-
{...(action.style === "danger" && { variant: "danger" as const })}
|
|
35
|
-
onConfirm={onConfirm}
|
|
36
|
-
testId={`render-edit-action-${action.id}-dialog`}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// One header action + its own busy/confirm state — same pattern as
|
|
42
|
-
// render-list.tsx's ToolbarActionView (each RenderEditAction is
|
|
43
|
-
// independently bound by the caller, there is no shared trigger pipeline
|
|
44
|
-
// to hook into like the built-in onDelete/onSubmit paths have).
|
|
45
|
-
export function RenderEditActionButton({
|
|
46
|
-
action,
|
|
47
|
-
iconOnly = false,
|
|
48
|
-
hideIcon = false,
|
|
49
|
-
Button,
|
|
50
|
-
Dialog,
|
|
51
|
-
onError,
|
|
52
|
-
}: {
|
|
53
|
-
readonly action: RenderEditAction;
|
|
54
|
-
/** Group-level collapse (see `shouldRenderActionsIconOnly`) — only takes
|
|
55
|
-
* effect when this action actually resolved an icon. */
|
|
56
|
-
readonly iconOnly?: boolean;
|
|
57
|
-
/** Record-header actions are text-only; the icon still shows when collapsed to icon-only. */
|
|
58
|
-
readonly hideIcon?: boolean;
|
|
59
|
-
readonly Button: ReturnType<typeof usePrimitives>["Button"];
|
|
60
|
-
readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
|
|
61
|
-
readonly onError: (text: string | null) => void;
|
|
62
|
-
}): ReactNode {
|
|
63
|
-
const [busy, setBusy] = useState(false);
|
|
64
|
-
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
65
|
-
|
|
66
|
-
const trigger = async (): Promise<void> => {
|
|
67
|
-
setBusy(true);
|
|
68
|
-
onError(null);
|
|
69
|
-
try {
|
|
70
|
-
await action.onPress();
|
|
71
|
-
} catch (e) {
|
|
72
|
-
onError(e instanceof Error ? e.message : String(e));
|
|
73
|
-
} finally {
|
|
74
|
-
setBusy(false);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const variant = action.style ?? "secondary";
|
|
79
|
-
const showIconOnly = iconOnly && action.icon !== undefined;
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<>
|
|
83
|
-
<Button
|
|
84
|
-
type="button"
|
|
85
|
-
variant={variant}
|
|
86
|
-
loading={busy}
|
|
87
|
-
{...(action.icon !== undefined && (!hideIcon || showIconOnly) && { icon: action.icon })}
|
|
88
|
-
{...(showIconOnly && { size: "icon" as const, ariaLabel: action.label })}
|
|
89
|
-
onClick={() => {
|
|
90
|
-
if (needsActionConfirm(action)) {
|
|
91
|
-
setConfirmOpen(true);
|
|
92
|
-
} else {
|
|
93
|
-
void trigger();
|
|
94
|
-
}
|
|
95
|
-
}}
|
|
96
|
-
testId={`render-edit-action-${action.id}`}
|
|
97
|
-
>
|
|
98
|
-
{showIconOnly ? null : action.label}
|
|
99
|
-
</Button>
|
|
100
|
-
<RenderEditActionConfirmDialog
|
|
101
|
-
action={action}
|
|
102
|
-
open={confirmOpen}
|
|
103
|
-
onOpenChange={setConfirmOpen}
|
|
104
|
-
onConfirm={trigger}
|
|
105
|
-
Dialog={Dialog}
|
|
106
|
-
/>
|
|
107
|
-
</>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { EditSectionViewModel, SubmitResult } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
|
|
3
|
-
// Hides the Save button when no field is editable and no extension section
|
|
4
|
-
// contributes to the composed form submit. Extensions that persist via their
|
|
5
|
-
// own dispatcher writes must not opt in (fw#2359).
|
|
6
|
-
// Explicit-positive per kind (653/1), not `s.kind !== "fields"` — a future
|
|
7
|
-
// third EditSectionViewModel member would otherwise default to "editable"
|
|
8
|
-
// without anyone deciding that on purpose.
|
|
9
|
-
export function hasEditableSection(sections: readonly EditSectionViewModel[]): boolean {
|
|
10
|
-
return sections.some(
|
|
11
|
-
(s) =>
|
|
12
|
-
(s.kind === "extension" && s.contributesToFormSubmit) ||
|
|
13
|
-
(s.kind === "fields" && s.visible && s.fields.some((f) => !f.readOnly && f.visible)),
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Single source of truth for the extension-section entity-id. The section mount
|
|
18
|
-
// and persistExtensions MUST resolve the same id — otherwise a section mounts
|
|
19
|
-
// editable against one id (vm.id) while the persist step writes to (or skips)
|
|
20
|
-
// another (null), silently dropping the user's input. An explicit `null` prop
|
|
21
|
-
// forces "no entity" (no extension persistence); an omitted prop (undefined)
|
|
22
|
-
// falls back to vm.id (= values["id"]), which the update form carries for the
|
|
23
|
-
// existing row, so editing custom fields on that row actually persists.
|
|
24
|
-
export function resolveExtensionEntityId(
|
|
25
|
-
entityIdProp: string | null | undefined,
|
|
26
|
-
vmId: string | null,
|
|
27
|
-
): string | null {
|
|
28
|
-
return entityIdProp !== undefined ? entityIdProp : vmId;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// After a submit, decide whether to invoke the caller's onSubmit. The success
|
|
32
|
-
// callback typically navigates away, which unmounts the extension-error banner.
|
|
33
|
-
// Suppress the callback ONLY when the entity write succeeded but an extension-
|
|
34
|
-
// section persist failed: the user must stay on the form to see the banner and
|
|
35
|
-
// retry. Every other case still notifies the caller — entity failures and
|
|
36
|
-
// validation blocks carry information the caller needs.
|
|
37
|
-
export function shouldNotifyCaller(
|
|
38
|
-
result: SubmitResult<unknown>,
|
|
39
|
-
extensionsPersisted: boolean,
|
|
40
|
-
): boolean {
|
|
41
|
-
return !(result.isSuccess && !extensionsPersisted);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Matched on the first path segment (embedded-list rows are dotted, e.g. `tasks.2.title`).
|
|
45
|
-
// No issues at all counts as unmatched, so the caller keeps the banner.
|
|
46
|
-
export function hasIssueWithoutRenderedField(
|
|
47
|
-
issuePaths: readonly string[],
|
|
48
|
-
sections: readonly EditSectionViewModel[],
|
|
49
|
-
): boolean {
|
|
50
|
-
if (issuePaths.length === 0) return true;
|
|
51
|
-
const renderedFields = new Set(
|
|
52
|
-
sections
|
|
53
|
-
.filter((s) => s.kind === "fields" && (s.visible || s.fields.length === 0))
|
|
54
|
-
.flatMap((s) => (s.kind === "fields" ? s.fields.filter((f) => f.visible) : []))
|
|
55
|
-
.map((f) => f.field),
|
|
56
|
-
);
|
|
57
|
-
return issuePaths.some((path) => !renderedFields.has(path.split(".")[0] ?? path));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Extension, relatedList and writeForm sections skip the `fields` filter
|
|
61
|
-
// (writeForm's fields belong to its own independent form, not the host's);
|
|
62
|
-
// a `fields` section left with zero fields after filtering is dropped, not
|
|
63
|
-
// rendered empty.
|
|
64
|
-
export function filterEditSections(
|
|
65
|
-
sections: readonly EditSectionViewModel[],
|
|
66
|
-
fieldsFilter: readonly string[] | undefined,
|
|
67
|
-
): readonly EditSectionViewModel[] {
|
|
68
|
-
if (fieldsFilter === undefined) return sections;
|
|
69
|
-
const filterSet = new Set(fieldsFilter);
|
|
70
|
-
const result: EditSectionViewModel[] = [];
|
|
71
|
-
for (const section of sections) {
|
|
72
|
-
if (
|
|
73
|
-
section.kind === "extension" ||
|
|
74
|
-
section.kind === "relatedList" ||
|
|
75
|
-
section.kind === "writeForm"
|
|
76
|
-
) {
|
|
77
|
-
result.push(section);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const fields = section.fields.filter((f) => filterSet.has(f.field));
|
|
81
|
-
if (fields.length === 0) continue;
|
|
82
|
-
result.push({ ...section, fields });
|
|
83
|
-
}
|
|
84
|
-
return result;
|
|
85
|
-
}
|