@cosmicdrift/kumiko-renderer 0.384.0 → 0.385.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.
Files changed (97) hide show
  1. package/dist/app/action-form-body.d.ts +50 -0
  2. package/dist/app/action-form-body.js +275 -0
  3. package/dist/app/client-actions.js +2 -2
  4. package/dist/app/config-edit-body.d.ts +9 -0
  5. package/dist/app/config-edit-body.js +340 -0
  6. package/dist/app/entity-edit-prefill.d.ts +27 -0
  7. package/dist/app/entity-edit-prefill.js +377 -0
  8. package/dist/app/entity-edit-screen.d.ts +11 -0
  9. package/dist/app/entity-edit-screen.js +395 -0
  10. package/dist/app/entity-list-screen.d.ts +10 -0
  11. package/dist/app/entity-list-screen.js +547 -0
  12. package/dist/app/kumiko-screen.d.ts +1 -36
  13. package/dist/app/kumiko-screen.js +16 -2855
  14. package/dist/app/projection-detail-body.d.ts +12 -0
  15. package/dist/app/projection-detail-body.js +464 -0
  16. package/dist/app/projection-detail-shim.d.ts +1 -1
  17. package/dist/app/projection-detail-shim.js +5 -1
  18. package/dist/app/projection-list-body.d.ts +10 -0
  19. package/dist/app/projection-list-body.js +245 -0
  20. package/dist/app/projection-list-shim.d.ts +2 -1
  21. package/dist/app/projection-list-shim.js +4 -0
  22. package/dist/app/reference-facet-bridge.js +2 -2
  23. package/dist/app/row-actions.js +3 -4
  24. package/dist/app/screen-shared.d.ts +72 -0
  25. package/dist/app/screen-shared.js +325 -0
  26. package/dist/app/secret-mint-body.js +3 -3
  27. package/dist/components/edit-mounts.d.ts +33 -0
  28. package/dist/components/edit-mounts.js +53 -0
  29. package/dist/components/error-boundary.js +2 -2
  30. package/dist/components/reference-create-dialog.js +1 -1
  31. package/dist/components/related-list-section.js +2 -2
  32. package/dist/components/render-edit-helpers.d.ts +29 -0
  33. package/dist/components/render-edit-helpers.js +121 -0
  34. package/dist/components/render-edit.d.ts +1 -4
  35. package/dist/components/render-edit.js +41 -443
  36. package/dist/components/render-field.js +3 -4
  37. package/dist/components/render-list.js +2 -2
  38. package/dist/components/use-wizard-form-draft.d.ts +51 -0
  39. package/dist/components/use-wizard-form-draft.js +291 -0
  40. package/dist/dev-console.d.ts +3 -0
  41. package/dist/dev-console.js +15 -0
  42. package/dist/headless/contracts/primitives.js +14 -17
  43. package/dist/headless/format/index.js +3 -3
  44. package/dist/headless/view-model/edit.d.ts +1 -1
  45. package/dist/headless/view-model/edit.js +9 -16
  46. package/dist/headless/view-model/list.d.ts +1 -1
  47. package/dist/headless/view-model/list.js +3 -8
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +5 -5
  50. package/dist/live/error-mapping.js +3 -4
  51. package/dist/web/app/create-app.js +5 -8
  52. package/dist/web/app/dashboard-body.js +4 -599
  53. package/dist/web/app/dashboard-panels.d.ts +53 -0
  54. package/dist/web/app/dashboard-panels.js +373 -0
  55. package/dist/web/app/dashboard-shared.d.ts +90 -0
  56. package/dist/web/app/dashboard-shared.js +171 -0
  57. package/dist/web/app/dashboard-text.d.ts +6 -0
  58. package/dist/web/app/dashboard-text.js +65 -0
  59. package/dist/web/layout/nav-tree.js +3 -4
  60. package/dist/web/layout/target-resolver-stub.js +2 -2
  61. package/dist/web/primitives/banner.d.ts +4 -0
  62. package/dist/web/primitives/banner.js +28 -0
  63. package/dist/web/primitives/button.d.ts +7 -0
  64. package/dist/web/primitives/button.js +48 -0
  65. package/dist/web/primitives/card-styles.d.ts +8 -0
  66. package/dist/web/primitives/card-styles.js +18 -0
  67. package/dist/web/primitives/card.d.ts +4 -0
  68. package/dist/web/primitives/card.js +41 -0
  69. package/dist/web/primitives/contexts.d.ts +22 -0
  70. package/dist/web/primitives/contexts.js +50 -0
  71. package/dist/web/primitives/data-table-cell.d.ts +76 -0
  72. package/dist/web/primitives/data-table-cell.js +403 -0
  73. package/dist/web/primitives/data-table.d.ts +4 -0
  74. package/dist/web/primitives/data-table.js +279 -0
  75. package/dist/web/primitives/default-form.d.ts +4 -0
  76. package/dist/web/primitives/default-form.js +79 -0
  77. package/dist/web/primitives/facet-filter.d.ts +14 -0
  78. package/dist/web/primitives/facet-filter.js +39 -0
  79. package/dist/web/primitives/field.d.ts +63 -0
  80. package/dist/web/primitives/field.js +181 -0
  81. package/dist/web/primitives/form.d.ts +53 -0
  82. package/dist/web/primitives/form.js +252 -0
  83. package/dist/web/primitives/index.d.ts +7 -30
  84. package/dist/web/primitives/index.js +27 -2006
  85. package/dist/web/primitives/input.d.ts +4 -0
  86. package/dist/web/primitives/input.js +166 -0
  87. package/dist/web/primitives/layout-elements.d.ts +14 -0
  88. package/dist/web/primitives/layout-elements.js +194 -0
  89. package/dist/web/primitives/modal.js +1 -1
  90. package/dist/web/primitives/pager.d.ts +22 -0
  91. package/dist/web/primitives/pager.js +89 -0
  92. package/dist/web/primitives/row-actions.d.ts +13 -0
  93. package/dist/web/primitives/row-actions.js +178 -0
  94. package/dist/web/primitives/secret-reveal.d.ts +4 -0
  95. package/dist/web/primitives/secret-reveal.js +67 -0
  96. package/package.json +5 -5
  97. package/src/changes.json +6 -0
@@ -0,0 +1,377 @@
1
+ import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
+ import { useMemo } from "react";
5
+ import { rendererWarn } from "../dev-console.js";
6
+ import { useQuery } from "../hooks/use-query.js";
7
+ // `defaultCurrency` is entityEdit-only — it enables the `{amount, currency}`
8
+ // write shape (fw#1923); config-edit's plain-number contract needs bare `0`.
9
+ // `moneyCurrencyOverrides` (fw#2933) replaces `defaultCurrency` per field name
10
+ // — used for a `money` field that declares `currency: { kind: "tenant" }`, so
11
+ // its empty initial value carries the tenant's own currency instead of the
12
+ // entity-wide default.
13
+ export function versionOf(record) {
14
+ const version = record["version"];
15
+ return typeof version === "number" ? version : 1;
16
+ }
17
+ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides) {
18
+ const out = {};
19
+ for (const [name, def] of Object.entries(fields)) {
20
+ const type = propertyOf(def, "type");
21
+ const defaultValue = propertyOf(def, "default");
22
+ if (defaultValue !== undefined) {
23
+ out[name] = defaultValue;
24
+ continue;
25
+ }
26
+ // An untouched optional number must not persist as a deliberate 0.
27
+ if (type === "number" && propertyOf(def, "required") !== true) {
28
+ out[name] = undefined;
29
+ continue;
30
+ }
31
+ if (type === "embedded" && propertyOf(def, "multiple") === true) {
32
+ out[name] = [];
33
+ continue;
34
+ }
35
+ if (type === "money") {
36
+ const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
37
+ if (currency !== undefined) {
38
+ out[name] = { amount: 0, currency };
39
+ continue;
40
+ }
41
+ }
42
+ out[name] =
43
+ type === "boolean"
44
+ ? false
45
+ : type === "number" || type === "money"
46
+ ? 0
47
+ : type === "multiSelect"
48
+ ? []
49
+ : "";
50
+ }
51
+ return out;
52
+ }
53
+ // A `money` field opts a currently-empty value into the tenant-settings
54
+ // bundle's per-tenant currency (fw#2933) via `currency: { kind: "tenant" }`
55
+ // on its FieldDefinition. Read structurally — renderer only depends on the
56
+ // client-safe FieldDefinition subset, not the concrete MoneyFieldDef/
57
+ // MoneyCurrencySource types, same idiom as the other field-shape narrowings
58
+ // in this file (PrefillFieldShape etc.).
59
+ export function tenantCurrencyMoneyFieldNames(fields) {
60
+ const names = [];
61
+ for (const [name, def] of Object.entries(fields)) {
62
+ if (propertyOf(def, "type") === "money" &&
63
+ propertyOf(propertyOf(def, "currency"), "kind") === "tenant") {
64
+ names.push(name);
65
+ }
66
+ }
67
+ return names;
68
+ }
69
+ // Resolves what a tenant-declared money field's currency should be, from the
70
+ // same `config:query:values` query ConfigEditBody already uses. Never falls
71
+ // back to "EUR" while the query is genuinely still in flight (fw#2933) — a
72
+ // screen with no tenant-declared money field, or a query that errored or
73
+ // returned no value for the key, resolves immediately instead of blocking.
74
+ function resolveTenantCurrency(fieldNames, query, fallback) {
75
+ if (fieldNames.length === 0)
76
+ return { status: "not-needed" };
77
+ if (query.error) {
78
+ return fallback === undefined
79
+ ? { status: "unresolved" }
80
+ : { status: "ready", currency: fallback };
81
+ }
82
+ if (query.data === null)
83
+ return { status: "loading" };
84
+ const raw = query.data[TENANT_CURRENCY_CONFIG_KEY]?.value;
85
+ if (typeof raw === "string" && raw !== "")
86
+ return { status: "ready", currency: raw };
87
+ return fallback === undefined
88
+ ? { status: "unresolved" }
89
+ : { status: "ready", currency: fallback };
90
+ }
91
+ // A money field naming a fixed ISO code (`currency: { kind: "literal", code }`,
92
+ // fw#2839) resolves with no query at all. Read structurally, same idiom as
93
+ // tenantCurrencyMoneyFieldNames above.
94
+ export function literalCurrencyOverrides(fields) {
95
+ const out = {};
96
+ for (const [name, def] of Object.entries(fields)) {
97
+ const currency = propertyOf(def, "currency");
98
+ const code = propertyOf(currency, "code");
99
+ if (propertyOf(def, "type") === "money" &&
100
+ propertyOf(currency, "kind") === "literal" &&
101
+ typeof code === "string") {
102
+ out[name] = code;
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ // Per-field currency for the money fields that declare their own source: a
108
+ // literal code resolves synchronously (fw#2839), a `{ kind: "tenant" }` field
109
+ // needs the tenant-settings config query and makes the caller hold the form
110
+ // until it lands (fw#2933) — seeding the fallback meanwhile would render, and
111
+ // on a fast click submit, the wrong currency. Callers pass the tenant field
112
+ // names themselves because an edit form only needs the fetch for fields the
113
+ // record has no stored value for.
114
+ export function useMoneyCurrencyOverrides(fields, tenantFieldNames,
115
+ // `undefined` = no safe last resort (entity-less forms): an unresolvable
116
+ // tenant currency is reported via `unresolved` instead of guessing a code.
117
+ fallback) {
118
+ const query = useQuery("config:query:values", { keys: [TENANT_CURRENCY_CONFIG_KEY] }, { enabled: tenantFieldNames.length > 0 });
119
+ const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
120
+ const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
121
+ const overrides = useMemo(() => {
122
+ const out = { ...literalCurrencyOverrides(fields) };
123
+ if (resolvedTenantCurrency !== undefined) {
124
+ for (const name of tenantFieldNames)
125
+ out[name] = resolvedTenantCurrency;
126
+ }
127
+ return Object.keys(out).length > 0 ? out : undefined;
128
+ }, [fields, tenantFieldNames, resolvedTenantCurrency]);
129
+ return {
130
+ overrides,
131
+ loading: resolution.status === "loading",
132
+ unresolved: resolution.status === "unresolved",
133
+ };
134
+ }
135
+ function multiSelectOptionValues(shape) {
136
+ if (shape.options === undefined || shape.options.length === 0)
137
+ return undefined;
138
+ return new Set(shape.options.map((o) => (typeof o === "string" ? o : o.value)));
139
+ }
140
+ // The currency now comes from a user-controlled URL param and reaches
141
+ // `Intl.NumberFormat`, which throws a RangeError on a malformed code — reject
142
+ // it here instead of crashing the form render.
143
+ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
144
+ export function isMoneyValue(value) {
145
+ if (typeof value !== "object" || value === null)
146
+ return false;
147
+ const amount = propertyOf(value, "amount");
148
+ const currency = propertyOf(value, "currency");
149
+ return (typeof amount === "number" &&
150
+ Number.isFinite(amount) &&
151
+ typeof currency === "string" &&
152
+ CURRENCY_CODE_PATTERN.test(currency));
153
+ }
154
+ export const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
155
+ function parseJsonOrRaw(raw) {
156
+ try {
157
+ return JSON.parse(raw);
158
+ }
159
+ catch {
160
+ return raw;
161
+ }
162
+ }
163
+ function warnPrefillParam(message) {
164
+ rendererWarn(`[kumiko] ${message}`);
165
+ }
166
+ // An actionForm has no entity and thus no defaultCurrency, so a bare-number
167
+ // prefill would stay a number the handler's zod schema rejects (fw#2763).
168
+ function coerceMoneyValue(raw, fieldName, defaultCurrency) {
169
+ if (raw === null || raw === undefined)
170
+ return undefined;
171
+ const value = typeof raw === "string" ? parseJsonOrRaw(raw) : raw;
172
+ if (value === null)
173
+ return undefined;
174
+ if (isMoneyValue(value))
175
+ return { amount: value.amount, currency: value.currency.toUpperCase() };
176
+ if (typeof value === "object") {
177
+ warnPrefillParam(`money field "${fieldName}" got an object without a usable {amount, currency} shape — the prefill is ignored.`);
178
+ return undefined;
179
+ }
180
+ if (typeof value === "string" && value.trim() === "")
181
+ return undefined;
182
+ const amount = Number(typeof value === "number" ? value : raw);
183
+ if (!Number.isFinite(amount))
184
+ return undefined;
185
+ if (defaultCurrency !== undefined)
186
+ return { amount, currency: defaultCurrency };
187
+ warnPrefillParam(`money field "${fieldName}" was prefilled with a bare number and no currency is available — pass {"amount":<number>,"currency":"<ISO code>"} as the param value, otherwise the handler's schema rejects the submit.`);
188
+ return amount;
189
+ }
190
+ // Mirrors isRepresentableAtScale in the framework's schema-builder, which the
191
+ // renderer cannot import (server engine). Keep the tolerance identical so a
192
+ // prefilled cell never passes here and fails the handler's schema.
193
+ function isRepresentableAtScale(value, scale) {
194
+ const scaled = value * 10 ** scale;
195
+ const tolerance = Math.abs(scaled) * 8 * Number.EPSILON + Number.EPSILON;
196
+ return Math.abs(scaled - Math.round(scaled)) <= tolerance;
197
+ }
198
+ // Server schema (`z.iso.datetime()`) accepts only a `Z` suffix, while
199
+ // Temporal.Instant.from also takes numeric offsets.
200
+ function isUtcIsoInstant(value) {
201
+ if (!value.endsWith("Z"))
202
+ return false;
203
+ try {
204
+ Temporal.Instant.from(value);
205
+ return true;
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ const EMBEDDED_LIST_PREFILL_ROW_LIMIT = 500;
212
+ // The server schema (`z.string().date()`) rejects calendar-invalid days like 2020-02-31.
213
+ function isValidIsoDate(value) {
214
+ if (!ISO_DATE_PATTERN.test(value))
215
+ return false;
216
+ try {
217
+ Temporal.PlainDate.from(value, { overflow: "reject" });
218
+ return true;
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
224
+ function isDecimalWithinSafeRange(value, scale) {
225
+ return scale === undefined || Math.abs(value) <= Number.MAX_SAFE_INTEGER / 10 ** scale;
226
+ }
227
+ function coerceEmbeddedCell(value, cell) {
228
+ switch (cell.type) {
229
+ case "number":
230
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
231
+ case "decimal":
232
+ return typeof value === "number" &&
233
+ Number.isFinite(value) &&
234
+ (cell.scale === undefined ||
235
+ (isRepresentableAtScale(value, cell.scale) &&
236
+ isDecimalWithinSafeRange(value, cell.scale)))
237
+ ? value
238
+ : undefined;
239
+ case "date":
240
+ return typeof value === "string" && isValidIsoDate(value) ? value : undefined;
241
+ case "timestamp":
242
+ return typeof value === "string" && isUtcIsoInstant(value) ? value : undefined;
243
+ case "money":
244
+ // Row cells are signed minor units; the currency lives on the head, so a
245
+ // top-level `{amount, currency}` in major units would land 100x off.
246
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
247
+ case "boolean":
248
+ return typeof value === "boolean" ? value : undefined;
249
+ case "select":
250
+ return typeof value === "string" && (cell.options ?? []).includes(value) ? value : undefined;
251
+ default:
252
+ return typeof value === "string" ? value : undefined;
253
+ }
254
+ }
255
+ // A partially coerced list is the hazard the prefill exists to avoid: the
256
+ // handler replaces the list whole, so half a list overwrites the rest.
257
+ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
258
+ const parsed = parseJsonOrRaw(raw);
259
+ if (!Array.isArray(parsed)) {
260
+ warnPrefillParam(`embedded-list field "${fieldName}" was prefilled with something that is not a JSON array — the prefill is ignored.`);
261
+ return undefined;
262
+ }
263
+ if (parsed.length > (maxItems ?? EMBEDDED_LIST_PREFILL_ROW_LIMIT)) {
264
+ warnPrefillParam(`embedded-list field "${fieldName}" was prefilled with ${parsed.length} rows, more than it accepts — the prefill is ignored.`);
265
+ return undefined;
266
+ }
267
+ const rows = [];
268
+ for (const candidate of parsed) {
269
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
270
+ warnPrefillParam(`embedded-list field "${fieldName}" was prefilled with a row that is not an object — the prefill is ignored.`);
271
+ return undefined;
272
+ }
273
+ const cellEntries = [];
274
+ for (const [cellName, cell] of Object.entries(schema)) {
275
+ const cellValue = propertyOf(candidate, cellName);
276
+ if (cellValue === undefined || cellValue === null)
277
+ continue;
278
+ const coerced = coerceEmbeddedCell(cellValue, cell);
279
+ if (coerced === undefined) {
280
+ warnPrefillParam(`embedded-list field "${fieldName}" was prefilled with a cell "${cellName}" the sub-schema rejects — the prefill is ignored.`);
281
+ return undefined;
282
+ }
283
+ cellEntries.push([cellName, coerced]);
284
+ }
285
+ rows.push(Object.fromEntries(cellEntries));
286
+ }
287
+ return rows;
288
+ }
289
+ function coercePrefillString(raw, name, shape, fallback, defaultCurrency, moneyCurrencyOverrides) {
290
+ if (shape.type === "number") {
291
+ if (raw.trim() === "")
292
+ return fallback;
293
+ const parsed = Number(raw);
294
+ return Number.isFinite(parsed) ? parsed : fallback;
295
+ }
296
+ if (shape.type === "money") {
297
+ const coerced = coerceMoneyValue(raw, name, moneyCurrencyOverrides?.[name] ?? defaultCurrency);
298
+ return coerced === undefined ? fallback : coerced;
299
+ }
300
+ if (shape.type === "boolean")
301
+ return raw === "true";
302
+ if (shape.type === "multiSelect") {
303
+ // Row-action navigate stringifies arrays via String(arr) → "a,b" (or
304
+ // JSON when the navigate helper JSON.stringifies). Try JSON first
305
+ // (even without a "[" prefix), then comma-split; filter against
306
+ // field.options when present so unknown values never reach the form.
307
+ const optionValues = multiSelectOptionValues(shape);
308
+ const splitComma = () => raw === ""
309
+ ? []
310
+ : raw
311
+ .split(",")
312
+ .map((s) => s.trim())
313
+ .filter(Boolean);
314
+ let values;
315
+ try {
316
+ const parsed = JSON.parse(raw);
317
+ if (Array.isArray(parsed)) {
318
+ values = parsed.map((v) => String(v));
319
+ }
320
+ else if (typeof parsed === "string") {
321
+ values = [parsed];
322
+ }
323
+ else {
324
+ values = splitComma();
325
+ }
326
+ }
327
+ catch {
328
+ values = splitComma();
329
+ }
330
+ return optionValues !== undefined ? values.filter((v) => optionValues.has(v)) : values;
331
+ }
332
+ if (shape.type === "embedded" && shape.multiple === true) {
333
+ return coerceEmbeddedListRows(raw, name, shape.schema ?? {}, shape.maxItems) ?? fallback;
334
+ }
335
+ return raw;
336
+ }
337
+ export function mergeSearchParamsIntoInitial(fields, sources) {
338
+ const { searchParams, renderableFields, defaultCurrency, moneyCurrencyOverrides, drawerOverrides, handoffValues, } = sources;
339
+ const urlPrefillFields = new Set(sources.urlPrefillFields ?? []);
340
+ const defaults = buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides);
341
+ const merged = { ...defaults };
342
+ for (const [name, fieldDef] of Object.entries(fields)) {
343
+ if (renderableFields !== undefined && !renderableFields.has(name))
344
+ continue;
345
+ const shape = fieldDef;
346
+ // Neither gate is lifted by an allowlist entry or a handoff.
347
+ if (shape.sensitive === true)
348
+ continue;
349
+ if (shape.format === "password")
350
+ continue;
351
+ // An `undefined` override (row-extractor key whose row lacks the field)
352
+ // must not wipe the field default.
353
+ if (drawerOverrides !== undefined &&
354
+ Object.hasOwn(drawerOverrides, name) &&
355
+ drawerOverrides[name] !== undefined) {
356
+ merged[name] = drawerOverrides[name];
357
+ continue;
358
+ }
359
+ const handedOff = handoffValues !== undefined && Object.hasOwn(handoffValues, name)
360
+ ? handoffValues[name]
361
+ : undefined;
362
+ if (handedOff !== undefined) {
363
+ merged[name] =
364
+ typeof handedOff === "string"
365
+ ? coercePrefillString(handedOff, name, shape, defaults[name], defaultCurrency, moneyCurrencyOverrides)
366
+ : handedOff;
367
+ continue;
368
+ }
369
+ if (!urlPrefillFields.has(name))
370
+ continue;
371
+ const raw = searchParams[name];
372
+ if (raw === undefined)
373
+ continue;
374
+ merged[name] = coercePrefillString(raw, name, shape, defaults[name], defaultCurrency, moneyCurrencyOverrides);
375
+ }
376
+ return merged;
377
+ }
@@ -0,0 +1,11 @@
1
+ import type { EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ import type { Translate } from "../headless/index.ts";
4
+ import type { FeatureSchema } from "./feature-schema.ts";
5
+ export declare function EntityEditScreen({ schema, screen, translate, entityId, onCopyLink, }: {
6
+ readonly schema: FeatureSchema;
7
+ readonly screen: EntityEditScreenDefinition;
8
+ readonly translate?: Translate;
9
+ readonly entityId?: string;
10
+ readonly onCopyLink?: () => Promise<void> | void;
11
+ }): ReactNode;