@entifix/react-controls 0.19.0 → 0.20.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/README.md +8 -0
- package/dist/i18n/catalog/en.d.ts.map +1 -1
- package/dist/i18n/catalog/en.js +55 -9
- package/dist/i18n/catalog/en.js.map +1 -1
- package/dist/i18n/catalog/es.d.ts +64 -5
- package/dist/i18n/catalog/es.d.ts.map +1 -1
- package/dist/i18n/catalog/es.js +61 -9
- package/dist/i18n/catalog/es.js.map +1 -1
- package/dist/i18n/catalog/index.d.ts +102 -10
- package/dist/i18n/catalog/index.d.ts.map +1 -1
- package/dist/i18n/entity-labels.d.ts +14 -0
- package/dist/i18n/entity-labels.d.ts.map +1 -1
- package/dist/i18n/entity-labels.js +15 -0
- package/dist/i18n/entity-labels.js.map +1 -1
- package/dist/i18n/i18n-context.js +2 -2
- package/dist/i18n/i18n-context.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +0 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +0 -1
- package/dist/primitives.js.map +1 -1
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/theme-context.d.ts +15 -3
- package/dist/theme/theme-context.d.ts.map +1 -1
- package/dist/theme/theme-context.js +49 -23
- package/dist/theme/theme-context.js.map +1 -1
- package/dist/theme/theme-cookie.d.ts +9 -0
- package/dist/theme/theme-cookie.d.ts.map +1 -0
- package/dist/theme/theme-cookie.js +8 -0
- package/dist/theme/theme-cookie.js.map +1 -0
- package/dist/theme/theme-script.d.ts +8 -6
- package/dist/theme/theme-script.d.ts.map +1 -1
- package/dist/theme/theme-script.js +21 -12
- package/dist/theme/theme-script.js.map +1 -1
- package/dist/ui/actions/action-surfaces.d.ts +1 -1
- package/dist/ui/actions/action-surfaces.js +1 -1
- package/dist/ui/actions/action-surfaces.js.map +1 -1
- package/dist/ui/actions/error-actions.d.ts +32 -0
- package/dist/ui/actions/error-actions.d.ts.map +1 -0
- package/dist/ui/actions/error-actions.js +40 -0
- package/dist/ui/actions/error-actions.js.map +1 -0
- package/dist/ui/actions/index.d.ts +3 -0
- package/dist/ui/actions/index.d.ts.map +1 -1
- package/dist/ui/actions/index.js +3 -0
- package/dist/ui/actions/index.js.map +1 -1
- package/dist/ui/actions/shortcut.d.ts +13 -0
- package/dist/ui/actions/shortcut.d.ts.map +1 -0
- package/dist/ui/actions/shortcut.js +18 -0
- package/dist/ui/actions/shortcut.js.map +1 -0
- package/dist/ui/actions/use-verb-invoker.d.ts +55 -0
- package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
- package/dist/ui/actions/use-verb-invoker.js +98 -0
- package/dist/ui/actions/use-verb-invoker.js.map +1 -0
- package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
- package/dist/ui/atoms/cell-value/cell-value.js +28 -14
- package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
- package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
- package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
- package/dist/ui/atoms/field-control/field-control.js +2 -0
- package/dist/ui/atoms/field-control/field-control.js.map +1 -1
- package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
- package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
- package/dist/ui/atoms/number-input/quantity-input.js +2 -1
- package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
- package/dist/ui/atoms/table/table.d.ts +12 -3
- package/dist/ui/atoms/table/table.d.ts.map +1 -1
- package/dist/ui/atoms/table/table.js +59 -10
- package/dist/ui/atoms/table/table.js.map +1 -1
- package/dist/ui/atoms/text/text.d.ts +2 -2
- package/dist/ui/atoms/text/text.d.ts.map +1 -1
- package/dist/ui/atoms/text/text.js.map +1 -1
- package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
- package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/action-bar.js +218 -0
- package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
- package/dist/ui/molecules/action-bar/index.d.ts +2 -0
- package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/index.js +3 -0
- package/dist/ui/molecules/action-bar/index.js.map +1 -0
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
- package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
- package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
- package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
- package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
- package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
- package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/index.js +3 -0
- package/dist/ui/molecules/filter-chips/index.js.map +1 -0
- package/dist/ui/molecules/menu/menu.d.ts +11 -1
- package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
- package/dist/ui/molecules/menu/menu.js +62 -2
- package/dist/ui/molecules/menu/menu.js.map +1 -1
- package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
- package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
- package/dist/ui/molecules/pagination/pagination.js +35 -18
- package/dist/ui/molecules/pagination/pagination.js.map +1 -1
- package/dist/ui/molecules/region-state/index.d.ts +2 -0
- package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/index.js +3 -0
- package/dist/ui/molecules/region-state/index.js.map +1 -0
- package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
- package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/region-state.js +83 -0
- package/dist/ui/molecules/region-state/region-state.js.map +1 -0
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
- package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
- package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.js +129 -42
- package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.js +19 -12
- package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.js +322 -214
- package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts +16 -0
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.js +332 -197
- package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
- package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/header-sort.js +54 -0
- package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.js +106 -0
- package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
- package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
- package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
- package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
- package/dist/ui/organisms/top-bar/top-bar.js +14 -5
- package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.js +7 -1
- package/dist/ui/organisms/wizard/wizard.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
- package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
- package/dist/ui/utils/live-updates.d.ts +24 -0
- package/dist/ui/utils/live-updates.d.ts.map +1 -0
- package/dist/ui/utils/live-updates.js +20 -0
- package/dist/ui/utils/live-updates.js.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.js +42 -0
- package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
- package/dist/ui/utils/use-hotkey.d.ts +44 -25
- package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
- package/dist/ui/utils/use-hotkey.js +80 -48
- package/dist/ui/utils/use-hotkey.js.map +1 -1
- package/package.json +5 -5
- package/dist/ui/molecules/split-button/index.d.ts +0 -2
- package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/index.js +0 -3
- package/dist/ui/molecules/split-button/index.js.map +0 -1
- package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
- package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/split-button.js +0 -35
- package/dist/ui/molecules/split-button/split-button.js.map +0 -1
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
3
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { cloneEntityDraft, describeEntityColumns, readDraftString, readLocalizedDraft, readRowDrafts } from '@entifix/core';
|
|
4
|
-
import { useId, useState } from 'react';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { cloneEntityDraft, describeEntityColumns, extractMetaEntity, readDraftString, readLocalizedDraft, readRowDrafts } from '@entifix/core';
|
|
5
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
6
|
+
import { useEntityNoun, useLocalizedDescriptors, useT, useTranslateKey } from '../../../i18n/index.js';
|
|
7
|
+
import { renderAnchor, useCasesForSurface } from '../../actions/index.js';
|
|
8
|
+
import { useVerbInvoker } from '../../actions/use-verb-invoker.js';
|
|
7
9
|
import { Button, button } from '../../atoms/button/index.js';
|
|
8
10
|
import { CellValue } from '../../atoms/cell-value/index.js';
|
|
9
11
|
import { FieldControl } from '../../atoms/field-control/index.js';
|
|
10
12
|
import { LocalizedFieldControl } from '../../atoms/localized-field-control/index.js';
|
|
11
13
|
import { Skeleton } from '../../atoms/skeleton/index.js';
|
|
12
14
|
import { Text } from '../../atoms/text/index.js';
|
|
15
|
+
import { ActionBar, itemsForUseCases } from '../../molecules/action-bar/index.js';
|
|
13
16
|
import { Card } from '../../molecules/card/index.js';
|
|
14
|
-
import { ConfirmDialog } from '../../molecules/confirm-dialog/index.js';
|
|
15
17
|
import { EntityLinkInput } from '../../molecules/entity-link-input/index.js';
|
|
16
18
|
import { LoadingBoundary } from '../../molecules/loading-boundary/index.js';
|
|
17
|
-
import {
|
|
19
|
+
import { RegionState } from '../../molecules/region-state/index.js';
|
|
18
20
|
import { Stack } from '../../molecules/stack/index.js';
|
|
19
21
|
import { cn } from '../../utils/cn.js';
|
|
22
|
+
import { useLiveUpdates } from '../../utils/live-updates.js';
|
|
23
|
+
import { useFocusOnArrival } from '../../utils/use-focus-on-arrival.js';
|
|
20
24
|
import { EntityDetailGrid } from '../entity-detail-grid/index.js';
|
|
21
25
|
import { readEntityFormFields } from './entity-form-slots.js';
|
|
22
26
|
import { resolveEntityFormFields } from './use-entity-form-fields.js';
|
|
@@ -59,14 +63,10 @@ import { resolveEntityFormFields } from './use-entity-form-fields.js';
|
|
|
59
63
|
*
|
|
60
64
|
* It is presentational: the draft, its errors and the save/delete actions are
|
|
61
65
|
* all props, so the same form hosts on a plain route and inside a workspace tab.
|
|
62
|
-
*/ /**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* without the row wrapping — the same number Fiori's object-page header settles
|
|
67
|
-
* on. It is a layout constant, not a policy: raising it does not grant anything.
|
|
68
|
-
*/ const VISIBLE_ACTION_LIMIT = 4;
|
|
69
|
-
export function EntityForm({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, embedded = false, isLoading = false, skeleton = true, isSaving = false, isPending = false, isDeleting = false, error, backHref, renderLink = renderAnchor, title, metadata, isMetadataLoading = false, onUseCase, onClone, children }) {
|
|
66
|
+
*/ /** The key the built-in Delete runs under, which no declared verb can take. */ const DELETE_KEY = '$delete';
|
|
67
|
+
/** Save, from anywhere in the form — the caret in a field included. */ const SAVE_SHORTCUT = 'Mod+S';
|
|
68
|
+
/** The footer's declared primary, when it is not Save. */ const PRIMARY_SHORTCUT = 'Mod+Enter';
|
|
69
|
+
export function EntityForm({ entityConstructor, entity, values, onFieldChange, linkSources, onLinkChange, mode: modeProp, defaultMode, onModeChange, errors, formError, onSubmit, onDelete, deletePolicy = 'confirm', recordLabel: hostRecordLabel, embedded = false, isLoading = false, skeleton = true, isSaving = false, isPending = false, isDeleting = false, error, onRetry, backHref, renderLink = renderAnchor, title, metadata, isMetadataLoading = false, onUseCase, onClone, children }) {
|
|
70
70
|
const formId = useId();
|
|
71
71
|
const slots = readEntityFormFields(children);
|
|
72
72
|
// The *field* descriptors, which stay local and synchronous — a column is a
|
|
@@ -105,7 +105,12 @@ export function EntityForm({ entityConstructor, entity, values, onFieldChange, l
|
|
|
105
105
|
};
|
|
106
106
|
const t = useT();
|
|
107
107
|
const translateKey = useTranslateKey();
|
|
108
|
-
const
|
|
108
|
+
const entityNoun = useEntityNoun();
|
|
109
|
+
const live = useLiveUpdates();
|
|
110
|
+
// A form reached by a write — saved, handed off by a wizard — takes focus on
|
|
111
|
+
// its heading, so it never rests on `<body>` (ADR 0028).
|
|
112
|
+
const heading = useRef(null);
|
|
113
|
+
useFocusOnArrival(heading);
|
|
109
114
|
const busy = isSaving || isPending || isDeleting;
|
|
110
115
|
// A draft restored from storage holds relation ids and no instances, so every
|
|
111
116
|
// picker is resolving its target for a moment after mount. Saving inside that
|
|
@@ -141,214 +146,285 @@ export function EntityForm({ entityConstructor, entity, values, onFieldChange, l
|
|
|
141
146
|
const declaredFooterUseCases = useCasesForSurface('form-footer', metadata == null ? void 0 : metadata.useCases);
|
|
142
147
|
const headerUseCases = onUseCase ? declaredHeaderUseCases : [];
|
|
143
148
|
const footerUseCases = onUseCase ? declaredFooterUseCases : [];
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
// Every verb on the form, and its own Delete, goes through one invoker: a
|
|
150
|
+
// destructive verb asks, asks for the name, or runs and offers Undo, as it
|
|
151
|
+
// declared — `revoke-sessions` ends every session a user holds.
|
|
152
|
+
const verbs = useVerbInvoker({
|
|
153
|
+
run: (key)=>key === DELETE_KEY ? onDelete == null ? void 0 : onDelete() : onUseCase == null ? void 0 : onUseCase(key),
|
|
154
|
+
busy,
|
|
155
|
+
served: metadata == null ? void 0 : metadata.useCases
|
|
156
|
+
});
|
|
157
|
+
// What a confirmation names: the record as the reader knows it — its first
|
|
158
|
+
// filled-in field — or, for a record with nothing filled in, the entity.
|
|
159
|
+
const metaEntity = extractMetaEntity(entityConstructor);
|
|
160
|
+
const named = fields.find((field)=>field.type !== 'id' && readDraftString(draft, field.name) !== '');
|
|
161
|
+
const recordLabel = hostRecordLabel != null ? hostRecordLabel : named === undefined ? entityNoun(metaEntity) : readDraftString(draft, named.name);
|
|
162
|
+
const invoke = (descriptor)=>verbs.invoke({
|
|
163
|
+
descriptor,
|
|
164
|
+
target: undefined,
|
|
165
|
+
object: recordLabel,
|
|
166
|
+
count: 1
|
|
167
|
+
});
|
|
168
|
+
const requestDelete = ()=>{
|
|
169
|
+
// Undo is the host's to offer: only it can put a deleted record back.
|
|
170
|
+
if (deletePolicy === 'undo') {
|
|
171
|
+
onDelete == null ? void 0 : onDelete();
|
|
157
172
|
return;
|
|
158
173
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}, descriptor.key);
|
|
171
|
-
};
|
|
172
|
-
const useCaseMenuItem = (descriptor)=>{
|
|
173
|
-
var _descriptor_confirm;
|
|
174
|
-
return /*#__PURE__*/ _jsx(Menu.Item, {
|
|
175
|
-
tone: ((_descriptor_confirm = descriptor.confirm) == null ? void 0 : _descriptor_confirm.tone) === 'destructive' ? 'destructive' : 'neutral',
|
|
176
|
-
disabled: busy,
|
|
177
|
-
onClick: ()=>invoke(descriptor),
|
|
178
|
-
children: translateKey(descriptor.labelKey)
|
|
179
|
-
}, descriptor.key);
|
|
174
|
+
invoke({
|
|
175
|
+
key: DELETE_KEY,
|
|
176
|
+
binding: 'entity',
|
|
177
|
+
placement: 'determining',
|
|
178
|
+
labelKey: 'controls:form.delete',
|
|
179
|
+
confirm: {
|
|
180
|
+
tone: 'destructive',
|
|
181
|
+
messageKey: 'controls:confirm.deleteMessage'
|
|
182
|
+
},
|
|
183
|
+
reversible: deletePolicy
|
|
184
|
+
});
|
|
180
185
|
};
|
|
186
|
+
const headerItems = itemsForUseCases(headerUseCases, translateKey, invoke, busy);
|
|
187
|
+
const footerItems = itemsForUseCases(footerUseCases, translateKey, invoke, busy);
|
|
181
188
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* last) would reorder the row every time a verb was added.
|
|
189
|
-
*
|
|
190
|
-
* The split is only applied when there is something to fold: a fifth action
|
|
191
|
-
* is the point at which a menu is cheaper than a wider row, and a menu
|
|
192
|
-
* holding one entry is strictly worse than the button it replaced.
|
|
193
|
-
*/ const withOverflow = (descriptors)=>{
|
|
194
|
-
if (descriptors.length <= VISIBLE_ACTION_LIMIT) {
|
|
195
|
-
return descriptors.map(useCaseButton);
|
|
196
|
-
}
|
|
197
|
-
const visible = descriptors.slice(0, VISIBLE_ACTION_LIMIT);
|
|
198
|
-
const folded = descriptors.slice(VISIBLE_ACTION_LIMIT);
|
|
199
|
-
return [
|
|
200
|
-
...visible.map(useCaseButton),
|
|
201
|
-
/*#__PURE__*/ _jsxs(Menu, {
|
|
202
|
-
children: [
|
|
203
|
-
/*#__PURE__*/ _jsx(Menu.Trigger, {
|
|
204
|
-
"aria-label": t('form.moreActions'),
|
|
205
|
-
children: OVERFLOW_GLYPH
|
|
206
|
-
}),
|
|
207
|
-
/*#__PURE__*/ _jsx(Menu.Items, {
|
|
208
|
-
children: folded.map(useCaseMenuItem)
|
|
209
|
-
})
|
|
210
|
-
]
|
|
211
|
-
}, "overflow")
|
|
212
|
-
];
|
|
189
|
+
* A save attempt. Counted, so a refused one moves focus to the error summary
|
|
190
|
+
* once — not again on every keystroke that leaves an error standing.
|
|
191
|
+
*/ const [attempt, setAttempt] = useState(0);
|
|
192
|
+
const save = ()=>{
|
|
193
|
+
setAttempt((current)=>current + 1);
|
|
194
|
+
onSubmit == null ? void 0 : onSubmit(draft);
|
|
213
195
|
};
|
|
214
|
-
|
|
196
|
+
const summary = useRef(null);
|
|
197
|
+
const summaryId = useId();
|
|
198
|
+
const invalid = editing ? fields.filter((field)=>(errors == null ? void 0 : errors[field.name]) !== undefined) : [];
|
|
199
|
+
const focusedAttempt = useRef(0);
|
|
200
|
+
useEffect(()=>{
|
|
201
|
+
var _summary_current;
|
|
202
|
+
if (attempt === focusedAttempt.current || invalid.length === 0) return;
|
|
203
|
+
focusedAttempt.current = attempt;
|
|
204
|
+
(_summary_current = summary.current) == null ? void 0 : _summary_current.focus();
|
|
205
|
+
}, [
|
|
206
|
+
attempt,
|
|
207
|
+
invalid.length
|
|
208
|
+
]);
|
|
209
|
+
// Save is the footer's primary unless the entity declared one of its own — a
|
|
210
|
+
// determining "Publish" is what that page is for, and Save steps aside.
|
|
211
|
+
const declaresFooterPrimary = footerItems.some((item)=>item.emphasis === 'primary');
|
|
212
|
+
const footerActions = [
|
|
213
|
+
...may('write') ? [
|
|
214
|
+
{
|
|
215
|
+
key: 'save',
|
|
216
|
+
// A pending write waits on the push stream; while that is down
|
|
217
|
+
// it says so, rather than waiting without a word.
|
|
218
|
+
label: isPending ? t(live.connection === 'open' ? 'form.pending' : 'live.waiting') : isSaving ? t('form.saving') : t('form.save'),
|
|
219
|
+
emphasis: declaresFooterPrimary ? 'default' : 'primary',
|
|
220
|
+
disabled: busy || resolvingLinks,
|
|
221
|
+
submit: true,
|
|
222
|
+
shortcut: SAVE_SHORTCUT,
|
|
223
|
+
onSelect: save
|
|
224
|
+
}
|
|
225
|
+
] : [],
|
|
226
|
+
// The page's main act answers Mod+Enter, unless it declared its own.
|
|
227
|
+
...footerItems.map((item)=>item.emphasis === 'primary' && item.shortcut === undefined ? _extends({}, item, {
|
|
228
|
+
shortcut: PRIMARY_SHORTCUT
|
|
229
|
+
}) : item),
|
|
230
|
+
// Clone is the CRUD triple's, not a declared verb's: every entity can be
|
|
231
|
+
// copied, so giving it a `@useCase()` would mean declaring the same verb on
|
|
232
|
+
// all 28 of them. It needs `write`, because what it produces is a create.
|
|
233
|
+
...onClone && entity && may('write') ? [
|
|
234
|
+
{
|
|
235
|
+
key: 'clone',
|
|
236
|
+
label: t('form.clone'),
|
|
237
|
+
disabled: busy,
|
|
238
|
+
onSelect: ()=>onClone(cloneEntityDraft(entityConstructor, draft))
|
|
239
|
+
}
|
|
240
|
+
] : [],
|
|
241
|
+
...onDelete && may('delete') ? [
|
|
242
|
+
{
|
|
243
|
+
key: 'delete',
|
|
244
|
+
label: isDeleting ? t('form.deleting') : t('form.delete'),
|
|
245
|
+
destructive: true,
|
|
246
|
+
disabled: busy,
|
|
247
|
+
onSelect: requestDelete
|
|
248
|
+
}
|
|
249
|
+
] : []
|
|
250
|
+
];
|
|
251
|
+
// A real form while editing, so Enter in a field saves; an embedded one
|
|
252
|
+
// belongs to its host's flow, and a caller who may not write has no Save.
|
|
253
|
+
const asForm = editing && !embedded && may('write');
|
|
254
|
+
const rows = fields.map((field)=>/*#__PURE__*/ _jsx(FieldRow, {
|
|
255
|
+
field: field,
|
|
256
|
+
entity: entity,
|
|
257
|
+
draft: draft,
|
|
258
|
+
value: readDraftString(draft, field.name),
|
|
259
|
+
editing: editing,
|
|
260
|
+
error: editing ? errors == null ? void 0 : errors[field.name] : undefined,
|
|
261
|
+
setField: setField,
|
|
262
|
+
linkSource: linkSources == null ? void 0 : linkSources[field.name],
|
|
263
|
+
onLinkChange: onLinkChange,
|
|
264
|
+
id: `${formId}-${field.name}`
|
|
265
|
+
}, field.name));
|
|
266
|
+
const body = /*#__PURE__*/ _jsxs(Stack, {
|
|
267
|
+
gap: "s",
|
|
215
268
|
children: [
|
|
216
|
-
/*#__PURE__*/ _jsxs(Stack, {
|
|
217
|
-
|
|
269
|
+
!embedded && /*#__PURE__*/ _jsxs(Stack, {
|
|
270
|
+
direction: "row",
|
|
271
|
+
gap: "xs",
|
|
272
|
+
align: "center",
|
|
218
273
|
children: [
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
weight: "semibold",
|
|
228
|
-
children: title != null ? title : entity || isLoading ? t('form.details') : t('form.new')
|
|
229
|
-
}),
|
|
230
|
-
modeProp === undefined && entity && /*#__PURE__*/ _jsx(Button, {
|
|
231
|
-
type: "button",
|
|
232
|
-
variant: "ghost",
|
|
233
|
-
size: "sm",
|
|
234
|
-
onClick: toggleMode,
|
|
235
|
-
children: editing ? t('form.view') : t('form.edit')
|
|
236
|
-
}),
|
|
237
|
-
/*#__PURE__*/ _jsx(LoadingBoundary, {
|
|
238
|
-
isLoading: isMetadataLoading,
|
|
239
|
-
lines: 0,
|
|
240
|
-
children: /*#__PURE__*/ _jsxs(_Fragment, {
|
|
241
|
-
children: [
|
|
242
|
-
withOverflow(headerUseCases),
|
|
243
|
-
slots.headerActions
|
|
244
|
-
]
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
]
|
|
274
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
275
|
+
as: "h2",
|
|
276
|
+
ref: heading,
|
|
277
|
+
tabIndex: -1,
|
|
278
|
+
step: 1,
|
|
279
|
+
weight: "semibold",
|
|
280
|
+
className: "focus-ring",
|
|
281
|
+
children: title != null ? title : entity || isLoading ? t('form.details') : t('form.new')
|
|
248
282
|
}),
|
|
249
|
-
/*#__PURE__*/ _jsx(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
283
|
+
modeProp === undefined && entity && /*#__PURE__*/ _jsx(Button, {
|
|
284
|
+
type: "button",
|
|
285
|
+
variant: "ghost",
|
|
286
|
+
size: "sm",
|
|
287
|
+
onClick: toggleMode,
|
|
288
|
+
children: editing ? t('form.view') : t('form.edit')
|
|
254
289
|
}),
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
children: errorMessage(error)
|
|
260
|
-
}),
|
|
261
|
-
showSkeleton ? customSkeleton != null ? customSkeleton : fields.map((field)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
262
|
-
gap: "3xs",
|
|
290
|
+
/*#__PURE__*/ _jsx(LoadingBoundary, {
|
|
291
|
+
isLoading: isMetadataLoading,
|
|
292
|
+
lines: 0,
|
|
293
|
+
children: /*#__PURE__*/ _jsxs(_Fragment, {
|
|
263
294
|
children: [
|
|
264
|
-
/*#__PURE__*/ _jsx(
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
headerItems.length > 0 && /*#__PURE__*/ _jsx(ActionBar, {
|
|
296
|
+
surface: "form-header",
|
|
297
|
+
items: headerItems
|
|
267
298
|
}),
|
|
268
|
-
|
|
269
|
-
className: "h-9 w-full"
|
|
270
|
-
})
|
|
299
|
+
slots.headerActions
|
|
271
300
|
]
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
]
|
|
304
|
+
}),
|
|
305
|
+
/*#__PURE__*/ _jsx("span", {
|
|
306
|
+
role: "status",
|
|
307
|
+
"aria-live": "polite",
|
|
308
|
+
className: "sr-only",
|
|
309
|
+
children: isLoading ? t('form.loading') : ''
|
|
310
|
+
}),
|
|
311
|
+
/*#__PURE__*/ _jsx(RegionState, {
|
|
312
|
+
plural: entityNoun(metaEntity, 'plural'),
|
|
313
|
+
error: error,
|
|
314
|
+
onRetry: onRetry,
|
|
315
|
+
// Only a load that can be retried leaves an out-of-date record; a
|
|
316
|
+
// failed save leaves what the reader typed, which is current.
|
|
317
|
+
hasStaleData: entity !== undefined && onRetry !== undefined
|
|
318
|
+
}),
|
|
319
|
+
invalid.length > 0 && /*#__PURE__*/ _jsxs("div", {
|
|
320
|
+
ref: summary,
|
|
321
|
+
tabIndex: -1,
|
|
322
|
+
role: "alert",
|
|
323
|
+
"aria-labelledby": summaryId,
|
|
324
|
+
"data-testid": "entity-form-error-summary",
|
|
325
|
+
className: "rounded-lg border-l-4 border-danger bg-danger-subtle px-s py-xs text-step-sm text-content focus-ring",
|
|
326
|
+
children: [
|
|
327
|
+
/*#__PURE__*/ _jsx("p", {
|
|
328
|
+
id: summaryId,
|
|
329
|
+
className: "m-0 font-semibold",
|
|
330
|
+
children: t('form.errorSummary', {
|
|
331
|
+
count: invalid.length
|
|
332
|
+
})
|
|
297
333
|
}),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
335
|
+
className: "m-0 ps-s",
|
|
336
|
+
children: invalid.map((field)=>/*#__PURE__*/ _jsx("li", {
|
|
337
|
+
children: /*#__PURE__*/ _jsx("a", {
|
|
338
|
+
href: `#${formId}-${field.name}`,
|
|
339
|
+
// The content colour: primary on the danger surface misses
|
|
340
|
+
// 4.5:1.
|
|
341
|
+
className: "font-medium text-content underline underline-offset-2 focus-ring",
|
|
342
|
+
onClick: (event)=>{
|
|
343
|
+
var _document_getElementById;
|
|
344
|
+
event.preventDefault();
|
|
345
|
+
(_document_getElementById = document.getElementById(`${formId}-${field.name}`)) == null ? void 0 : _document_getElementById.focus();
|
|
346
|
+
},
|
|
347
|
+
children: errors == null ? void 0 : errors[field.name]
|
|
348
|
+
})
|
|
349
|
+
}, field.name))
|
|
350
|
+
})
|
|
351
|
+
]
|
|
352
|
+
}),
|
|
353
|
+
editing && fields.some((field)=>field.required && !field.readonly) && /*#__PURE__*/ _jsx(Text, {
|
|
354
|
+
step: -1,
|
|
355
|
+
tone: "muted",
|
|
356
|
+
children: t('form.requiredHint')
|
|
357
|
+
}),
|
|
358
|
+
showSkeleton ? customSkeleton != null ? customSkeleton : fields.map((field)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
359
|
+
gap: "3xs",
|
|
360
|
+
children: [
|
|
361
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
362
|
+
shape: "line",
|
|
363
|
+
className: "w-24"
|
|
364
|
+
}),
|
|
365
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
366
|
+
className: "h-9 w-full"
|
|
367
|
+
})
|
|
368
|
+
]
|
|
369
|
+
}, field.name)) : editing ? rows : // when there is no control, and a term and its value is what
|
|
370
|
+
// the pair is.
|
|
371
|
+
rows.length > 0 && /*#__PURE__*/ _jsx("dl", {
|
|
372
|
+
className: "m-0 flex flex-col gap-s",
|
|
373
|
+
children: rows
|
|
374
|
+
}),
|
|
375
|
+
ownedCollections.map((field)=>/*#__PURE__*/ _jsx(EntityDetailGrid, {
|
|
376
|
+
descriptor: field,
|
|
377
|
+
rows: readRowDrafts(draft[field.name]),
|
|
378
|
+
onRowsChange: (rows)=>setField(field.name, rows),
|
|
379
|
+
errors: editing ? errors : undefined,
|
|
380
|
+
editing: editing,
|
|
381
|
+
isLoading: isLoading,
|
|
382
|
+
skeleton: skeleton
|
|
383
|
+
}, field.name)),
|
|
384
|
+
editing && formError && /*#__PURE__*/ _jsx("span", {
|
|
385
|
+
role: "alert",
|
|
386
|
+
className: "text-step-sm text-danger",
|
|
387
|
+
children: formError
|
|
388
|
+
}),
|
|
389
|
+
editing && !embedded && /*#__PURE__*/ _jsxs(Stack, {
|
|
390
|
+
direction: "row",
|
|
391
|
+
gap: "xs",
|
|
392
|
+
align: "center",
|
|
393
|
+
children: [
|
|
394
|
+
/*#__PURE__*/ _jsx(ActionBar, {
|
|
395
|
+
surface: "form-footer",
|
|
396
|
+
size: "md",
|
|
397
|
+
items: footerActions
|
|
335
398
|
}),
|
|
336
|
-
slots.
|
|
399
|
+
slots.footerActions,
|
|
400
|
+
backHref && renderLink({
|
|
401
|
+
href: backHref,
|
|
402
|
+
className: cn(button({
|
|
403
|
+
variant: 'ghost'
|
|
404
|
+
}), busy && 'pointer-events-none opacity-50'),
|
|
405
|
+
children: t('form.back'),
|
|
406
|
+
intent: {
|
|
407
|
+
kind: 'back'
|
|
408
|
+
}
|
|
409
|
+
})
|
|
337
410
|
]
|
|
338
411
|
}),
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
412
|
+
slots.rest
|
|
413
|
+
]
|
|
414
|
+
});
|
|
415
|
+
return /*#__PURE__*/ _jsxs(Card, {
|
|
416
|
+
children: [
|
|
417
|
+
asForm ? /*#__PURE__*/ _jsx("form", {
|
|
418
|
+
noValidate: true,
|
|
419
|
+
// Enter submits through Save, the form's default button, which is
|
|
420
|
+
// disabled while a save is in flight — so a busy form stays put.
|
|
421
|
+
onSubmit: (event)=>{
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
save();
|
|
424
|
+
},
|
|
425
|
+
children: body
|
|
426
|
+
}) : body,
|
|
427
|
+
verbs.dialog
|
|
352
428
|
]
|
|
353
429
|
});
|
|
354
430
|
}
|
|
@@ -421,6 +497,8 @@ linkSources) {
|
|
|
421
497
|
* The source branch sits above the `virtual || isRelation` fallback and below
|
|
422
498
|
* `field.render`, which is what lets a slot still override a picked field.
|
|
423
499
|
*/ function FieldRow({ field, entity, draft, value, editing, error, setField, linkSource, onLinkChange, id }) {
|
|
500
|
+
const t = useT();
|
|
501
|
+
const errorId = `${id}-error`;
|
|
424
502
|
const readNode = field.readRender ? field.readRender(entity) : /*#__PURE__*/ _jsx(CellValue, {
|
|
425
503
|
value: entity ? entity[field.name] : undefined,
|
|
426
504
|
descriptor: field,
|
|
@@ -475,21 +553,51 @@ linkSources) {
|
|
|
475
553
|
value: value,
|
|
476
554
|
onChange: (next)=>setField(field.name, next),
|
|
477
555
|
id: id,
|
|
478
|
-
record: draft
|
|
556
|
+
record: draft,
|
|
557
|
+
"aria-invalid": error !== undefined || undefined,
|
|
558
|
+
"aria-describedby": error === undefined ? undefined : errorId,
|
|
559
|
+
"aria-required": field.required || undefined
|
|
479
560
|
});
|
|
480
561
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
gap: "3xs",
|
|
562
|
+
if (!editing) return /*#__PURE__*/ _jsxs("div", {
|
|
563
|
+
className: "flex flex-col gap-3xs",
|
|
484
564
|
children: [
|
|
485
|
-
|
|
486
|
-
htmlFor: id,
|
|
565
|
+
/*#__PURE__*/ _jsx("dt", {
|
|
487
566
|
className: "text-step-sm text-content-muted",
|
|
488
567
|
children: field.label
|
|
489
568
|
}),
|
|
569
|
+
/*#__PURE__*/ _jsx("dd", {
|
|
570
|
+
className: "m-0",
|
|
571
|
+
children: control
|
|
572
|
+
})
|
|
573
|
+
]
|
|
574
|
+
});
|
|
575
|
+
const ownsLabel = field.type === 'boolean' && !field.render && !field.virtual;
|
|
576
|
+
// Marked before anyone submits, not discovered after.
|
|
577
|
+
const marked = field.required && !field.readonly;
|
|
578
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
579
|
+
gap: "3xs",
|
|
580
|
+
children: [
|
|
581
|
+
!ownsLabel && // The marker sits beside the label, not in it: the name stays the
|
|
582
|
+
// field's own, and `aria-required` is what says it to a reader.
|
|
583
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
584
|
+
className: "flex items-baseline gap-3xs",
|
|
585
|
+
children: [
|
|
586
|
+
/*#__PURE__*/ _jsx("label", {
|
|
587
|
+
htmlFor: id,
|
|
588
|
+
className: "text-step-sm text-content-muted",
|
|
589
|
+
children: field.label
|
|
590
|
+
}),
|
|
591
|
+
marked && /*#__PURE__*/ _jsx("span", {
|
|
592
|
+
"aria-hidden": "true",
|
|
593
|
+
className: "text-step-sm text-danger",
|
|
594
|
+
children: t('form.requiredMarker')
|
|
595
|
+
})
|
|
596
|
+
]
|
|
597
|
+
}),
|
|
490
598
|
control,
|
|
491
599
|
error && /*#__PURE__*/ _jsx("span", {
|
|
492
|
-
|
|
600
|
+
id: errorId,
|
|
493
601
|
className: "text-step-sm text-danger",
|
|
494
602
|
children: error
|
|
495
603
|
})
|