@happyvertical/smrt-svelte 0.55.23 → 0.55.24
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/AGENTS.md +2 -2
- package/README.md +2 -2
- package/agents/overview-surfaces.md +108 -4
- package/agents/screens.md +171 -5
- package/dist/components/overview/index.d.ts +4 -1
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +3 -0
- package/dist/components/overview/record.d.ts +51 -0
- package/dist/components/overview/record.d.ts.map +1 -0
- package/dist/components/overview/record.js +146 -0
- package/dist/components/overview/server.d.ts +4 -0
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +4 -0
- package/dist/components/overview/types.d.ts +12 -0
- package/dist/components/overview/types.d.ts.map +1 -1
- package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/RecordTable.svelte +167 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +13 -2
- package/dist/components/overview/widgets/core.d.ts.map +1 -1
- package/dist/components/overview/widgets/core.js +43 -2
- package/dist/components/overview/widgets/data.d.ts +97 -8
- package/dist/components/overview/widgets/data.d.ts.map +1 -1
- package/dist/components/overview/widgets/line-totals.d.ts +48 -0
- package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
- package/dist/components/overview/widgets/line-totals.js +91 -0
- package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
- package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-loaders.js +150 -0
- package/dist/components/overview/widgets/record-table.d.ts +45 -0
- package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-table.js +201 -0
- package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
- package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-widgets.js +100 -0
- package/dist/components/screens/DetailScreen.svelte +105 -6
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
- package/dist/components/screens/EditForm.svelte +39 -3
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecipeScreens.svelte +18 -18
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecordHeader.svelte +238 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecordScreen.svelte +511 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/columns.d.ts +29 -0
- package/dist/components/screens/columns.d.ts.map +1 -0
- package/dist/components/screens/columns.js +115 -0
- package/dist/components/screens/fields.d.ts +12 -2
- package/dist/components/screens/fields.d.ts.map +1 -1
- package/dist/components/screens/fields.js +37 -5
- package/dist/components/screens/index.d.ts +7 -2
- package/dist/components/screens/index.d.ts.map +1 -1
- package/dist/components/screens/index.js +6 -1
- package/dist/components/screens/record-header.d.ts +51 -0
- package/dist/components/screens/record-header.d.ts.map +1 -0
- package/dist/components/screens/record-header.js +118 -0
- package/dist/components/screens/relations.d.ts +49 -0
- package/dist/components/screens/relations.d.ts.map +1 -0
- package/dist/components/screens/relations.js +98 -0
- package/dist/components/screens/roles.d.ts +56 -0
- package/dist/components/screens/roles.d.ts.map +1 -0
- package/dist/components/screens/roles.js +174 -0
- package/dist/components/screens/types.d.ts +182 -1
- package/dist/components/screens/types.d.ts.map +1 -1
- package/dist/components/screens/values.d.ts +23 -1
- package/dist/components/screens/values.d.ts.map +1 -1
- package/dist/components/screens/values.js +80 -4
- package/dist/i18n/strings.overview.d.ts +25 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +25 -0
- package/dist/i18n/strings.screens.d.ts +8 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -1
- package/dist/i18n/strings.screens.js +8 -0
- package/package.json +16 -16
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* `basic` markers anywhere => everything basic; any marker => unmarked fields
|
|
11
11
|
* advanced).
|
|
12
12
|
*/
|
|
13
|
+
import { type FieldRole } from './roles.js';
|
|
13
14
|
import type { ScreenCollectionDefinition, ScreenField, ScreenFieldDefinition, ScreenInputKind, ScreenMode, ScreenPolicy } from './types.js';
|
|
14
15
|
/** Columns shown by default on a list when the policy marks many basic. */
|
|
15
16
|
export declare const DEFAULT_MAX_LIST_COLUMNS = 6;
|
|
@@ -27,13 +28,22 @@ export interface DeriveScreenFieldsOptions {
|
|
|
27
28
|
}
|
|
28
29
|
/** `dueDate` -> `Due date`; reference fields drop a trailing `Id`. */
|
|
29
30
|
export declare function humanizeFieldName(name: string): string;
|
|
31
|
+
/** Default label of a field: reference fields drop a trailing `Id`. */
|
|
32
|
+
export declare function humanizeLabel(name: string, definition: ScreenFieldDefinition): string;
|
|
30
33
|
/** Singular and plural display names derived from the class name. */
|
|
31
34
|
export declare function screenTitles(definition: ScreenCollectionDefinition): {
|
|
32
35
|
singular: string;
|
|
33
36
|
plural: string;
|
|
34
37
|
};
|
|
35
|
-
/**
|
|
36
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Which control renders a field, from its wire type, its `enum`, its widget
|
|
40
|
+
* hint and its roles (`deriveFieldRoles`): a text field with an `enum` is a
|
|
41
|
+
* `select`; an INTEGER field with the `money` role (an explicit role, a
|
|
42
|
+
* `format: 'money'` hint, the legacy `ui.widget: 'currency'`, or a money
|
|
43
|
+
* name like `totalAmount`) is `money`. Without `roles`, only the
|
|
44
|
+
* definition's own hints count.
|
|
45
|
+
*/
|
|
46
|
+
export declare function inputKindFor(definition: ScreenFieldDefinition, roles?: readonly FieldRole[]): ScreenInputKind;
|
|
37
47
|
/**
|
|
38
48
|
* Mismatch between a definition and a policy, or `null` when they belong
|
|
39
49
|
* together. Components surface this instead of rendering the wrong object.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../src/components/screens/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EACV,0BAA0B,EAC1B,WAAW,EACX,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,YAAY,EACb,MAAM,YAAY,CAAC;AAepB,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,UAAU,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,2BAA2B;IAC3B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAStD;
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../src/components/screens/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EACV,0BAA0B,EAC1B,WAAW,EACX,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,YAAY,EACb,MAAM,YAAY,CAAC;AAepB,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,UAAU,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,2BAA2B;IAC3B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,uEAAuE;AACvE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,qBAAqB,GAChC,MAAM,CASR;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,UAAU,EAAE,0BAA0B,GAAG;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAWA;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,qBAAqB,EACjC,KAAK,CAAC,EAAE,SAAS,SAAS,EAAE,GAC3B,eAAe,CAgCjB;AAUD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,0BAA0B,EACtC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,IAAI,GACtC,MAAM,GAAG,IAAI,CAKf;AAiCD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,0BAA0B,EACtC,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,IAAI,EACvC,OAAO,EAAE,yBAAyB,GACjC,WAAW,EAAE,CAmHf;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,WAAW,EAAE,GAC7B,KAAK,CAAC,CAAC,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAQvC"}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of list / view / edit fields from a manifest-derived
|
|
3
|
+
* collection definition and its resolved field policy (#3718).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors smrt-fields' `ObjectForm` selection rule: a field renders only when
|
|
6
|
+
* it is in BOTH the generated web definition and the resolved policy, so
|
|
7
|
+
* sensitive, transient and read-permission-gated fields (which the policy
|
|
8
|
+
* resolver omits) can never appear on a generated screen. Without a policy the
|
|
9
|
+
* code seed applies: the manifest `ui` hints and the cold-start rule (no
|
|
10
|
+
* `basic` markers anywhere => everything basic; any marker => unmarked fields
|
|
11
|
+
* advanced).
|
|
12
|
+
*/
|
|
13
|
+
import { deriveFieldRoles } from './roles.js';
|
|
1
14
|
/** Managed by the platform; never editable and never a default column. */
|
|
2
15
|
const SYSTEM_FIELDS = new Set([
|
|
3
16
|
'id',
|
|
@@ -23,7 +36,8 @@ export function humanizeFieldName(name) {
|
|
|
23
36
|
const lower = spaced.toLowerCase();
|
|
24
37
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
25
38
|
}
|
|
26
|
-
|
|
39
|
+
/** Default label of a field: reference fields drop a trailing `Id`. */
|
|
40
|
+
export function humanizeLabel(name, definition) {
|
|
27
41
|
if ((definition.type === 'foreignKey' ||
|
|
28
42
|
definition.type === 'crossPackageRef') &&
|
|
29
43
|
/.[a-z]Id$/.test(name)) {
|
|
@@ -44,10 +58,19 @@ export function screenTitles(definition) {
|
|
|
44
58
|
: `${singular}s`;
|
|
45
59
|
return { singular, plural };
|
|
46
60
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Which control renders a field, from its wire type, its `enum`, its widget
|
|
63
|
+
* hint and its roles (`deriveFieldRoles`): a text field with an `enum` is a
|
|
64
|
+
* `select`; an INTEGER field with the `money` role (an explicit role, a
|
|
65
|
+
* `format: 'money'` hint, the legacy `ui.widget: 'currency'`, or a money
|
|
66
|
+
* name like `totalAmount`) is `money`. Without `roles`, only the
|
|
67
|
+
* definition's own hints count.
|
|
68
|
+
*/
|
|
69
|
+
export function inputKindFor(definition, roles) {
|
|
49
70
|
const widget = definition.ui?.widget;
|
|
50
71
|
if (definition.type === 'text') {
|
|
72
|
+
if (Array.isArray(definition.enum) && definition.enum.length > 0)
|
|
73
|
+
return 'select';
|
|
51
74
|
if (widget === 'textarea')
|
|
52
75
|
return 'textarea';
|
|
53
76
|
if (widget === 'email')
|
|
@@ -61,7 +84,10 @@ export function inputKindFor(definition) {
|
|
|
61
84
|
// Money is integer minor units only; a currency hint on a decimal is a
|
|
62
85
|
// plain decimal (rates and confidences are the only decimals).
|
|
63
86
|
if (definition.type === 'integer') {
|
|
64
|
-
|
|
87
|
+
const money = roles?.includes('money') ??
|
|
88
|
+
(widget === 'currency' ||
|
|
89
|
+
(definition.format ?? definition.ui?.format) === 'money');
|
|
90
|
+
return money ? 'money' : 'integer';
|
|
65
91
|
}
|
|
66
92
|
switch (definition.type) {
|
|
67
93
|
case 'decimal':
|
|
@@ -132,6 +158,7 @@ export function deriveScreenFields(definition, policy, options) {
|
|
|
132
158
|
!SYSTEM_FIELDS.has(c.name) &&
|
|
133
159
|
c.name !== definition.idField);
|
|
134
160
|
const seedBasic = policy ? null : seedBasicSet(candidates);
|
|
161
|
+
const roles = deriveFieldRoles(definition);
|
|
135
162
|
const decorate = (c) => {
|
|
136
163
|
const policyField = policy?.fields[c.name];
|
|
137
164
|
if (policy && !policyField)
|
|
@@ -145,17 +172,21 @@ export function deriveScreenFields(definition, policy, options) {
|
|
|
145
172
|
return null;
|
|
146
173
|
const label = policyField?.label?.trim();
|
|
147
174
|
const help = policyField?.help ?? c.definition.description ?? null;
|
|
175
|
+
const fieldRoles = roles[c.name] ?? [];
|
|
176
|
+
const kind = inputKindFor(c.definition, fieldRoles);
|
|
148
177
|
return {
|
|
149
178
|
name: c.name,
|
|
150
179
|
label: label || humanizeLabel(c.name, c.definition),
|
|
151
180
|
help: help && help.trim() !== '' ? help : null,
|
|
152
181
|
type: c.definition.type,
|
|
153
|
-
kind
|
|
182
|
+
kind,
|
|
154
183
|
required: isRequired(c.definition, policyField?.required),
|
|
155
184
|
tier,
|
|
156
185
|
group: policyField?.group ?? c.definition.ui?.group ?? null,
|
|
157
186
|
order: policyField?.order ?? c.definition.ui?.order ?? null,
|
|
158
187
|
definition: c.definition,
|
|
188
|
+
roles: fieldRoles,
|
|
189
|
+
...(kind === 'select' ? { options: [...(c.definition.enum ?? [])] } : {}),
|
|
159
190
|
};
|
|
160
191
|
};
|
|
161
192
|
const decorated = candidates
|
|
@@ -177,6 +208,7 @@ export function deriveScreenFields(definition, policy, options) {
|
|
|
177
208
|
group: null,
|
|
178
209
|
order: null,
|
|
179
210
|
definition: fieldDefinition,
|
|
211
|
+
roles: roles[name] ?? [],
|
|
180
212
|
};
|
|
181
213
|
};
|
|
182
214
|
if (include) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
export { lineItemColumns, relatedColumns } from './columns.js';
|
|
1
2
|
export { default as DetailScreen } from './DetailScreen.svelte';
|
|
2
3
|
export { default as EditForm } from './EditForm.svelte';
|
|
3
4
|
export { DEFAULT_MAX_LIST_COLUMNS, type DeriveScreenFieldsOptions, deriveScreenFields, groupScreenFields, humanizeFieldName, inputKindFor, screenSourceError, screenTitles, } from './fields.js';
|
|
4
5
|
export { default as ListScreen } from './ListScreen.svelte';
|
|
5
6
|
export { default as RecipeScreens } from './RecipeScreens.svelte';
|
|
6
|
-
export
|
|
7
|
-
export {
|
|
7
|
+
export { default as RecordScreen } from './RecordScreen.svelte';
|
|
8
|
+
export { deriveRecordHeader, RECORD_HEADER_MAX_FACTS, type RecordFact, type RecordFactKind, type RecordHeaderModel, recordTotals, totalsFields, } from './record-header.js';
|
|
9
|
+
export { deriveScreenRelations, relationKey, type ScreenRelation, } from './relations.js';
|
|
10
|
+
export { currencyCodeField, deriveFieldRoles, FIELD_ROLES, type FieldRole, fieldsWithRole, fieldWithRole, hasLineItemRoles, isDocumentLike, type ScreenFieldRoles, } from './roles.js';
|
|
11
|
+
export type { DetailScreenProps, EditFormProps, EditFormSubmitResult, ListScreenProps, RecipeScreensProps, RecipeScreensSource, RecipeScreenView, RecordScreenMode, RecordScreenProps, ScreenCollectionDefinition, ScreenDraft, ScreenDraftValue, ScreenField, ScreenFieldDefinition, ScreenFieldErrorCode, ScreenFieldFormat, ScreenFieldPolicy, ScreenFieldType, ScreenFieldUIHints, ScreenFieldVisibility, ScreenInputKind, ScreenMode, ScreenPolicy, ScreenRecord, ScreenRelationshipDefinition, ScreenRelationshipKind, } from './types.js';
|
|
12
|
+
export { compareScreenValues, currencyMinorDigits, DEFAULT_SCREEN_CURRENCY, draftForCreate, draftFromRecord, type FormatValueContext, formatScreenValue, jsonScreenEntries, majorStringToMinorUnits, minorUnitsToMajorString, type ParsedDraft, parseDraft, type ScreenJsonEntry, shortScreenId, toDatetimeLocal, toDraftValue, } from './values.js';
|
|
8
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/screens/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,0BAA0B,EAC1B,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/screens/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,YAAY,EACZ,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,EACX,KAAK,SAAS,EACd,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,0BAA0B,EAC1B,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,KAAK,kBAAkB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,KAAK,WAAW,EAChB,UAAU,EACV,KAAK,eAAe,EACpB,aAAa,EACb,eAAe,EACf,YAAY,GACb,MAAM,aAAa,CAAC"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
export { lineItemColumns, relatedColumns } from './columns.js';
|
|
1
2
|
export { default as DetailScreen } from './DetailScreen.svelte';
|
|
2
3
|
export { default as EditForm } from './EditForm.svelte';
|
|
3
4
|
export { DEFAULT_MAX_LIST_COLUMNS, deriveScreenFields, groupScreenFields, humanizeFieldName, inputKindFor, screenSourceError, screenTitles, } from './fields.js';
|
|
4
5
|
export { default as ListScreen } from './ListScreen.svelte';
|
|
5
6
|
export { default as RecipeScreens } from './RecipeScreens.svelte';
|
|
6
|
-
export {
|
|
7
|
+
export { default as RecordScreen } from './RecordScreen.svelte';
|
|
8
|
+
export { deriveRecordHeader, RECORD_HEADER_MAX_FACTS, recordTotals, totalsFields, } from './record-header.js';
|
|
9
|
+
export { deriveScreenRelations, relationKey, } from './relations.js';
|
|
10
|
+
export { currencyCodeField, deriveFieldRoles, FIELD_ROLES, fieldsWithRole, fieldWithRole, hasLineItemRoles, isDocumentLike, } from './roles.js';
|
|
11
|
+
export { compareScreenValues, currencyMinorDigits, DEFAULT_SCREEN_CURRENCY, draftForCreate, draftFromRecord, formatScreenValue, jsonScreenEntries, majorStringToMinorUnits, minorUnitsToMajorString, parseDraft, shortScreenId, toDatetimeLocal, toDraftValue, } from './values.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ScreenFieldRoles } from './roles.js';
|
|
2
|
+
import type { ScreenCollectionDefinition, ScreenField, ScreenRecord } from './types.js';
|
|
3
|
+
export type RecordFactKind = 'party' | 'date' | 'money' | 'contact';
|
|
4
|
+
export interface RecordFact {
|
|
5
|
+
kind: RecordFactKind;
|
|
6
|
+
field: ScreenField;
|
|
7
|
+
}
|
|
8
|
+
export interface RecordHeaderModel {
|
|
9
|
+
heading: string;
|
|
10
|
+
/** The heading leads with the identifier (a document): render it monospace there. */
|
|
11
|
+
headingIdentifier: boolean;
|
|
12
|
+
identifier?: {
|
|
13
|
+
field: ScreenField;
|
|
14
|
+
value: string;
|
|
15
|
+
};
|
|
16
|
+
status?: {
|
|
17
|
+
field: ScreenField;
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
facts: RecordFact[];
|
|
21
|
+
/** Fields the header shows; the details card leaves them out. */
|
|
22
|
+
shown: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Most key facts in a header. */
|
|
25
|
+
export declare const RECORD_HEADER_MAX_FACTS = 4;
|
|
26
|
+
/**
|
|
27
|
+
* The header of `record`: `fields` are the view fields
|
|
28
|
+
* (`deriveScreenFields(..., { mode: 'view' })`), `roles` the definition's
|
|
29
|
+
* (`deriveFieldRoles`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function deriveRecordHeader(definition: ScreenCollectionDefinition, fields: readonly ScreenField[], record: ScreenRecord, roles: ScreenFieldRoles, options?: {
|
|
32
|
+
title?: string;
|
|
33
|
+
maxFacts?: number;
|
|
34
|
+
}): RecordHeaderModel;
|
|
35
|
+
/**
|
|
36
|
+
* The record's own totals, read by role (`subtotal`, `discount`, `tax`,
|
|
37
|
+
* `total`): what the line-items totals block shows as authoritative. With
|
|
38
|
+
* `visible` (the policy-filtered view fields), a field the policy omits is
|
|
39
|
+
* never read.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recordTotals(record: ScreenRecord, roles: ScreenFieldRoles, visible?: readonly {
|
|
42
|
+
name: string;
|
|
43
|
+
}[]): {
|
|
44
|
+
subtotal?: number | null;
|
|
45
|
+
discount?: number | null;
|
|
46
|
+
tax?: number | null;
|
|
47
|
+
total?: number | null;
|
|
48
|
+
};
|
|
49
|
+
/** Fields holding a totals role (`subtotal` / `discount` / `tax` / `total`). */
|
|
50
|
+
export declare function totalsFields(roles: ScreenFieldRoles): string[];
|
|
51
|
+
//# sourceMappingURL=record-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-header.d.ts","sourceRoot":"","sources":["../../../src/components/screens/record-header.ts"],"names":[],"mappings":"AAcA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,EACV,0BAA0B,EAC1B,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,iEAAiE;IACjE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,kCAAkC;AAClC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAYzC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,0BAA0B,EACtC,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,gBAAgB,EACvB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,iBAAiB,CAyEnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GACpC;IACD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAcA;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAM9D"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record page header (#3718 record page), pure and Node-tested: the
|
|
3
|
+
* heading, the monospace identifier, the status and two to four key facts,
|
|
4
|
+
* all read by role from the fields the screen may show (so a field the
|
|
5
|
+
* policy omits can never reach the header).
|
|
6
|
+
*
|
|
7
|
+
* - Heading: the `title` prop, else a document's kind and identifier
|
|
8
|
+
* ("Order GRA-3159", never the customer), else the title-role field, else
|
|
9
|
+
* the identifier, else the class name.
|
|
10
|
+
* - Facts, in order: the party (a link), the lead date, the total money (the
|
|
11
|
+
* `total` role, else the first money field), then a contact (email, then
|
|
12
|
+
* phone) while there is room. Empty values are skipped.
|
|
13
|
+
*/
|
|
14
|
+
import { screenTitles } from './fields.js';
|
|
15
|
+
import { isDocumentLike } from './roles.js';
|
|
16
|
+
/** Most key facts in a header. */
|
|
17
|
+
export const RECORD_HEADER_MAX_FACTS = 4;
|
|
18
|
+
function present(value) {
|
|
19
|
+
if (value === null || value === undefined)
|
|
20
|
+
return false;
|
|
21
|
+
if (typeof value === 'string')
|
|
22
|
+
return value.trim() !== '';
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function hasRole(field, role) {
|
|
26
|
+
return (field.roles ?? []).includes(role);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The header of `record`: `fields` are the view fields
|
|
30
|
+
* (`deriveScreenFields(..., { mode: 'view' })`), `roles` the definition's
|
|
31
|
+
* (`deriveFieldRoles`).
|
|
32
|
+
*/
|
|
33
|
+
export function deriveRecordHeader(definition, fields, record, roles, options = {}) {
|
|
34
|
+
const { singular } = screenTitles(definition);
|
|
35
|
+
const filled = fields.filter((f) => present(record[f.name]));
|
|
36
|
+
const find = (role) => filled.find((f) => hasRole(f, role));
|
|
37
|
+
const shown = [];
|
|
38
|
+
const titleField = find('title');
|
|
39
|
+
const identifierField = find('identifier');
|
|
40
|
+
const statusField = find('status');
|
|
41
|
+
const identifierValue = identifierField
|
|
42
|
+
? String(record[identifierField.name])
|
|
43
|
+
: undefined;
|
|
44
|
+
let heading;
|
|
45
|
+
let headingIdentifier = false;
|
|
46
|
+
if (options.title) {
|
|
47
|
+
heading = options.title;
|
|
48
|
+
}
|
|
49
|
+
else if (identifierValue && isDocumentLike(roles)) {
|
|
50
|
+
heading = `${singular} ${identifierValue}`;
|
|
51
|
+
headingIdentifier = true;
|
|
52
|
+
}
|
|
53
|
+
else if (titleField) {
|
|
54
|
+
heading = String(record[titleField.name]);
|
|
55
|
+
}
|
|
56
|
+
else if (identifierValue) {
|
|
57
|
+
heading = identifierValue;
|
|
58
|
+
headingIdentifier = true;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
heading = singular;
|
|
62
|
+
}
|
|
63
|
+
if (titleField && !headingIdentifier && !options.title)
|
|
64
|
+
shown.push(titleField.name);
|
|
65
|
+
const out = {
|
|
66
|
+
heading,
|
|
67
|
+
headingIdentifier,
|
|
68
|
+
facts: [],
|
|
69
|
+
shown,
|
|
70
|
+
};
|
|
71
|
+
if (identifierField && identifierValue) {
|
|
72
|
+
out.identifier = { field: identifierField, value: identifierValue };
|
|
73
|
+
shown.push(identifierField.name);
|
|
74
|
+
}
|
|
75
|
+
if (statusField) {
|
|
76
|
+
out.status = {
|
|
77
|
+
field: statusField,
|
|
78
|
+
value: String(record[statusField.name]),
|
|
79
|
+
};
|
|
80
|
+
shown.push(statusField.name);
|
|
81
|
+
}
|
|
82
|
+
const max = options.maxFacts ?? RECORD_HEADER_MAX_FACTS;
|
|
83
|
+
const add = (kind, field) => {
|
|
84
|
+
if (!field || shown.includes(field.name) || out.facts.length >= max)
|
|
85
|
+
return;
|
|
86
|
+
out.facts.push({ kind, field });
|
|
87
|
+
shown.push(field.name);
|
|
88
|
+
};
|
|
89
|
+
add('party', find('party'));
|
|
90
|
+
add('date', filled.find((f) => hasRole(f, 'date') && f.name !== 'createdAt' && f.name !== 'updatedAt'));
|
|
91
|
+
add('money', find('total') ?? find('money'));
|
|
92
|
+
add('contact', filled.find((f) => f.kind === 'email'));
|
|
93
|
+
add('contact', filled.find((f) => f.kind === 'tel'));
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The record's own totals, read by role (`subtotal`, `discount`, `tax`,
|
|
98
|
+
* `total`): what the line-items totals block shows as authoritative. With
|
|
99
|
+
* `visible` (the policy-filtered view fields), a field the policy omits is
|
|
100
|
+
* never read.
|
|
101
|
+
*/
|
|
102
|
+
export function recordTotals(record, roles, visible) {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const role of ['subtotal', 'discount', 'tax', 'total']) {
|
|
105
|
+
const field = Object.keys(roles).find((name) => roles[name]?.includes(role) &&
|
|
106
|
+
(!visible || visible.some((f) => f.name === name)));
|
|
107
|
+
if (!field)
|
|
108
|
+
continue;
|
|
109
|
+
const value = record[field];
|
|
110
|
+
out[role] =
|
|
111
|
+
typeof value === 'number' && Number.isSafeInteger(value) ? value : null;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** Fields holding a totals role (`subtotal` / `discount` / `tax` / `total`). */
|
|
116
|
+
export function totalsFields(roles) {
|
|
117
|
+
return Object.keys(roles).filter((name) => ['subtotal', 'discount', 'tax', 'total'].some((role) => roles[name]?.includes(role)));
|
|
118
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of a model's relations from the generated web collection
|
|
3
|
+
* definitions (#3718 record view).
|
|
4
|
+
*
|
|
5
|
+
* A `children` relation is another collection whose `foreignKey` /
|
|
6
|
+
* `crossPackageRef` edge targets this model (a tab of the record page's
|
|
7
|
+
* related records); an `items` relation is a `children` one whose rows are
|
|
8
|
+
* this record's line items (the line-items table); a `reference` relation is one of this model's
|
|
9
|
+
* own FK fields (rendered as a link on the detail). Edges whose target or
|
|
10
|
+
* source is not in `definitions` are skipped: an unexposed model has neither
|
|
11
|
+
* a definition nor a route, so nothing could be linked.
|
|
12
|
+
*/
|
|
13
|
+
import type { RecordListColumn } from '../overview/widgets/data.js';
|
|
14
|
+
import type { ScreenCollectionDefinition } from './types.js';
|
|
15
|
+
export interface ScreenRelation {
|
|
16
|
+
/** Qualified model id (`objectRef`) of the child (`children`) or target (`reference`). */
|
|
17
|
+
model: string;
|
|
18
|
+
/** FK field name on the child (`children`) or on this model (`reference`). */
|
|
19
|
+
field: string;
|
|
20
|
+
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* `items` is a `children` relation whose children are this record's line
|
|
23
|
+
* items: the edge says so (`relationships[].role: 'items'`), or the child
|
|
24
|
+
* has line-item roles (a quantity and a money field).
|
|
25
|
+
*/
|
|
26
|
+
kind: 'children' | 'reference' | 'items';
|
|
27
|
+
/**
|
|
28
|
+
* `children` / `items` only: the table columns for the child model, chosen
|
|
29
|
+
* by role (`relatedColumns`; `lineItemColumns` for items), the FK back to
|
|
30
|
+
* this model excluded.
|
|
31
|
+
*/
|
|
32
|
+
columns?: readonly RecordListColumn[];
|
|
33
|
+
/** `items` only: the child field lines are ordered by (`sortOrder`, ..., else `createdAt`). */
|
|
34
|
+
sort?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A relation's stable key, `<Class>-<field>` (`Order-customerId`): what the
|
|
38
|
+
* `related` widget's `relations` option lists and default widget ids end in.
|
|
39
|
+
*/
|
|
40
|
+
export declare function relationKey(relation: Pick<ScreenRelation, 'model' | 'field'>): string;
|
|
41
|
+
/**
|
|
42
|
+
* Relations of `model` (a qualified `objectRef`, or a key of `definitions`).
|
|
43
|
+
* `items` come first, then `children`, each sorted by label; `reference`
|
|
44
|
+
* relations follow in declaration order. A child with two FKs to the same
|
|
45
|
+
* parent yields two relations, labelled `Orders (Owner)` /
|
|
46
|
+
* `Orders (Approver)` (and is only `items` when an edge says so).
|
|
47
|
+
*/
|
|
48
|
+
export declare function deriveScreenRelations(definitions: Readonly<Record<string, ScreenCollectionDefinition>>, model: string): ScreenRelation[];
|
|
49
|
+
//# sourceMappingURL=relations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../../../src/components/screens/relations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAIpE,OAAO,KAAK,EACV,0BAA0B,EAE3B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,+FAA+F;IAC/F,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAID;;;GAGG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC,GAChD,MAAM,CAKR;AAyBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC,EACjE,KAAK,EAAE,MAAM,GACZ,cAAc,EAAE,CAqElB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { lineItemColumns, relatedColumns } from './columns.js';
|
|
2
|
+
import { humanizeLabel, screenTitles } from './fields.js';
|
|
3
|
+
import { deriveFieldRoles, hasLineItemRoles } from './roles.js';
|
|
4
|
+
const LINE_ORDER_FIELDS = ['sortOrder', 'position', 'lineNumber', 'sequence'];
|
|
5
|
+
/**
|
|
6
|
+
* A relation's stable key, `<Class>-<field>` (`Order-customerId`): what the
|
|
7
|
+
* `related` widget's `relations` option lists and default widget ids end in.
|
|
8
|
+
*/
|
|
9
|
+
export function relationKey(relation) {
|
|
10
|
+
const className = relation.model.split(/[:/]/).pop() ?? relation.model;
|
|
11
|
+
return `${className}-${relation.field}`
|
|
12
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
13
|
+
.slice(0, 64);
|
|
14
|
+
}
|
|
15
|
+
const FK_KINDS = new Set([
|
|
16
|
+
'foreignKey',
|
|
17
|
+
'crossPackageRef',
|
|
18
|
+
]);
|
|
19
|
+
function fieldLabel(field, definition) {
|
|
20
|
+
return humanizeLabel(field, definition.fields[field]);
|
|
21
|
+
}
|
|
22
|
+
/** Resolve a relationship target by `definitions` key, then by `name`. */
|
|
23
|
+
function targetOf(definitions, collection) {
|
|
24
|
+
return (definitions[collection] ??
|
|
25
|
+
Object.values(definitions).find((d) => d.name === collection));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Relations of `model` (a qualified `objectRef`, or a key of `definitions`).
|
|
29
|
+
* `items` come first, then `children`, each sorted by label; `reference`
|
|
30
|
+
* relations follow in declaration order. A child with two FKs to the same
|
|
31
|
+
* parent yields two relations, labelled `Orders (Owner)` /
|
|
32
|
+
* `Orders (Approver)` (and is only `items` when an edge says so).
|
|
33
|
+
*/
|
|
34
|
+
export function deriveScreenRelations(definitions, model) {
|
|
35
|
+
const self = Object.values(definitions).find((d) => d.objectRef === model) ??
|
|
36
|
+
definitions[model];
|
|
37
|
+
if (!self)
|
|
38
|
+
return [];
|
|
39
|
+
const children = [];
|
|
40
|
+
const perChild = new Map();
|
|
41
|
+
for (const child of Object.values(definitions)) {
|
|
42
|
+
const edges = (child.relationships ?? []).filter((edge) => FK_KINDS.has(edge.kind) &&
|
|
43
|
+
edge.field in child.fields &&
|
|
44
|
+
targetOf(definitions, edge.relatedCollection)?.objectRef ===
|
|
45
|
+
self.objectRef);
|
|
46
|
+
const titles = screenTitles(child);
|
|
47
|
+
const roles = edges.length > 0 ? deriveFieldRoles(child) : {};
|
|
48
|
+
const lineLike = hasLineItemRoles(roles);
|
|
49
|
+
const list = edges.map((edge) => {
|
|
50
|
+
const label = edges.length > 1
|
|
51
|
+
? `${titles.plural} (${fieldLabel(edge.field, child)})`
|
|
52
|
+
: titles.plural;
|
|
53
|
+
if (edge.role === 'items' || (lineLike && edges.length === 1)) {
|
|
54
|
+
return {
|
|
55
|
+
model: child.objectRef,
|
|
56
|
+
field: edge.field,
|
|
57
|
+
label,
|
|
58
|
+
kind: 'items',
|
|
59
|
+
columns: lineItemColumns(child, roles, { exclude: [edge.field] }),
|
|
60
|
+
sort: LINE_ORDER_FIELDS.find((name) => name in child.fields) ??
|
|
61
|
+
'createdAt',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
model: child.objectRef,
|
|
66
|
+
field: edge.field,
|
|
67
|
+
label,
|
|
68
|
+
kind: 'children',
|
|
69
|
+
columns: relatedColumns(child, roles, { exclude: [edge.field] }),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
if (list.length > 0)
|
|
73
|
+
perChild.set(child.objectRef, list);
|
|
74
|
+
}
|
|
75
|
+
for (const list of perChild.values())
|
|
76
|
+
children.push(...list);
|
|
77
|
+
children.sort((a, b) => Number(b.kind === 'items') - Number(a.kind === 'items') ||
|
|
78
|
+
a.label.localeCompare(b.label));
|
|
79
|
+
const references = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
for (const edge of self.relationships ?? []) {
|
|
82
|
+
if (!FK_KINDS.has(edge.kind) || seen.has(edge.field))
|
|
83
|
+
continue;
|
|
84
|
+
if (!(edge.field in self.fields))
|
|
85
|
+
continue;
|
|
86
|
+
const target = targetOf(definitions, edge.relatedCollection);
|
|
87
|
+
if (!target)
|
|
88
|
+
continue;
|
|
89
|
+
seen.add(edge.field);
|
|
90
|
+
references.push({
|
|
91
|
+
model: target.objectRef,
|
|
92
|
+
field: edge.field,
|
|
93
|
+
label: fieldLabel(edge.field, self),
|
|
94
|
+
kind: 'reference',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return [...children, ...references];
|
|
98
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field roles (#3718 record page): what a field MEANS on a record page, as
|
|
3
|
+
* opposed to how it is stored. A record header reads its title, identifier,
|
|
4
|
+
* status and key facts by role; related tables pick their columns by role;
|
|
5
|
+
* line items and the totals block read quantity, money and the
|
|
6
|
+
* subtotal / tax / discount / total roles.
|
|
7
|
+
*
|
|
8
|
+
* Pure and Node-tested. Sources, in order:
|
|
9
|
+
*
|
|
10
|
+
* 1. Explicit metadata on the field definition: `role`, `format`, `enum`,
|
|
11
|
+
* `display`. An explicit `role` is authoritative: name heuristics never
|
|
12
|
+
* add to it (a `format` / `display` still does).
|
|
13
|
+
* 2. Manifest `ui` hints: `ui.format` (`money` / `date` / `datetime`), and
|
|
14
|
+
* `ui.widget: 'currency'` on an INTEGER field (the legacy money hint; on a
|
|
15
|
+
* text field it names the currency CODE, which is not money).
|
|
16
|
+
* 3. Name heuristics, only for fields with no explicit `role`.
|
|
17
|
+
*
|
|
18
|
+
* Money is integer minor units only (the cross-package invariant), so the
|
|
19
|
+
* name heuristics never mark a decimal as money; an explicit `role` or
|
|
20
|
+
* `format` on a decimal is honoured as declared.
|
|
21
|
+
*/
|
|
22
|
+
import type { ScreenCollectionDefinition } from './types.js';
|
|
23
|
+
/** What a field means on a record page. One field may hold several roles. */
|
|
24
|
+
export type FieldRole = 'title' | 'identifier' | 'status' | 'money' | 'quantity' | 'date' | 'party' | 'subtotal' | 'tax' | 'discount' | 'total';
|
|
25
|
+
/** Every role, in the order a role list is reported. */
|
|
26
|
+
export declare const FIELD_ROLES: readonly FieldRole[];
|
|
27
|
+
/** Roles of every declared field of a definition, keyed by field name. */
|
|
28
|
+
export type ScreenFieldRoles = Readonly<Record<string, readonly FieldRole[]>>;
|
|
29
|
+
/**
|
|
30
|
+
* Roles of every declared field of `definition` (a key per field, possibly
|
|
31
|
+
* an empty list). Without an explicit `display` field, the first text field
|
|
32
|
+
* named `name` / `title` / `label` / `displayName` takes the `title` role.
|
|
33
|
+
*/
|
|
34
|
+
export declare function deriveFieldRoles(definition: ScreenCollectionDefinition): Record<string, FieldRole[]>;
|
|
35
|
+
/** Fields holding `role`, in declaration order. */
|
|
36
|
+
export declare function fieldsWithRole(roles: ScreenFieldRoles, role: FieldRole): string[];
|
|
37
|
+
/** The first field holding `role`, or `undefined`. */
|
|
38
|
+
export declare function fieldWithRole(roles: ScreenFieldRoles, role: FieldRole): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The field naming a record's currency: a text field hinted
|
|
41
|
+
* `ui.widget: 'currency'` (the manifest's currency-code picker), else a text
|
|
42
|
+
* field named `currency` / `currencyCode`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function currencyCodeField(definition: ScreenCollectionDefinition): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Whether records of this definition read like documents (an order, an
|
|
47
|
+
* invoice): an identifier plus a party or a total. Their header leads with
|
|
48
|
+
* the identifier ("Order GRA-3159") rather than a name.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isDocumentLike(roles: ScreenFieldRoles): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a child definition has line-item roles: a quantity and a money
|
|
53
|
+
* field (`deriveScreenRelations` classifies such a child relation `items`).
|
|
54
|
+
*/
|
|
55
|
+
export declare function hasLineItemRoles(roles: ScreenFieldRoles): boolean;
|
|
56
|
+
//# sourceMappingURL=roles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../../src/components/screens/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,0BAA0B,EAG3B,MAAM,YAAY,CAAC;AAEpB,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,YAAY,GACZ,QAAQ,GACR,OAAO,GACP,UAAU,GACV,MAAM,GACN,OAAO,GACP,UAAU,GACV,KAAK,GACL,UAAU,GACV,OAAO,CAAC;AAEZ,wDAAwD;AACxD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAY3C,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC,CAAC;AAsG9E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,0BAA0B,GACrC,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CA0C7B;AAED,mDAAmD;AACnD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,GACd,MAAM,EAAE,CAEV;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAC3B,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,SAAS,GACd,MAAM,GAAG,SAAS,CAEpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,0BAA0B,GACrC,MAAM,GAAG,SAAS,CAUpB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAM/D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAKjE"}
|