@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +12 -4
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/app-features-context.js +4 -4
- package/dist/app/column-renderers.d.ts +7 -9
- package/dist/app/column-renderers.js +15 -18
- package/dist/app/config-edit-body.js +7 -3
- package/dist/app/config-edit-shim.d.ts +7 -8
- package/dist/app/config-edit-shim.js +18 -20
- package/dist/app/custom-screens.d.ts +3 -3
- package/dist/app/custom-screens.js +8 -8
- package/dist/app/dashboard-body.d.ts +2 -2
- package/dist/app/dashboard-body.js +2 -2
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +18 -13
- package/dist/app/entity-edit-screen.js +4 -4
- package/dist/app/entity-list-screen.js +3 -3
- package/dist/app/extension-form-submit.d.ts +1 -1
- package/dist/app/extension-form-submit.js +14 -15
- package/dist/app/extension-sections.d.ts +29 -28
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.js +2 -2
- package/dist/app/kumiko-screen.js +8 -10
- package/dist/app/layout-fields.js +1 -1
- package/dist/app/list-facets.js +3 -3
- package/dist/app/nav.d.ts +10 -10
- package/dist/app/nav.js +13 -13
- package/dist/app/projection-detail-body.js +4 -4
- package/dist/app/projection-detail-shim.d.ts +6 -5
- package/dist/app/projection-detail-shim.js +23 -22
- package/dist/app/projection-list-body.js +10 -10
- package/dist/app/projection-list-shim.d.ts +5 -5
- package/dist/app/projection-list-shim.js +17 -18
- package/dist/app/qn.js +10 -12
- package/dist/app/qualify-screen-id.js +1 -1
- package/dist/app/screen-access.js +2 -2
- package/dist/app/screen-shared.js +1 -1
- package/dist/app/secret-mint-body.js +4 -4
- package/dist/app/write-failed-error.js +8 -7
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +5 -5
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- package/dist/components/reference-create-dialog.js +3 -3
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +15 -2
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit-logic.js +1 -1
- package/dist/components/render-edit-types.d.ts +6 -6
- package/dist/components/render-edit.js +74 -73
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +62 -61
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +93 -84
- package/dist/components/use-wizard-form-draft.js +10 -10
- package/dist/context/dispatcher-context.js +5 -5
- package/dist/context/user-roles-context.js +2 -2
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +14 -6
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +4 -2
- package/dist/headless/view-model/list.js +184 -150
- package/dist/headless/view-model/types.d.ts +6 -3
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/reference-limits.js +11 -12
- package/dist/hooks/use-list-url-state.d.ts +13 -13
- package/dist/hooks/use-list-url-state.js +21 -22
- package/dist/hooks/use-query.js +3 -3
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +32 -19
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +6 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +244 -221
- package/dist/primitives.js +1 -1
- package/dist/sse/live-events.d.ts +5 -5
- package/dist/sse/live-events.js +17 -17
- package/dist/tokens.d.ts +8 -8
- package/dist/tokens.js +16 -17
- package/dist/web/app/browser-locale.d.ts +9 -9
- package/dist/web/app/browser-locale.js +19 -19
- package/dist/web/app/client-plugin.d.ts +44 -52
- package/dist/web/app/client-plugin.js +4 -5
- package/dist/web/app/create-app.d.ts +28 -31
- package/dist/web/app/create-app.js +29 -25
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/document-lang-sync.js +1 -1
- package/dist/web/app/draft-storage.js +1 -1
- package/dist/web/app/nav-providers-context.d.ts +3 -3
- package/dist/web/app/nav-providers-context.js +3 -3
- package/dist/web/app/nav.d.ts +18 -17
- package/dist/web/app/nav.js +66 -64
- package/dist/web/app/rich-content-editor.js +1 -1
- package/dist/web/components/config-cascade.js +17 -18
- package/dist/web/components/config-source-badge.js +4 -4
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/layout/app-layout.d.ts +9 -9
- package/dist/web/layout/app-layout.js +2 -2
- package/dist/web/layout/avatar.d.ts +4 -3
- package/dist/web/layout/avatar.js +15 -17
- package/dist/web/layout/default-app-shell.d.ts +22 -23
- package/dist/web/layout/editor-panel.js +3 -3
- package/dist/web/layout/nav-tree.js +86 -84
- package/dist/web/layout/profile-menu.d.ts +1 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/sidebar-brand.d.ts +7 -7
- package/dist/web/layout/sidebar-user.d.ts +1 -1
- package/dist/web/layout/sidebar-user.js +4 -4
- package/dist/web/layout/sidebar.d.ts +8 -8
- package/dist/web/layout/sidebar.js +7 -7
- package/dist/web/layout/target-resolver-stub.d.ts +5 -5
- package/dist/web/layout/target-resolver-stub.js +20 -20
- package/dist/web/layout/target-url.d.ts +7 -7
- package/dist/web/layout/target-url.js +22 -22
- package/dist/web/layout/theme-toggle.js +4 -5
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/layout/workspace-shell.d.ts +21 -21
- package/dist/web/layout/workspace-shell.js +29 -30
- package/dist/web/lib/cn.d.ts +4 -4
- package/dist/web/lib/cn.js +4 -4
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.d.ts +8 -8
- package/dist/web/primitives/action-menu.js +14 -13
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +10 -10
- package/dist/web/primitives/card-styles.js +1 -1
- package/dist/web/primitives/card.js +2 -2
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/combobox.d.ts +12 -12
- package/dist/web/primitives/combobox.js +34 -35
- package/dist/web/primitives/data-table-cell.js +2 -2
- package/dist/web/primitives/data-table.js +6 -6
- package/dist/web/primitives/date-field.js +2 -2
- package/dist/web/primitives/date-input.js +4 -4
- package/dist/web/primitives/default-form.js +2 -2
- package/dist/web/primitives/dropdown-menu.js +14 -15
- package/dist/web/primitives/embedded-list-input.js +3 -3
- package/dist/web/primitives/facet-filter.js +2 -2
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +8 -3
- package/dist/web/primitives/layout-elements.js +7 -5
- package/dist/web/primitives/layout.js +2 -2
- package/dist/web/primitives/located-timestamp-input.js +4 -4
- package/dist/web/primitives/money-input.js +13 -17
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +6 -7
- package/dist/web/primitives/secret-reveal.js +1 -1
- package/dist/web/primitives/timestamp-input.d.ts +9 -9
- package/dist/web/primitives/timestamp-input.js +21 -21
- package/dist/web/primitives/toast.js +12 -13
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/sse/live-events.d.ts +6 -6
- package/dist/web/sse/live-events.js +3 -3
- package/dist/web/tokens.d.ts +11 -12
- package/dist/web/tokens.js +35 -37
- package/dist/web/version/update-checker.js +14 -13
- package/dist/web/widgets/ai-text-field.js +1 -1
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
- package/dist/web/widgets/bounded-load-cache.js +10 -4
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +7 -7
- package/dist/web/widgets/detail-list.d.ts +5 -3
- package/dist/web/widgets/detail-list.js +5 -3
- package/dist/web/widgets/feed-list.d.ts +2 -2
- package/dist/web/widgets/feed-list.js +2 -2
- package/dist/web/widgets/form-fields.d.ts +14 -14
- package/dist/web/widgets/form-fields.js +14 -13
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +5 -4
- package/dist/web/widgets/infinity-list.js +3 -3
- package/dist/web/widgets/mode-switch.d.ts +2 -2
- package/dist/web/widgets/mode-switch.js +2 -2
- package/dist/web/widgets/pdf-preview.js +1 -1
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/progress-list.d.ts +1 -2
- package/dist/web/widgets/progress-list.js +1 -2
- package/dist/web/widgets/query-table.d.ts +6 -6
- package/dist/web/widgets/result-panel.d.ts +16 -16
- package/dist/web/widgets/result-panel.js +10 -10
- package/dist/web/widgets/section-card.d.ts +3 -3
- package/dist/web/widgets/section-card.js +3 -3
- package/dist/web/widgets/stat.d.ts +4 -4
- package/dist/web/widgets/stat.js +6 -6
- package/dist/web/widgets/states.d.ts +4 -4
- package/dist/web/widgets/states.js +4 -4
- package/dist/web/widgets/upload-zone.js +13 -5
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
- package/src/changes.json +49 -0
|
@@ -184,73 +184,98 @@ function unitDisplaySpec(spec) {
|
|
|
184
184
|
const v = spec["unitDisplay"];
|
|
185
185
|
return isOneOf(INTL_UNIT_DISPLAYS, v) ? v : "short";
|
|
186
186
|
}
|
|
187
|
+
function isEmptyCellValue(value) {
|
|
188
|
+
return value === null || value === undefined || value === "";
|
|
189
|
+
}
|
|
187
190
|
export function applyFormatSpec(spec, value, translate) {
|
|
188
|
-
const isEmpty = value === null || value === undefined || value === "";
|
|
189
191
|
// priority renders its emptyLabel for empty values — every other format
|
|
190
192
|
// collapses empty to "".
|
|
191
|
-
if (
|
|
193
|
+
if (isEmptyCellValue(value) && spec.format !== "priority")
|
|
192
194
|
return "";
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
195
|
+
if (!isBuiltInCellFormat(spec.format))
|
|
196
|
+
return formatUnknownCellFormat(spec, value);
|
|
197
|
+
return CELL_FORMATTERS[spec.format](spec, value, translate);
|
|
198
|
+
}
|
|
199
|
+
function formatTemporalCell(spec, value) {
|
|
200
|
+
const isTimestamp = spec.format === "timestamp";
|
|
201
|
+
return formatDateCell(value, spec.format, {
|
|
202
|
+
locale: stringSpec(spec, "locale"),
|
|
203
|
+
dateStyle: dateStyleSpec(spec, "dateStyle"),
|
|
204
|
+
timeStyle: isTimestamp ? dateStyleSpec(spec, "timeStyle") : undefined,
|
|
205
|
+
timeZone: isTimestamp ? stringSpec(spec, "timeZone") : undefined,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function formatBooleanCell(spec, value, translate) {
|
|
209
|
+
if (value === true) {
|
|
210
|
+
const trueLabel = stringSpec(spec, "trueLabel");
|
|
211
|
+
return trueLabel === undefined ? "✓" : (translate?.(trueLabel) ?? trueLabel);
|
|
212
|
+
}
|
|
213
|
+
if (value === false) {
|
|
214
|
+
const falseLabel = stringSpec(spec, "falseLabel");
|
|
215
|
+
return falseLabel === undefined ? "" : (translate?.(falseLabel) ?? falseLabel);
|
|
216
|
+
}
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
function formatCurrencyCell(spec, value) {
|
|
220
|
+
const sym = stringSpec(spec, "symbol") ?? "";
|
|
221
|
+
return sym.length > 0 ? `${value} ${sym}` : String(value);
|
|
222
|
+
}
|
|
223
|
+
function formatNumericCell(spec, value) {
|
|
224
|
+
return formatNumberCell(value, stringSpec(spec, "locale"), booleanSpec(spec, "grouping") ?? true, spec.format === "bigInt" ? {} : fractionDigitsSpec(spec));
|
|
225
|
+
}
|
|
226
|
+
function formatUnitSpecCell(spec, value) {
|
|
227
|
+
return formatUnitCell(value, stringSpec(spec, "unit"), stringSpec(spec, "locale"), unitDisplaySpec(spec));
|
|
228
|
+
}
|
|
229
|
+
function formatPriorityCell(spec, value) {
|
|
230
|
+
const emptyLabel = stringSpec(spec, "emptyLabel") ?? EMPTY_VALUE_PLACEHOLDER;
|
|
231
|
+
const prefix = stringSpec(spec, "prefix") ?? "";
|
|
232
|
+
if (isEmptyCellValue(value) || value === 0)
|
|
233
|
+
return emptyLabel;
|
|
234
|
+
return `${prefix}${value}`;
|
|
235
|
+
}
|
|
236
|
+
function formatEnumOptionCell(spec, value, translate) {
|
|
237
|
+
const keyPrefix = stringSpec(spec, "keyPrefix");
|
|
238
|
+
const labelFor = (item) => {
|
|
239
|
+
const raw = typeof item === "string" ? item : String(item);
|
|
240
|
+
if (keyPrefix === undefined || translate === undefined)
|
|
241
|
+
return raw;
|
|
242
|
+
const key = `${keyPrefix}${raw}`;
|
|
243
|
+
const translated = translate(key);
|
|
244
|
+
return translated === key ? raw : translated;
|
|
245
|
+
};
|
|
246
|
+
// multiSelect-style columns hold an array of option values
|
|
247
|
+
return Array.isArray(value) ? value.map(labelFor).join(", ") : labelFor(value);
|
|
248
|
+
}
|
|
249
|
+
// Thumbnails and file links are drawn by the web list cell; this is the
|
|
250
|
+
// text fallback for every other surface.
|
|
251
|
+
function formatFileCell(_spec, value) {
|
|
252
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
253
|
+
}
|
|
254
|
+
function formatJsonCell(spec, value) {
|
|
255
|
+
return stringifyJsonOrString(value, typeof spec["indent"] === "number" ? spec["indent"] : 2);
|
|
256
|
+
}
|
|
257
|
+
const CELL_FORMATTERS = {
|
|
258
|
+
timestamp: formatTemporalCell,
|
|
259
|
+
date: formatTemporalCell,
|
|
260
|
+
boolean: formatBooleanCell,
|
|
261
|
+
currency: formatCurrencyCell,
|
|
262
|
+
number: formatNumericCell,
|
|
263
|
+
decimal: formatNumericCell,
|
|
264
|
+
bigInt: formatNumericCell,
|
|
265
|
+
unit: formatUnitSpecCell,
|
|
266
|
+
priority: formatPriorityCell,
|
|
267
|
+
enumOption: formatEnumOptionCell,
|
|
268
|
+
image: formatFileCell,
|
|
269
|
+
file: formatFileCell,
|
|
270
|
+
json: formatJsonCell,
|
|
271
|
+
};
|
|
272
|
+
// Own-key check: a format such as "toString" must not resolve to an inherited member.
|
|
273
|
+
function isBuiltInCellFormat(format) {
|
|
274
|
+
return Object.hasOwn(CELL_FORMATTERS, format);
|
|
275
|
+
}
|
|
276
|
+
function formatUnknownCellFormat(spec, value) {
|
|
277
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
278
|
+
rendererWarn(`[kumiko] applyFormatSpec: unknown format key "${spec.format}" — registered via FieldFormatRegistry module augmentation?`);
|
|
255
279
|
}
|
|
280
|
+
return typeof value === "string" ? value : String(value);
|
|
256
281
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Currency decimal places — covers the world's zero- and three-decimal
|
|
2
2
|
// outliers, defaults to 2 for everything else. Shared between renderer/web's
|
|
3
3
|
// MoneyInput (minor-unit display factor) and RenderField's minor/major-unit
|
|
4
|
-
// conversion
|
|
4
|
+
// conversion — a single source keeps both sides
|
|
5
5
|
// scaling the same currency the same way.
|
|
6
6
|
export function currencyDecimals(code) {
|
|
7
7
|
if (code === "JPY" || code === "KRW" || code === "VND" || code === "ISK")
|
package/dist/headless/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Re-
|
|
2
|
-
//
|
|
1
|
+
// Re-exports from framework/ui-types so renderer code uses the same
|
|
2
|
+
// parser as the server boot validator (cross-feature references).
|
|
3
3
|
export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
4
|
export { generateRequestId, StreamFrame } from "./dispatcher/index.js";
|
|
5
5
|
export { createFormController, groupIssuesByPath, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./form/index.js";
|
|
@@ -52,7 +52,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
|
|
|
52
52
|
return declaredFieldSpecs.length > 0 && remainingFields.length === 0;
|
|
53
53
|
}
|
|
54
54
|
// relatedList runs its own query — nothing here to resolve against
|
|
55
|
-
// entity/values, the spec passes through verbatim
|
|
55
|
+
// entity/values, the spec passes through verbatim.
|
|
56
56
|
export function computeRelatedListSectionViewModel(sectionSpec, translate) {
|
|
57
57
|
return {
|
|
58
58
|
kind: "relatedList",
|
|
@@ -461,7 +461,7 @@ export function computeEditViewModel(input) {
|
|
|
461
461
|
const visible = hasVisibleContent(fields, sectionSpec.actions, values);
|
|
462
462
|
// Tabs mode renders this section as its own panel, where two columns reads
|
|
463
463
|
// better than the single-column default a stacked form keeps. Applies to
|
|
464
|
-
// entityEdit tabs as well
|
|
464
|
+
// entityEdit tabs as well, not just projectionDetail.
|
|
465
465
|
const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
|
|
466
466
|
const groups = ("groups" in sectionSpec ? sectionSpec.groups : undefined)
|
|
467
467
|
?.map((group) => ({ ...group, fields: group.fields.filter((f) => !isHiddenByExclusion(f)) }))
|
|
@@ -469,6 +469,7 @@ export function computeEditViewModel(input) {
|
|
|
469
469
|
.map((group) => ({
|
|
470
470
|
title: translate(group.title),
|
|
471
471
|
columns: group.columns ?? 2,
|
|
472
|
+
columnsDeclared: group.columns !== undefined,
|
|
472
473
|
fields: group.fields.map((fieldSpec) => {
|
|
473
474
|
const fieldName = normalizeEditField(fieldSpec).field;
|
|
474
475
|
const fieldView = fields.find((f) => f.field === fieldName);
|
|
@@ -491,6 +492,7 @@ export function computeEditViewModel(input) {
|
|
|
491
492
|
}),
|
|
492
493
|
...(sectionSpec.subtitle !== undefined && { subtitle: translate(sectionSpec.subtitle) }),
|
|
493
494
|
columns: sectionSpec.columns ?? defaultColumns,
|
|
495
|
+
columnsDeclared: sectionSpec.columns !== undefined,
|
|
494
496
|
fields,
|
|
495
497
|
...(groups !== undefined && { groups }),
|
|
496
498
|
...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
|
|
@@ -9,168 +9,202 @@ const WRITE_ONLY_SET_LABEL_KEY = "kumiko.field.writeOnly.set";
|
|
|
9
9
|
// follow-up; this v1 assumes the caller passed a row-filter that drops any
|
|
10
10
|
// field the user may not see.
|
|
11
11
|
export function computeListViewModel(input) {
|
|
12
|
-
const { screen,
|
|
13
|
-
const columns =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
refFeature: refTarget.featureName,
|
|
39
|
-
refLabelField: normalized.refLabelField ?? "id",
|
|
40
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
41
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
42
|
-
});
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const fieldDef = entity.fields[normalized.field];
|
|
46
|
-
if (!fieldDef) {
|
|
47
|
-
// Not a stored field — may be a read-time derived field (value appended
|
|
48
|
-
// by the list-query handler). Render it as its display valueType; derived
|
|
49
|
-
// columns carry no reference/select metadata and never server-sort.
|
|
50
|
-
const derivedDef = entity.derivedFields?.[normalized.field];
|
|
51
|
-
if (!derivedDef) {
|
|
52
|
-
// Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
|
|
53
|
-
// a real base-table column that's never a declared entity field, e.g.
|
|
54
|
-
// a SystemAdmin cross-tenant list exposing tenantId. Unlike the
|
|
55
|
-
// derivedFields branch above, these are actual DB columns, so
|
|
56
|
-
// server-side ORDER BY works — sortable stays true, except for the
|
|
57
|
-
// ID columns in LIST_ROW_META_REFERENCES (see their reference branch).
|
|
58
|
-
const rowMetaType = LIST_ROW_META_COLUMNS[normalized.field];
|
|
59
|
-
if (rowMetaType !== undefined) {
|
|
60
|
-
const rowMetaRef = LIST_ROW_META_REFERENCES[normalized.field];
|
|
61
|
-
columns.push({
|
|
62
|
-
field: normalized.field,
|
|
63
|
-
label: translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field)),
|
|
64
|
-
type: rowMetaRef !== undefined ? "reference" : rowMetaType,
|
|
65
|
-
// The DB column holds the GUID, not the resolved label — server-side
|
|
66
|
-
// ORDER BY would sort by GUID while the cell shows the name.
|
|
67
|
-
sortable: rowMetaRef === undefined,
|
|
68
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
69
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
70
|
-
...(rowMetaRef !== undefined && {
|
|
71
|
-
refEntity: rowMetaRef.refEntity,
|
|
72
|
-
refFeature: rowMetaRef.refFeature,
|
|
73
|
-
refLabelField: rowMetaRef.refLabelField,
|
|
74
|
-
}),
|
|
75
|
-
});
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
// A virtual presentational column — drawn entirely by a columnRenderer
|
|
79
|
-
// component from the row (e.g. tag chips); value is undefined and it
|
|
80
|
-
// never server-sorts. `field` is just the column key. It needs BOTH a
|
|
81
|
-
// label AND a renderer — renderer is what actually draws it; a label
|
|
82
|
-
// alone with no renderer would push an empty, unrendered column into
|
|
83
|
-
// the view model. Missing either is an author typo (or a stale
|
|
84
|
-
// field-rename) → fail loud instead of silently drawing nothing.
|
|
85
|
-
if (normalized.label !== undefined && normalized.renderer !== undefined) {
|
|
86
|
-
columns.push({
|
|
87
|
-
field: normalized.field,
|
|
88
|
-
label: translate(normalized.label),
|
|
89
|
-
type: "text",
|
|
90
|
-
sortable: false,
|
|
91
|
-
renderer: normalized.renderer,
|
|
92
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
throw new Error(`computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`);
|
|
97
|
-
}
|
|
98
|
-
columns.push({
|
|
99
|
-
field: normalized.field,
|
|
100
|
-
label: translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field)),
|
|
101
|
-
type: derivedDef.valueType,
|
|
102
|
-
// Display-only: a header sort would round-trip to the server, which has
|
|
103
|
-
// no column to sort by (see DerivedFieldDef). Never offer the affordance.
|
|
104
|
-
sortable: false,
|
|
105
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
106
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const label = translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field));
|
|
111
|
-
// Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
|
|
112
|
-
// kann same-feature ("user") oder cross-feature ("users:user")
|
|
113
|
-
// sein. parseRefTarget gibt (featureName, entityName), der
|
|
114
|
-
// Renderer baut die Lookup-QN als
|
|
115
|
-
// `<refFeature>:query:<refEntity>:list`.
|
|
116
|
-
const refRaw = fieldDef.type === "reference" ? fieldDef.entity : undefined;
|
|
117
|
-
const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
|
|
118
|
-
const refEntity = refTarget?.entityName;
|
|
119
|
-
const refFeature = refTarget?.featureName;
|
|
120
|
-
const refLabelField = fieldDef.type === "reference" ? (fieldDef.labelField ?? "id") : undefined;
|
|
121
|
-
// For select and multiSelect fields: bake in translated option labels.
|
|
122
|
-
// Convention matches the form path → one translation source for list +
|
|
123
|
-
// form. A missing key returns the key itself per convention; the
|
|
124
|
-
// renderer then falls back to humanizeSlug.
|
|
125
|
-
const optionLabels = fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
126
|
-
? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, screen.entity, normalized.field, value), fieldDef.options)
|
|
127
|
-
: undefined;
|
|
128
|
-
const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
|
|
129
|
-
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
130
|
-
const fileDisplay = deriveListFileDisplay(fieldDef, normalized.renderer);
|
|
131
|
-
const column = {
|
|
132
|
-
field: normalized.field,
|
|
133
|
-
label,
|
|
134
|
-
type: fieldDef.type,
|
|
135
|
-
sortable: fieldIsSortable(fieldDef),
|
|
136
|
-
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
137
|
-
...(optionLabels !== undefined && { optionLabels }),
|
|
138
|
-
...(optionTones !== undefined && { optionTones }),
|
|
139
|
-
...(refEntity !== undefined && { refEntity }),
|
|
140
|
-
...(refFeature !== undefined && { refFeature }),
|
|
141
|
-
...(refLabelField !== undefined && { refLabelField }),
|
|
142
|
-
...(grouping !== undefined && { grouping }),
|
|
143
|
-
...(fileDisplay !== undefined && { fileDisplay }),
|
|
144
|
-
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
145
|
-
};
|
|
146
|
-
columns.push(column);
|
|
12
|
+
const { screen, rows } = input;
|
|
13
|
+
const columns = screen.columns.map((spec) => resolveListColumn(input, normalizeListColumn(spec)));
|
|
14
|
+
const listRows = rows.map(writeOnlyRowFormatter(input));
|
|
15
|
+
return {
|
|
16
|
+
screenId: screen.id,
|
|
17
|
+
entityName: screen.entity,
|
|
18
|
+
columns,
|
|
19
|
+
rows: listRows,
|
|
20
|
+
...(screen.slots && { slots: screen.slots }),
|
|
21
|
+
isEmpty: listRows.length === 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// First match wins; the order encodes which metadata source a column name
|
|
25
|
+
// resolves against when several could claim it.
|
|
26
|
+
const LIST_COLUMN_RESOLVERS = [
|
|
27
|
+
declaredReferenceColumn,
|
|
28
|
+
storedFieldColumn,
|
|
29
|
+
derivedFieldColumn,
|
|
30
|
+
rowMetaColumn,
|
|
31
|
+
virtualRendererColumn,
|
|
32
|
+
];
|
|
33
|
+
function resolveListColumn(input, column) {
|
|
34
|
+
for (const resolve of LIST_COLUMN_RESOLVERS) {
|
|
35
|
+
const resolved = resolve(input, column);
|
|
36
|
+
if (resolved)
|
|
37
|
+
return resolved;
|
|
147
38
|
}
|
|
39
|
+
// No metadata source claims the name: an author typo or a stale field
|
|
40
|
+
// rename, so fail loud instead of silently drawing nothing.
|
|
41
|
+
throw new Error(`computeListViewModel: screen "${input.screen.id}" references unknown field "${column.field}" on entity "${input.screen.entity}"`);
|
|
42
|
+
}
|
|
43
|
+
function translatedColumnLabel(input, column) {
|
|
44
|
+
return input.translate(column.label ?? fieldLabelKey(input.featureName, input.screen.entity, column.field));
|
|
45
|
+
}
|
|
46
|
+
// A real non-text entity field keeps its own type (documented on
|
|
47
|
+
// ListColumnSpec.refEntity); only the all-"text" pseudo-entities of the
|
|
48
|
+
// projection shims and fields without a stored def take the declared path.
|
|
49
|
+
function declaredReferenceColumn(input, column) {
|
|
50
|
+
const storedFieldType = input.entity.fields[column.field]?.type;
|
|
51
|
+
// skip: no declared reference, or a real non-text field that keeps its own type
|
|
52
|
+
if (column.refEntity === undefined ||
|
|
53
|
+
(storedFieldType !== undefined && storedFieldType !== "text")) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Declared reference metadata (ListColumnSpec.refEntity) — for
|
|
57
|
+
// projectionList/relatedList columns, which have no EntityDefinition
|
|
58
|
+
// field to carry a real "reference" type. Checked before the
|
|
59
|
+
// entity.fields lookup so it also fires against the pseudo-entities
|
|
60
|
+
// that hardcode every field as "text" (projection-list-shim,
|
|
61
|
+
// related-list-section) — those never reach the stored reference branch
|
|
62
|
+
// since fieldDef.type is always "text" there.
|
|
63
|
+
const refTarget = parseRefTarget(column.refEntity, input.featureName);
|
|
64
|
+
return {
|
|
65
|
+
field: column.field,
|
|
66
|
+
label: translatedColumnLabel(input, column),
|
|
67
|
+
type: "reference",
|
|
68
|
+
// The declared value is the referenced id, not the resolved label —
|
|
69
|
+
// there is no column to server-sort by, same as row-meta references.
|
|
70
|
+
sortable: false,
|
|
71
|
+
refEntity: refTarget.entityName,
|
|
72
|
+
refFeature: refTarget.featureName,
|
|
73
|
+
refLabelField: column.refLabelField ?? "id",
|
|
74
|
+
...(column.renderer !== undefined && { renderer: column.renderer }),
|
|
75
|
+
...(column.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function storedFieldColumn(input, column) {
|
|
79
|
+
const fieldDef = input.entity.fields[column.field];
|
|
80
|
+
// skip: not a stored field, a later resolver decides
|
|
81
|
+
if (!fieldDef)
|
|
82
|
+
return undefined;
|
|
83
|
+
return {
|
|
84
|
+
field: column.field,
|
|
85
|
+
label: translatedColumnLabel(input, column),
|
|
86
|
+
type: fieldDef.type,
|
|
87
|
+
sortable: fieldIsSortable(fieldDef),
|
|
88
|
+
...(column.renderer !== undefined && { renderer: column.renderer }),
|
|
89
|
+
...storedFieldMetadata(input, column, fieldDef),
|
|
90
|
+
...(column.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Key order is part of the serialized view model, so it stays fixed here.
|
|
94
|
+
function storedFieldMetadata(input, column, fieldDef) {
|
|
95
|
+
// For select and multiSelect fields: bake in translated option labels.
|
|
96
|
+
// Convention matches the form path → one translation source for list +
|
|
97
|
+
// form. A missing key returns the key itself per convention; the
|
|
98
|
+
// renderer then falls back to humanizeSlug.
|
|
99
|
+
const optionLabels = fieldDef.type === "select" || fieldDef.type === "multiSelect"
|
|
100
|
+
? buildOptionLabels(input.translate, (value) => fieldOptionLabelKey(input.featureName, input.screen.entity, column.field, value), fieldDef.options)
|
|
101
|
+
: undefined;
|
|
102
|
+
const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
|
|
103
|
+
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
104
|
+
const fileDisplay = deriveListFileDisplay(fieldDef, column.renderer);
|
|
105
|
+
return {
|
|
106
|
+
...(optionLabels !== undefined && { optionLabels }),
|
|
107
|
+
...(optionTones !== undefined && { optionTones }),
|
|
108
|
+
...storedReferenceMetadata(fieldDef, input.featureName),
|
|
109
|
+
...(grouping !== undefined && { grouping }),
|
|
110
|
+
...(fileDisplay !== undefined && { fileDisplay }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Reference fields may target the same feature ("user") or another feature
|
|
114
|
+
// ("users:user"). parseRefTarget returns the feature and entity names, which
|
|
115
|
+
// the renderer uses to build the lookup query as
|
|
116
|
+
// `<refFeature>:query:<refEntity>:list`.
|
|
117
|
+
function storedReferenceMetadata(fieldDef, featureName) {
|
|
118
|
+
if (fieldDef.type !== "reference")
|
|
119
|
+
return {};
|
|
120
|
+
const refTarget = parseRefTarget(fieldDef.entity, featureName);
|
|
121
|
+
return {
|
|
122
|
+
refEntity: refTarget.entityName,
|
|
123
|
+
refFeature: refTarget.featureName,
|
|
124
|
+
refLabelField: fieldDef.labelField ?? "id",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Not a stored field — may be a read-time derived field (value appended
|
|
128
|
+
// by the list-query handler). Render it as its display valueType; derived
|
|
129
|
+
// columns carry no reference/select metadata and never server-sort.
|
|
130
|
+
function derivedFieldColumn(input, column) {
|
|
131
|
+
const derivedDef = input.entity.derivedFields?.[column.field];
|
|
132
|
+
// skip: not a derived field, a later resolver decides
|
|
133
|
+
if (!derivedDef)
|
|
134
|
+
return undefined;
|
|
135
|
+
return {
|
|
136
|
+
field: column.field,
|
|
137
|
+
label: translatedColumnLabel(input, column),
|
|
138
|
+
type: derivedDef.valueType,
|
|
139
|
+
// Display-only: a header sort would round-trip to the server, which has
|
|
140
|
+
// no column to sort by (see DerivedFieldDef). Never offer the affordance.
|
|
141
|
+
sortable: false,
|
|
142
|
+
...(column.renderer !== undefined && { renderer: column.renderer }),
|
|
143
|
+
...(column.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
|
|
147
|
+
// a real base-table column that's never a declared entity field, e.g.
|
|
148
|
+
// a SystemAdmin cross-tenant list exposing tenantId. Unlike derived
|
|
149
|
+
// fields, these are actual DB columns, so server-side ORDER BY works —
|
|
150
|
+
// sortable stays true, except for the ID columns in LIST_ROW_META_REFERENCES.
|
|
151
|
+
function rowMetaColumn(input, column) {
|
|
152
|
+
const rowMetaType = LIST_ROW_META_COLUMNS[column.field];
|
|
153
|
+
// skip: not a base-table column, a later resolver decides
|
|
154
|
+
if (rowMetaType === undefined)
|
|
155
|
+
return undefined;
|
|
156
|
+
const rowMetaRef = LIST_ROW_META_REFERENCES[column.field];
|
|
157
|
+
return {
|
|
158
|
+
field: column.field,
|
|
159
|
+
label: translatedColumnLabel(input, column),
|
|
160
|
+
type: rowMetaRef !== undefined ? "reference" : rowMetaType,
|
|
161
|
+
// The DB column holds the GUID, not the resolved label — server-side
|
|
162
|
+
// ORDER BY would sort by GUID while the cell shows the name.
|
|
163
|
+
sortable: rowMetaRef === undefined,
|
|
164
|
+
...(column.renderer !== undefined && { renderer: column.renderer }),
|
|
165
|
+
...(column.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
166
|
+
...(rowMetaRef !== undefined && {
|
|
167
|
+
refEntity: rowMetaRef.refEntity,
|
|
168
|
+
refFeature: rowMetaRef.refFeature,
|
|
169
|
+
refLabelField: rowMetaRef.refLabelField,
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// A virtual presentational column — drawn entirely by a columnRenderer
|
|
174
|
+
// component from the row (e.g. tag chips); value is undefined and it
|
|
175
|
+
// never server-sorts. `field` is just the column key. It needs BOTH a
|
|
176
|
+
// label AND a renderer — renderer is what actually draws it; a label
|
|
177
|
+
// alone with no renderer would push an empty, unrendered column into
|
|
178
|
+
// the view model.
|
|
179
|
+
function virtualRendererColumn(input, column) {
|
|
180
|
+
// skip: without both a label and a renderer the column is unknown
|
|
181
|
+
if (column.label === undefined || column.renderer === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
return {
|
|
184
|
+
field: column.field,
|
|
185
|
+
label: input.translate(column.label),
|
|
186
|
+
type: "text",
|
|
187
|
+
sortable: false,
|
|
188
|
+
renderer: column.renderer,
|
|
189
|
+
...(column.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// The wire carries `true` (set) / `null` (empty) for writeOnly fields; the cell shows a word, not "true".
|
|
193
|
+
function writeOnlyRowFormatter(input) {
|
|
194
|
+
const { screen, entity, translate } = input;
|
|
148
195
|
const writeOnlyFieldNames = screen.columns
|
|
149
196
|
.map((spec) => normalizeListColumn(spec).field)
|
|
150
197
|
.filter((name) => {
|
|
151
198
|
const fieldDef = entity.fields[name];
|
|
152
199
|
return fieldDef?.type === "text" && fieldDef.writeOnly === true;
|
|
153
200
|
});
|
|
154
|
-
|
|
155
|
-
const showWriteOnlyAsWord = (row) => {
|
|
201
|
+
return (row) => {
|
|
156
202
|
const shown = { ...row };
|
|
157
203
|
for (const name of writeOnlyFieldNames) {
|
|
158
204
|
if (row[name] === true)
|
|
159
205
|
shown[name] = translate(WRITE_ONLY_SET_LABEL_KEY);
|
|
160
206
|
}
|
|
161
|
-
return shown;
|
|
162
|
-
};
|
|
163
|
-
const listRows = rows.map((row) => ({
|
|
164
|
-
id: String(row["id"] ?? ""),
|
|
165
|
-
values: showWriteOnlyAsWord(row),
|
|
166
|
-
}));
|
|
167
|
-
return {
|
|
168
|
-
screenId: screen.id,
|
|
169
|
-
entityName: screen.entity,
|
|
170
|
-
columns,
|
|
171
|
-
rows: listRows,
|
|
172
|
-
...(screen.slots && { slots: screen.slots }),
|
|
173
|
-
isEmpty: listRows.length === 0,
|
|
207
|
+
return { id: String(row["id"] ?? ""), values: shown };
|
|
174
208
|
};
|
|
175
209
|
}
|
|
176
210
|
// Field-i18n-key convention matches what features register translations
|
|
@@ -191,7 +191,7 @@ export type EditFieldViewModel = {
|
|
|
191
191
|
* entity appears as the display label in the dropdown. Default: "id". */
|
|
192
192
|
readonly refLabelField?: string;
|
|
193
193
|
/** Only for `type: "reference"` — query handler QN the picker sources its
|
|
194
|
-
* options from
|
|
194
|
+
* options from, replacing the referenced entity's list handler.
|
|
195
195
|
* Rows carry `{ id, label }`; `refLabelField` is then unused for the
|
|
196
196
|
* picker but still drives list cells. */
|
|
197
197
|
readonly refOptionsQuery?: string;
|
|
@@ -268,7 +268,7 @@ export type EditFieldViewModel = {
|
|
|
268
268
|
/** Only set for a plain (non-list, non-embedded) `type: "money"` field —
|
|
269
269
|
* same source as embeddedListCurrency (EntityDefinition.defaultCurrency,
|
|
270
270
|
* falls back to "EUR"). The renderer needs this to build the payload's
|
|
271
|
-
* `{amount, currency}` shape
|
|
271
|
+
* `{amount, currency}` shape. */
|
|
272
272
|
readonly currency?: string;
|
|
273
273
|
/** Only set for `type: "number"` when NumberFieldDef.unit is set — a static
|
|
274
274
|
* suffix string, or a sibling-field reference the renderer resolves against
|
|
@@ -292,7 +292,7 @@ export type EditFieldsSectionViewModel = {
|
|
|
292
292
|
* `excludeFields`. Unlike a declared-empty section (review step), the
|
|
293
293
|
* renderer must not give it a tab or wizard step. */
|
|
294
294
|
readonly emptiedByWriteExclusion?: boolean;
|
|
295
|
-
/** Optional —
|
|
295
|
+
/** Optional — a titleless section renders only its fields (flat form). */
|
|
296
296
|
readonly title?: string;
|
|
297
297
|
/** Translated help text under the block heading, from
|
|
298
298
|
* `EditFieldsSection.description`. Renders even without `title`. */
|
|
@@ -300,12 +300,15 @@ export type EditFieldsSectionViewModel = {
|
|
|
300
300
|
/** Translated `EditFieldsSection.subtitle` (wizard step rail / up-next box). */
|
|
301
301
|
readonly subtitle?: string;
|
|
302
302
|
readonly columns: number;
|
|
303
|
+
/** True when the author declared `columns`; false means the tabs/stacked default. */
|
|
304
|
+
readonly columnsDeclared?: boolean;
|
|
303
305
|
readonly fields: readonly EditFieldViewModel[];
|
|
304
306
|
/** From `EditFieldsSection.groups` — each entry's `fields` are the same
|
|
305
307
|
* view-model objects as `fields` above, re-grouped (not recomputed). */
|
|
306
308
|
readonly groups?: readonly {
|
|
307
309
|
readonly title: string;
|
|
308
310
|
readonly columns: number;
|
|
311
|
+
readonly columnsDeclared?: boolean;
|
|
309
312
|
readonly fields: readonly EditFieldViewModel[];
|
|
310
313
|
}[];
|
|
311
314
|
/** From `EditFieldsSection.icon` — closed IconKey vocabulary, renders
|
|
@@ -12,6 +12,10 @@ export function createLiveRefetchScheduler(run, windowMs = LIVE_REFETCH_COALESCE
|
|
|
12
12
|
try {
|
|
13
13
|
await run();
|
|
14
14
|
}
|
|
15
|
+
catch {
|
|
16
|
+
// The caller surfaces its own load errors in state; a failed run must
|
|
17
|
+
// neither become an unhandled rejection nor drop the queued follow-up.
|
|
18
|
+
}
|
|
15
19
|
finally {
|
|
16
20
|
inFlight = false;
|
|
17
21
|
}
|