@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,165 +0,0 @@
|
|
|
1
|
-
// fw#2662: projectionDetail fields have no EntityDefinition to carry a real
|
|
2
|
-
// "reference" field type (the shim hardcodes every field as "text"), so a
|
|
3
|
-
// declared reference field used to render the raw id. This renders the real
|
|
4
|
-
// path (KumikoScreen -> ProjectionDetailScreen -> RenderEdit -> RenderField
|
|
5
|
-
// -> ReadOnlyReferenceValue) under a stub dispatcher.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
-
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
9
|
-
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
10
|
-
import { render, waitFor } from "@testing-library/react";
|
|
11
|
-
import type { ComponentType, ReactNode } from "react";
|
|
12
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
13
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
14
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
15
|
-
import { type CorePrimitives, PrimitivesProvider, type TextProps } from "../../primitives";
|
|
16
|
-
import type { FeatureSchema } from "../feature-schema";
|
|
17
|
-
import { KumikoScreen } from "../kumiko-screen";
|
|
18
|
-
import type { NavApi } from "../nav";
|
|
19
|
-
import { NavProvider } from "../nav";
|
|
20
|
-
|
|
21
|
-
const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
22
|
-
|
|
23
|
-
const noop = (): ReactNode => null;
|
|
24
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
25
|
-
// passChildren would drop testId — ReadOnlyReferenceValue/readOnlyDisplayText
|
|
26
|
-
// render their resolved text into `<Text testId="field-value-<field>">`, and
|
|
27
|
-
// the assertions below read it back by that testid.
|
|
28
|
-
const TestText: ComponentType<TextProps> = ({ children, testId }) => (
|
|
29
|
-
<span data-testid={testId}>{children}</span>
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const testPrimitives: CorePrimitives = {
|
|
33
|
-
Button: noop,
|
|
34
|
-
Banner: passChildren,
|
|
35
|
-
Field: passChildren,
|
|
36
|
-
Input: noop,
|
|
37
|
-
DataTable: noop,
|
|
38
|
-
Form: passChildren,
|
|
39
|
-
Section: passChildren,
|
|
40
|
-
Card: passChildren,
|
|
41
|
-
Grid: passChildren,
|
|
42
|
-
GridCell: passChildren,
|
|
43
|
-
Text: TestText,
|
|
44
|
-
Heading: noop,
|
|
45
|
-
Dialog: noop,
|
|
46
|
-
Modal: noop,
|
|
47
|
-
Lightbox: noop,
|
|
48
|
-
ConfigSourceBadge: noop,
|
|
49
|
-
ConfigCascadeView: noop,
|
|
50
|
-
Link: noop,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function stubDispatcher(): Dispatcher {
|
|
54
|
-
return {
|
|
55
|
-
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
56
|
-
query: (async (type: string) => {
|
|
57
|
-
if (type === "sessions:query:session:detail") {
|
|
58
|
-
return {
|
|
59
|
-
isSuccess: true,
|
|
60
|
-
data: { id: "sess-1", userId: "u-1", actedBy: SYSTEM_USER_ID, ip: "10.0.0.1" },
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
// user:user resolves through the tenant-scoped member directory, not the
|
|
64
|
-
// SystemAdmin-only user:query:user:list a TenantAdmin can't call (fw#3107).
|
|
65
|
-
if (type === "tenant:query:member-directory") {
|
|
66
|
-
return {
|
|
67
|
-
isSuccess: true,
|
|
68
|
-
data: { rows: [{ id: "u-1", label: "Jane Doe" }], nextCursor: null },
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return { isSuccess: true, data: { rows: [], nextCursor: null } };
|
|
72
|
-
}) as unknown as Dispatcher["query"],
|
|
73
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
74
|
-
statusStore: {
|
|
75
|
-
getState: () => "online",
|
|
76
|
-
subscribe: () => () => {},
|
|
77
|
-
} as unknown as Dispatcher["statusStore"],
|
|
78
|
-
async *stream() {},
|
|
79
|
-
pendingWrites: () => [],
|
|
80
|
-
pendingFiles: () => [],
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const detailScreen: ProjectionDetailScreenDefinition = {
|
|
85
|
-
id: "session-detail",
|
|
86
|
-
type: "projectionDetail",
|
|
87
|
-
query: "sessions:query:session:detail",
|
|
88
|
-
layout: {
|
|
89
|
-
sections: [
|
|
90
|
-
{
|
|
91
|
-
fields: [
|
|
92
|
-
{ field: "userId", refEntity: "user:user", refLabelField: "displayName" },
|
|
93
|
-
// Mirrors the audit-log detail actor field (fw#3103) — a system
|
|
94
|
-
// write has no read_users row for the bulk lookup to find.
|
|
95
|
-
{ field: "actedBy", refEntity: "user:user", refLabelField: "displayName" },
|
|
96
|
-
"ip",
|
|
97
|
-
],
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
function buildSchema(): FeatureSchema {
|
|
104
|
-
return {
|
|
105
|
-
featureName: "sessions",
|
|
106
|
-
entities: {},
|
|
107
|
-
screens: [detailScreen],
|
|
108
|
-
} as FeatureSchema;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const staticNav: NavApi = {
|
|
112
|
-
route: { screenId: "sessions:screen:session-detail" },
|
|
113
|
-
navigate: () => {},
|
|
114
|
-
replace: () => {},
|
|
115
|
-
hrefFor: () => "",
|
|
116
|
-
searchParams: {},
|
|
117
|
-
setSearchParams: () => {},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
function renderDetailScreen(): ReturnType<typeof render> {
|
|
121
|
-
return render(
|
|
122
|
-
<LocaleProvider
|
|
123
|
-
resolver={createStaticLocaleResolver({ locale: "en" })}
|
|
124
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
125
|
-
>
|
|
126
|
-
<DispatcherProvider dispatcher={stubDispatcher()}>
|
|
127
|
-
<NavProvider value={staticNav}>
|
|
128
|
-
<PrimitivesProvider value={testPrimitives}>
|
|
129
|
-
<KumikoScreen
|
|
130
|
-
schema={buildSchema()}
|
|
131
|
-
qn="sessions:screen:session-detail"
|
|
132
|
-
entityId="sess-1"
|
|
133
|
-
/>
|
|
134
|
-
</PrimitivesProvider>
|
|
135
|
-
</NavProvider>
|
|
136
|
-
</DispatcherProvider>
|
|
137
|
-
</LocaleProvider>,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
describe("projectionDetail reference field resolves labels (fw#2662)", () => {
|
|
142
|
-
test("a field with refEntity metadata shows the resolved display name instead of the raw id", async () => {
|
|
143
|
-
const { getByTestId } = renderDetailScreen();
|
|
144
|
-
|
|
145
|
-
await waitFor(() => {
|
|
146
|
-
expect(getByTestId("field-value-userId").textContent).toBe("Jane Doe");
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("SYSTEM_USER_ID resolves to the system label instead of the raw id (fw#3103)", async () => {
|
|
151
|
-
const { getByTestId } = renderDetailScreen();
|
|
152
|
-
|
|
153
|
-
await waitFor(() => {
|
|
154
|
-
expect(getByTestId("field-value-actedBy").textContent).toBe("System");
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("non-regression: a field without refEntity metadata renders its raw value unchanged", async () => {
|
|
159
|
-
const { getByTestId } = renderDetailScreen();
|
|
160
|
-
|
|
161
|
-
await waitFor(() => {
|
|
162
|
-
expect(getByTestId("field-value-ip").textContent).toBe("10.0.0.1");
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import {
|
|
4
|
-
synthesizeProjectionDetailEntity,
|
|
5
|
-
synthesizeProjectionDetailScreen,
|
|
6
|
-
} from "../projection-detail-shim";
|
|
7
|
-
|
|
8
|
-
// Regression for the writeForm extension (fw editable-detail-screens): the
|
|
9
|
-
// hard readOnly:true enforcement in synthesizeProjectionDetailScreen must
|
|
10
|
-
// still apply to every ordinary fields-section, unchanged — a writeForm
|
|
11
|
-
// section is the one deliberate exception (isFieldsEditSection excludes it,
|
|
12
|
-
// same as relatedList/extension), it must pass through editable.
|
|
13
|
-
describe("synthesizeProjectionDetailScreen", () => {
|
|
14
|
-
test("forces readOnly:true on a fields section, but leaves a writeForm section's own readOnly untouched", () => {
|
|
15
|
-
const screen: ProjectionDetailScreenDefinition = {
|
|
16
|
-
id: "order-detail",
|
|
17
|
-
type: "projectionDetail",
|
|
18
|
-
query: "orders:query:order:detail",
|
|
19
|
-
layout: {
|
|
20
|
-
sections: [
|
|
21
|
-
{ title: "Basics", fields: [{ field: "name", readOnly: false }] },
|
|
22
|
-
{
|
|
23
|
-
kind: "writeForm",
|
|
24
|
-
title: "Add note",
|
|
25
|
-
fieldDefs: { note: { type: "text" } },
|
|
26
|
-
fields: [{ field: "note", readOnly: false }],
|
|
27
|
-
handler: "orders:write:add-note",
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const result = synthesizeProjectionDetailScreen(screen);
|
|
34
|
-
|
|
35
|
-
const [fieldsSection, writeFormSection] = result.layout.sections;
|
|
36
|
-
if (fieldsSection === undefined || !("fields" in fieldsSection) || "kind" in fieldsSection) {
|
|
37
|
-
throw new Error("expected the first section to stay a plain fields section");
|
|
38
|
-
}
|
|
39
|
-
expect(fieldsSection.fields[0]).toMatchObject({ field: "name", readOnly: true });
|
|
40
|
-
|
|
41
|
-
if (writeFormSection === undefined || writeFormSection.kind !== "writeForm") {
|
|
42
|
-
throw new Error("expected the second section to stay kind: writeForm, untouched");
|
|
43
|
-
}
|
|
44
|
-
expect(writeFormSection.fields[0]).toEqual({ field: "note", readOnly: false });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("carries description through so RenderEdit can render it as the form subtitle (fw#2723)", () => {
|
|
48
|
-
const withDescription = synthesizeProjectionDetailScreen({
|
|
49
|
-
id: "order-detail",
|
|
50
|
-
type: "projectionDetail",
|
|
51
|
-
description: "Read-only view of a placed order.",
|
|
52
|
-
query: "orders:query:order:detail",
|
|
53
|
-
layout: { sections: [{ title: "Basics", fields: ["name"] }] },
|
|
54
|
-
});
|
|
55
|
-
expect(withDescription.description).toBe("Read-only view of a placed order.");
|
|
56
|
-
|
|
57
|
-
const withoutDescription = synthesizeProjectionDetailScreen({
|
|
58
|
-
id: "order-detail",
|
|
59
|
-
type: "projectionDetail",
|
|
60
|
-
query: "orders:query:order:detail",
|
|
61
|
-
layout: { sections: [{ title: "Basics", fields: ["name"] }] },
|
|
62
|
-
});
|
|
63
|
-
expect("description" in withoutDescription).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Regression for fw#2982: a section that puts its fields in `groups`
|
|
68
|
-
// instead of `fields` used to vanish entirely — synthesizeProjectionDetailEntity
|
|
69
|
-
// never registered the field names, so computeEditViewModel threw "references
|
|
70
|
-
// unknown field" for every one of them, and synthesizeProjectionDetailScreen
|
|
71
|
-
// let a group field's own readOnly value pass through unforced.
|
|
72
|
-
describe("groups-sections (fw#2982)", () => {
|
|
73
|
-
const groupsOnlyScreen: ProjectionDetailScreenDefinition = {
|
|
74
|
-
id: "contact-detail",
|
|
75
|
-
type: "projectionDetail",
|
|
76
|
-
query: "contacts:query:contact:detail",
|
|
77
|
-
layout: {
|
|
78
|
-
sections: [
|
|
79
|
-
{
|
|
80
|
-
title: "Stammdaten",
|
|
81
|
-
fields: [],
|
|
82
|
-
groups: [
|
|
83
|
-
{
|
|
84
|
-
title: "Kontakt",
|
|
85
|
-
fields: [{ field: "contactKind", readOnly: false }, "email"],
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
test("synthesizeProjectionDetailEntity registers every field declared only through groups", () => {
|
|
94
|
-
const entity = synthesizeProjectionDetailEntity(groupsOnlyScreen.layout);
|
|
95
|
-
expect(entity.fields["contactKind"]).toEqual({ type: "text" });
|
|
96
|
-
expect(entity.fields["email"]).toEqual({ type: "text" });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("synthesizeProjectionDetailScreen forces readOnly:true on a group field even when the author set readOnly:false", () => {
|
|
100
|
-
const result = synthesizeProjectionDetailScreen(groupsOnlyScreen);
|
|
101
|
-
const [section] = result.layout.sections;
|
|
102
|
-
if (section === undefined || !("fields" in section) || "kind" in section) {
|
|
103
|
-
throw new Error("expected the section to stay a plain fields section");
|
|
104
|
-
}
|
|
105
|
-
if (!("groups" in section) || section.groups === undefined) {
|
|
106
|
-
throw new Error("expected the section to keep its groups");
|
|
107
|
-
}
|
|
108
|
-
expect(section.groups[0]?.fields).toEqual([
|
|
109
|
-
{ field: "contactKind", readOnly: true },
|
|
110
|
-
{ field: "email", readOnly: true },
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("a screen mixing a plain fields section and a groups section resolves both, for both functions", () => {
|
|
115
|
-
const mixedScreen: ProjectionDetailScreenDefinition = {
|
|
116
|
-
id: "deposit-detail",
|
|
117
|
-
type: "projectionDetail",
|
|
118
|
-
query: "deposits:query:deposit:detail",
|
|
119
|
-
layout: {
|
|
120
|
-
sections: [
|
|
121
|
-
{ title: "Basics", fields: [{ field: "amount", readOnly: false }] },
|
|
122
|
-
{
|
|
123
|
-
title: "Parties",
|
|
124
|
-
fields: [],
|
|
125
|
-
groups: [{ title: "Tenant", fields: ["tenantName"] }],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const entity = synthesizeProjectionDetailEntity(mixedScreen.layout);
|
|
132
|
-
expect(entity.fields["amount"]).toEqual({ type: "text" });
|
|
133
|
-
expect(entity.fields["tenantName"]).toEqual({ type: "text" });
|
|
134
|
-
|
|
135
|
-
const result = synthesizeProjectionDetailScreen(mixedScreen);
|
|
136
|
-
const [fieldsSection, groupsSection] = result.layout.sections;
|
|
137
|
-
if (fieldsSection === undefined || !("fields" in fieldsSection) || "kind" in fieldsSection) {
|
|
138
|
-
throw new Error("expected the first section to stay a plain fields section");
|
|
139
|
-
}
|
|
140
|
-
expect(fieldsSection.fields[0]).toMatchObject({ field: "amount", readOnly: true });
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
groupsSection === undefined ||
|
|
144
|
-
!("groups" in groupsSection) ||
|
|
145
|
-
groupsSection.groups === undefined
|
|
146
|
-
) {
|
|
147
|
-
throw new Error("expected the second section to keep its groups");
|
|
148
|
-
}
|
|
149
|
-
expect(groupsSection.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// The boot-validator rejects fields+groups both non-empty on one section at
|
|
153
|
-
// boot time, so an author can never actually ship this shape — but the shim
|
|
154
|
-
// is not itself a validation pass, and this pins the union, not the
|
|
155
|
-
// either/or a naive `groups !== undefined ? grouped : fields` would take.
|
|
156
|
-
test("a single section carrying both fields and groups resolves both, for both functions", () => {
|
|
157
|
-
const bothScreen: ProjectionDetailScreenDefinition = {
|
|
158
|
-
id: "invoice-detail",
|
|
159
|
-
type: "projectionDetail",
|
|
160
|
-
query: "invoices:query:invoice:detail",
|
|
161
|
-
layout: {
|
|
162
|
-
sections: [
|
|
163
|
-
{
|
|
164
|
-
title: "Mixed",
|
|
165
|
-
fields: [{ field: "amount", readOnly: false }],
|
|
166
|
-
groups: [{ title: "Tenant", fields: ["tenantName"] }],
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const entity = synthesizeProjectionDetailEntity(bothScreen.layout);
|
|
173
|
-
expect(entity.fields["amount"]).toEqual({ type: "text" });
|
|
174
|
-
expect(entity.fields["tenantName"]).toEqual({ type: "text" });
|
|
175
|
-
|
|
176
|
-
const result = synthesizeProjectionDetailScreen(bothScreen);
|
|
177
|
-
const [section] = result.layout.sections;
|
|
178
|
-
if (section === undefined || !("fields" in section) || "kind" in section) {
|
|
179
|
-
throw new Error("expected the section to stay a plain fields section");
|
|
180
|
-
}
|
|
181
|
-
expect(section.fields[0]).toMatchObject({ field: "amount", readOnly: true });
|
|
182
|
-
|
|
183
|
-
if (!("groups" in section) || section.groups === undefined) {
|
|
184
|
-
throw new Error("expected the section to keep its groups");
|
|
185
|
-
}
|
|
186
|
-
expect(section.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// fw#2312: `singleton: true` marks a projectionDetail screen whose query
|
|
2
|
-
// determines the shown row from the caller's session/context instead of a
|
|
3
|
-
// row id in the path (a self-service "my profile"/"my data" screen has no
|
|
4
|
-
// row to link to). Before this flag, ProjectionDetailBody always rendered
|
|
5
|
-
// the "needs a row id in the path" error banner when `entityId` was
|
|
6
|
-
// undefined (kumiko-screen.tsx) — both user-profile and user-data-rights'
|
|
7
|
-
// privacy-center screen hit exactly that, unrenderable, until the flag
|
|
8
|
-
// existed (fw#2312 bug report). This proves the fix, the no-flag regression
|
|
9
|
-
// case stays covered, and that a stray/spoofed path id never reaches the
|
|
10
|
-
// query under singleton (renderer-side half of the security requirement —
|
|
11
|
-
// the boot-validator half lives in framework's projection-detail-
|
|
12
|
-
// singleton.test.ts).
|
|
13
|
-
|
|
14
|
-
import { describe, expect, test } from "bun:test";
|
|
15
|
-
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
16
|
-
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
17
|
-
import { render, waitFor } from "@testing-library/react";
|
|
18
|
-
import type { ComponentType, ReactNode } from "react";
|
|
19
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
20
|
-
import { UserRolesProvider } from "../../context/user-roles-context";
|
|
21
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
22
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
23
|
-
import {
|
|
24
|
-
type BannerProps,
|
|
25
|
-
type CorePrimitives,
|
|
26
|
-
type FormProps,
|
|
27
|
-
PrimitivesProvider,
|
|
28
|
-
} from "../../primitives";
|
|
29
|
-
import { AppFeaturesProvider } from "../app-features-context";
|
|
30
|
-
import type { FeatureSchema } from "../feature-schema";
|
|
31
|
-
import { KumikoScreen } from "../kumiko-screen";
|
|
32
|
-
import type { NavApi } from "../nav";
|
|
33
|
-
import { NavProvider } from "../nav";
|
|
34
|
-
|
|
35
|
-
const noop = (): ReactNode => null;
|
|
36
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
37
|
-
|
|
38
|
-
const FormWithActions: ComponentType<FormProps> = ({ children }) => (
|
|
39
|
-
<div data-testid="form-body">{children}</div>
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const TestBanner: ComponentType<BannerProps> = ({ children, testId }) => (
|
|
43
|
-
<div data-testid={testId}>{children}</div>
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const testPrimitives: CorePrimitives = {
|
|
47
|
-
Button: noop,
|
|
48
|
-
Banner: TestBanner,
|
|
49
|
-
Field: passChildren,
|
|
50
|
-
Input: noop,
|
|
51
|
-
DataTable: noop,
|
|
52
|
-
Form: FormWithActions,
|
|
53
|
-
Section: passChildren,
|
|
54
|
-
Card: passChildren,
|
|
55
|
-
Grid: passChildren,
|
|
56
|
-
GridCell: passChildren,
|
|
57
|
-
Text: passChildren,
|
|
58
|
-
Heading: noop,
|
|
59
|
-
Dialog: noop,
|
|
60
|
-
Modal: noop,
|
|
61
|
-
Lightbox: noop,
|
|
62
|
-
ConfigSourceBadge: noop,
|
|
63
|
-
ConfigCascadeView: noop,
|
|
64
|
-
Link: noop,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function stubDispatcher(
|
|
68
|
-
record: Readonly<Record<string, unknown>> | null,
|
|
69
|
-
queries: Array<{ type: string; payload: unknown }>,
|
|
70
|
-
): Dispatcher {
|
|
71
|
-
return {
|
|
72
|
-
write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
|
|
73
|
-
query: (async (type: string, payload: unknown) => {
|
|
74
|
-
queries.push({ type, payload });
|
|
75
|
-
return { isSuccess: true, data: record };
|
|
76
|
-
}) as unknown as Dispatcher["query"],
|
|
77
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
78
|
-
statusStore: {
|
|
79
|
-
getState: () => "online",
|
|
80
|
-
subscribe: () => () => {},
|
|
81
|
-
} as unknown as Dispatcher["statusStore"],
|
|
82
|
-
async *stream() {},
|
|
83
|
-
pendingWrites: () => [],
|
|
84
|
-
pendingFiles: () => [],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function meScreen(
|
|
89
|
-
overrides?: Partial<ProjectionDetailScreenDefinition>,
|
|
90
|
-
): FeatureSchema["screens"][number] {
|
|
91
|
-
return {
|
|
92
|
-
id: "profile",
|
|
93
|
-
type: "projectionDetail",
|
|
94
|
-
query: "app:query:me",
|
|
95
|
-
layout: { sections: [{ title: "s", fields: ["name"] }] },
|
|
96
|
-
...overrides,
|
|
97
|
-
} as FeatureSchema["screens"][number];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderScreen(opts: {
|
|
101
|
-
readonly screen: FeatureSchema["screens"][number];
|
|
102
|
-
readonly entityId?: string;
|
|
103
|
-
readonly queries: Array<{ type: string; payload: unknown }>;
|
|
104
|
-
readonly record?: Readonly<Record<string, unknown>> | null;
|
|
105
|
-
}): ReturnType<typeof render> {
|
|
106
|
-
const schema: FeatureSchema = { featureName: "app", entities: {}, screens: [opts.screen] };
|
|
107
|
-
const navApi: NavApi = {
|
|
108
|
-
route: { screenId: "app:screen:profile" },
|
|
109
|
-
navigate: () => {},
|
|
110
|
-
replace: () => {},
|
|
111
|
-
hrefFor: () => "",
|
|
112
|
-
searchParams: {},
|
|
113
|
-
setSearchParams: () => {},
|
|
114
|
-
};
|
|
115
|
-
return render(
|
|
116
|
-
<LocaleProvider
|
|
117
|
-
resolver={createStaticLocaleResolver({ locale: "de-DE" })}
|
|
118
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
119
|
-
>
|
|
120
|
-
<DispatcherProvider
|
|
121
|
-
dispatcher={stubDispatcher(
|
|
122
|
-
opts.record !== undefined ? opts.record : { id: "u1", name: "Ada" },
|
|
123
|
-
opts.queries,
|
|
124
|
-
)}
|
|
125
|
-
>
|
|
126
|
-
<AppFeaturesProvider features={[schema]}>
|
|
127
|
-
<UserRolesProvider roles={[]}>
|
|
128
|
-
<NavProvider value={navApi}>
|
|
129
|
-
<PrimitivesProvider value={testPrimitives}>
|
|
130
|
-
<KumikoScreen
|
|
131
|
-
schema={schema}
|
|
132
|
-
qn="app:screen:profile"
|
|
133
|
-
{...(opts.entityId !== undefined && { entityId: opts.entityId })}
|
|
134
|
-
/>
|
|
135
|
-
</PrimitivesProvider>
|
|
136
|
-
</NavProvider>
|
|
137
|
-
</UserRolesProvider>
|
|
138
|
-
</AppFeaturesProvider>
|
|
139
|
-
</DispatcherProvider>
|
|
140
|
-
</LocaleProvider>,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
describe("projectionDetail singleton (fw#2312)", () => {
|
|
145
|
-
test("singleton + no path id renders the record instead of the missing-id banner", async () => {
|
|
146
|
-
const queries: Array<{ type: string; payload: unknown }> = [];
|
|
147
|
-
const { queryByTestId, getByTestId } = renderScreen({
|
|
148
|
-
screen: meScreen({ singleton: true }),
|
|
149
|
-
queries,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
await waitFor(() => expect(getByTestId("form-body")).toBeTruthy());
|
|
153
|
-
expect(queryByTestId("kumiko-screen-projection-detail-missing-id")).toBeNull();
|
|
154
|
-
expect(queries).toEqual([{ type: "app:query:me", payload: {} }]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("no singleton flag + no path id still shows the missing-id banner (no regression)", async () => {
|
|
158
|
-
const queries: Array<{ type: string; payload: unknown }> = [];
|
|
159
|
-
const { getByTestId } = renderScreen({ screen: meScreen(), queries });
|
|
160
|
-
|
|
161
|
-
await waitFor(() =>
|
|
162
|
-
expect(getByTestId("kumiko-screen-projection-detail-missing-id")).toBeTruthy(),
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("singleton ignores a stray path id — the query never receives it", async () => {
|
|
167
|
-
const queries: Array<{ type: string; payload: unknown }> = [];
|
|
168
|
-
const { getByTestId } = renderScreen({
|
|
169
|
-
screen: meScreen({ singleton: true }),
|
|
170
|
-
entityId: "attacker-controlled-id",
|
|
171
|
-
queries,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
await waitFor(() => expect(getByTestId("form-body")).toBeTruthy());
|
|
175
|
-
expect(queries).toEqual([{ type: "app:query:me", payload: {} }]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("singleton + record-not-found banner never shows the (ignored) path id", async () => {
|
|
179
|
-
const queries: Array<{ type: string; payload: unknown }> = [];
|
|
180
|
-
const { getByTestId } = renderScreen({
|
|
181
|
-
screen: meScreen({ singleton: true }),
|
|
182
|
-
entityId: "99",
|
|
183
|
-
record: null,
|
|
184
|
-
queries,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const banner = await waitFor(() => getByTestId("kumiko-screen-record-missing"));
|
|
188
|
-
expect(banner.textContent).toBe("Record not found.");
|
|
189
|
-
expect(banner.textContent).not.toContain("99");
|
|
190
|
-
});
|
|
191
|
-
});
|