ngx-t-forms-types 0.0.30 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interfaces/Form/formSubmissionHandleInterface.d.ts +6 -0
- package/dist/interfaces/FormBuilder/DefaultEelement.js +76 -0
- package/dist/interfaces/FormBuilder/DefaultInputConfigInterface.d.ts +13 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.d.ts +6 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.js +6 -0
- package/dist/interfaces/FormBuilder/inputConfig/ElementEditConfig.js +188 -3
- package/dist/interfaces/Import/ImportIdentity.d.ts +32 -0
- package/dist/interfaces/Import/ImportIdentity.js +1 -0
- package/dist/interfaces/Import/ImportProgress.d.ts +12 -1
- package/dist/interfaces/Import/ImportRowState.d.ts +52 -1
- package/dist/interfaces/Import/index.d.ts +2 -1
- package/dist/interfaces/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
- package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
- package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
- package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
- package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
- package/dist/schemas/FormInputSchema.js +41 -2
- package/dist/schemas/MatOptionsSchema.js +7 -0
- package/dist/schemas/MscoaConfigSchema.js +1 -0
- package/dist/schemas/index.d.ts +2 -1
- package/dist/schemas/index.js +2 -1
- package/dist/skillet/authoring-rules.d.ts +41 -0
- package/dist/skillet/authoring-rules.js +61 -0
- package/dist/skillet/extra-members.d.ts +143 -0
- package/dist/skillet/extra-members.js +79 -0
- package/dist/skillet/form.d.ts +170 -0
- package/dist/skillet/form.js +139 -0
- package/dist/skillet/index.d.ts +43 -0
- package/dist/skillet/index.js +46 -0
- package/dist/skillet/input-members.d.ts +375 -0
- package/dist/skillet/input-members.js +186 -0
- package/dist/skillet/internal/coupling.d.ts +63 -0
- package/dist/skillet/internal/coupling.js +69 -0
- package/dist/skillet/internal/editor-guidance.d.ts +64 -0
- package/dist/skillet/internal/editor-guidance.js +291 -0
- package/dist/skillet/members/calculated-field.d.ts +155 -0
- package/dist/skillet/members/calculated-field.js +105 -0
- package/dist/skillet/members/conditional.d.ts +69 -0
- package/dist/skillet/members/conditional.js +16 -0
- package/dist/skillet/members/document-picker.d.ts +182 -0
- package/dist/skillet/members/document-picker.js +111 -0
- package/dist/skillet/members/mat-options.d.ts +187 -0
- package/dist/skillet/members/mat-options.js +92 -0
- package/dist/skillet/members/mscoa.d.ts +182 -0
- package/dist/skillet/members/mscoa.js +129 -0
- package/dist/skillet/members/pagination.d.ts +59 -0
- package/dist/skillet/members/pagination.js +16 -0
- package/dist/skillet/members/table.d.ts +178 -0
- package/dist/skillet/members/table.js +105 -0
- package/dist/skillet/members/validators.d.ts +197 -0
- package/dist/skillet/members/validators.js +74 -0
- package/dist/skillet/members/value.d.ts +154 -0
- package/dist/skillet/members/value.js +97 -0
- package/dist/skillet/shared-types.d.ts +57 -0
- package/dist/skillet/shared-types.js +69 -0
- package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
- package/dist/skillet/tests/house-rules.spec.js +162 -0
- package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
- package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
- package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
- package/dist/skillet/tests/variant-assembly.spec.js +643 -0
- package/dist/skillet/variants.d.ts +138 -0
- package/dist/skillet/variants.js +536 -0
- package/dist/skillet/workflow-context.d.ts +95 -0
- package/dist/skillet/workflow-context.js +128 -0
- package/package.json +78 -60
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
/**
|
|
3
|
+
* Compile-time coupling primitives that bind the Skillet schemas to the
|
|
4
|
+
* interfaces they describe.
|
|
5
|
+
*
|
|
6
|
+
* The rule these enforce, stated once:
|
|
7
|
+
*
|
|
8
|
+
* `s.Infer<node>` must be ASSIGNABLE TO the interface member's type.
|
|
9
|
+
*
|
|
10
|
+
* Assignability — not equality — is deliberate. A Skillet node is allowed to be
|
|
11
|
+
* NARROWER than the interface (we routinely refuse to let a model emit a `Date`
|
|
12
|
+
* object or a custom-script sentinel even where the interface permits one), but
|
|
13
|
+
* it may never be WIDER. Widening is precisely the drift that produces a value
|
|
14
|
+
* the rest of the codebase cannot hold, so that is the direction we fail on.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Guards a Skillet node against the type it is meant to produce.
|
|
18
|
+
*
|
|
19
|
+
* Curried so the expected type is written explicitly and the node type is
|
|
20
|
+
* inferred:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const colSize = matches<number>()(s.integer('Grid width', { minimum: 1 }));
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* If the node's inferred output stops being assignable to `Expected` — because
|
|
27
|
+
* someone retyped the interface member, or loosened the node — the argument
|
|
28
|
+
* collapses to `never` and the call fails to compile.
|
|
29
|
+
*/
|
|
30
|
+
export function matches() {
|
|
31
|
+
return (node) => node;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Derives Skillet enum entries from a TypeScript string enum at runtime, so the
|
|
35
|
+
* two can never disagree: add a member to the enum and it appears in the schema
|
|
36
|
+
* on the next evaluation, with `s.Infer` widening to match.
|
|
37
|
+
*
|
|
38
|
+
* The `Record<string, string>` constraint rejects numeric and mixed enums —
|
|
39
|
+
* `Object.values` on those also yields the reverse-mapped numeric keys, which
|
|
40
|
+
* would silently poison the entry list. Skillet enumerations are string-only.
|
|
41
|
+
*/
|
|
42
|
+
export function enumValues(enumObject) {
|
|
43
|
+
return Object.values(enumObject);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Asserts a literal tuple covers a string union exactly.
|
|
47
|
+
*
|
|
48
|
+
* Needed for union *types* (`'hard' | 'soft'`), which — unlike enums — have no
|
|
49
|
+
* runtime representation to derive from, so the members must be restated. This
|
|
50
|
+
* makes restating them safe: drop one and the argument becomes `never`.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const WRAP = exhaustive<'hard' | 'soft'>()(['hard', 'soft'] as const);
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function exhaustive() {
|
|
57
|
+
return (entries) => entries;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a node as an absent-able member.
|
|
61
|
+
*
|
|
62
|
+
* Skillet has no `optional()`: every declared key must appear in the model's
|
|
63
|
+
* response. Optionality is therefore modelled as an explicit null branch — the
|
|
64
|
+
* key is always emitted, and `null` is how the model says "not set". Strip the
|
|
65
|
+
* nulls when converting a generated column into a `FormColumnInputs`.
|
|
66
|
+
*/
|
|
67
|
+
export function optional(node) {
|
|
68
|
+
return s.anyOf([node, s.nullish()]);
|
|
69
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expressions this module could not read, exposed so a test can assert the set
|
|
3
|
+
* is empty.
|
|
4
|
+
*
|
|
5
|
+
* The grammar in use is closed — `key === value`, joined only by `||` — but it
|
|
6
|
+
* is a string, so nothing stops a future edit reaching for `&&` or `!==`.
|
|
7
|
+
* Rather than throw at import time and take a consumer's app down over editor
|
|
8
|
+
* copy, unreadable expressions are skipped and recorded here, which turns a
|
|
9
|
+
* silent loss of guidance into a failing test.
|
|
10
|
+
*/
|
|
11
|
+
export declare const UNREADABLE_EXPRESSIONS: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Composes the description for a property: the authored sentence, then the
|
|
14
|
+
* editor's hint where it adds something, then when the property applies.
|
|
15
|
+
*
|
|
16
|
+
* @param base authored, model-facing description of the property
|
|
17
|
+
* @param path the property's `deepBind` path — a single key for a top-level
|
|
18
|
+
* member, a longer path for a member of a nested config
|
|
19
|
+
*/
|
|
20
|
+
export declare function describe(base: string, ...path: string[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Composes a description for a member of a record edited by a SECONDARY
|
|
23
|
+
* editor — a preset filter within `workflowPickerConfig.presetFilters`, say.
|
|
24
|
+
*
|
|
25
|
+
* @param base authored, model-facing description of the property
|
|
26
|
+
* @param opensAt the `deepBind` path of the row that opens the editor
|
|
27
|
+
* @param path the property's path within the record being edited
|
|
28
|
+
*/
|
|
29
|
+
export declare function describeIn(base: string, opensAt: readonly string[], ...path: string[]): string;
|
|
30
|
+
/**
|
|
31
|
+
* Numeric bounds the editor enforces on a property, ready to spread into a
|
|
32
|
+
* Skillet numeric node so the schema constrains what the settings panel does.
|
|
33
|
+
*/
|
|
34
|
+
export declare function rangeFor(...path: string[]): {
|
|
35
|
+
minimum?: number;
|
|
36
|
+
maximum?: number;
|
|
37
|
+
} | undefined;
|
|
38
|
+
/** Property paths the editor config carries guidance for. Used by tests. */
|
|
39
|
+
export declare const GUIDED_PATHS: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* The top-level members the settings panel edits on EVERY element.
|
|
42
|
+
*
|
|
43
|
+
* A row named `SpecialElementKeys.Default` is not looked up in an element's
|
|
44
|
+
* `properties` at all: it is rendered for every element, gated only by its own
|
|
45
|
+
* `additionalTest` and `disabled` tests. So the members those rows edit are
|
|
46
|
+
* part of every element's surface in the builder while appearing in no
|
|
47
|
+
* element's property list — `readonly`, `onlySetTempErrorOnTouch` and
|
|
48
|
+
* `tourContent` among them. Read here so variant assembly can offer what the
|
|
49
|
+
* builder offers, from the same source the builder reads.
|
|
50
|
+
*
|
|
51
|
+
* Only ungated rows qualify. A gated row is conditional on a VALUE (`min`
|
|
52
|
+
* needs a numeric `type`; `calculatedFieldRules` needs the flag), which is not
|
|
53
|
+
* "always shown" and is handled elsewhere. Nested bindings (`script.onChange`,
|
|
54
|
+
* `matOptions.fetch.value.source`) are members of a nested object, not of the
|
|
55
|
+
* column, and are left to the modules that own those objects.
|
|
56
|
+
*/
|
|
57
|
+
export declare const ALWAYS_SHOWN_MEMBERS: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* Secondary-editor paths, as `openingPath -> propertyPath`. Used by tests to
|
|
60
|
+
* assert the nested editors are actually being reached: before these were
|
|
61
|
+
* collected, every hint inside a record-list editor was invisible here, and
|
|
62
|
+
* nothing failed to say so.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SECONDARY_GUIDED_PATHS: ReadonlyMap<string, readonly string[]>;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { SpecialElementKeys } from '../../interfaces/FormBuilder/FormInputKeys.js';
|
|
2
|
+
import { getElementEditorConfig } from '../../interfaces/FormBuilder/inputConfig/ElementEditConfig.js';
|
|
3
|
+
/** Joins a `deepBind` path into the key this module indexes rows by. */
|
|
4
|
+
const pathKey = (path) => path.join('.');
|
|
5
|
+
function collectRows() {
|
|
6
|
+
const rows = [];
|
|
7
|
+
for (const section of getElementEditorConfig.editorSections ?? []) {
|
|
8
|
+
for (const element of section.elements ?? []) {
|
|
9
|
+
rows.push(element);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return rows;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Rows belonging to a `secondaryElementEditorConfig`, indexed by the path of
|
|
16
|
+
* the row that opens them and then by their own path within that editor.
|
|
17
|
+
*
|
|
18
|
+
* ## Why these are indexed separately rather than prefixed into `rowsByPath`
|
|
19
|
+
*
|
|
20
|
+
* A secondary editor's rows bind RELATIVE to the record it edits, so making
|
|
21
|
+
* them globally addressable means prefixing them with something. The obvious
|
|
22
|
+
* prefix — the opening row's `deepBind` — is right in one case and wrong in the
|
|
23
|
+
* other, which is the whole reason for this split:
|
|
24
|
+
*
|
|
25
|
+
* - `workflowPickerConfig.presetFilters` opens a `RecordListManager` over the
|
|
26
|
+
* filters themselves, so its inner `op` row really is
|
|
27
|
+
* `workflowPickerConfig.presetFilters[].op`.
|
|
28
|
+
* - `mscoaConfig` opens a composite editor whose secondary rows edit
|
|
29
|
+
* `dualCashExclusion.rules[]`, NOT `mscoaConfig` directly. Prefixing there
|
|
30
|
+
* would file the dual-cash `pattern` hint under `mscoaConfig.pattern` — a
|
|
31
|
+
* path nothing has, silently attaching accounting-exclusion guidance to
|
|
32
|
+
* whatever member later claimed that name.
|
|
33
|
+
*
|
|
34
|
+
* Nothing on the row says which of the two it is, so rather than guess, the
|
|
35
|
+
* caller states the container it is describing and looks rows up within it.
|
|
36
|
+
* Keeping this index apart from `rowsByPath` also means top-level descriptions
|
|
37
|
+
* are unchanged by its existence.
|
|
38
|
+
*/
|
|
39
|
+
function collectSecondaryRows() {
|
|
40
|
+
const index = new Map();
|
|
41
|
+
for (const parent of collectRows()) {
|
|
42
|
+
const sections = parent.secondaryElementEditorConfig;
|
|
43
|
+
if (!sections?.length || !parent.deepBind?.length)
|
|
44
|
+
continue;
|
|
45
|
+
const parentKey = pathKey(parent.deepBind);
|
|
46
|
+
const inner = index.get(parentKey) ?? new Map();
|
|
47
|
+
for (const section of sections) {
|
|
48
|
+
for (const row of section.elements ?? []) {
|
|
49
|
+
if (!row.deepBind?.length)
|
|
50
|
+
continue;
|
|
51
|
+
const key = pathKey(row.deepBind);
|
|
52
|
+
const existing = inner.get(key);
|
|
53
|
+
if (existing)
|
|
54
|
+
existing.push(row);
|
|
55
|
+
else
|
|
56
|
+
inner.set(key, [row]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (inner.size)
|
|
60
|
+
index.set(parentKey, inner);
|
|
61
|
+
}
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every editor row, indexed by its `deepBind` path.
|
|
66
|
+
*
|
|
67
|
+
* `name` is not the index: it reads `'default'` on most rows, with the property
|
|
68
|
+
* actually being edited carried in `deepBind`. Nested paths
|
|
69
|
+
* (`matOptions.fetch.options`) are indexed too, so the sub-schemas for the
|
|
70
|
+
* pending members can draw on the same guidance when they are built.
|
|
71
|
+
*/
|
|
72
|
+
const rowsByPath = (() => {
|
|
73
|
+
const index = new Map();
|
|
74
|
+
for (const row of collectRows()) {
|
|
75
|
+
if (!row.deepBind?.length)
|
|
76
|
+
continue;
|
|
77
|
+
const key = pathKey(row.deepBind);
|
|
78
|
+
const existing = index.get(key);
|
|
79
|
+
if (existing)
|
|
80
|
+
existing.push(row);
|
|
81
|
+
else
|
|
82
|
+
index.set(key, [row]);
|
|
83
|
+
}
|
|
84
|
+
return index;
|
|
85
|
+
})();
|
|
86
|
+
/**
|
|
87
|
+
* Expressions this module could not read, exposed so a test can assert the set
|
|
88
|
+
* is empty.
|
|
89
|
+
*
|
|
90
|
+
* The grammar in use is closed — `key === value`, joined only by `||` — but it
|
|
91
|
+
* is a string, so nothing stops a future edit reaching for `&&` or `!==`.
|
|
92
|
+
* Rather than throw at import time and take a consumer's app down over editor
|
|
93
|
+
* copy, unreadable expressions are skipped and recorded here, which turns a
|
|
94
|
+
* silent loss of guidance into a failing test.
|
|
95
|
+
*/
|
|
96
|
+
export const UNREADABLE_EXPRESSIONS = [];
|
|
97
|
+
/**
|
|
98
|
+
* Renders one comparison as prose.
|
|
99
|
+
*
|
|
100
|
+
* `!==` is tested first because `===` is not a substring of it — splitting on
|
|
101
|
+
* `===` would leave `valueSource !` as the key and read the rule backwards,
|
|
102
|
+
* which is worse than not reading it at all.
|
|
103
|
+
*/
|
|
104
|
+
function readComparison(clause) {
|
|
105
|
+
const negated = clause.includes('!==');
|
|
106
|
+
const parts = clause.split(negated ? '!==' : '===');
|
|
107
|
+
if (parts.length !== 2)
|
|
108
|
+
return null;
|
|
109
|
+
const key = parts[0].trim();
|
|
110
|
+
const value = parts[1].trim();
|
|
111
|
+
if (!key || !value)
|
|
112
|
+
return null;
|
|
113
|
+
return { key, negated, value };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Turns one applicability test into a clause such as
|
|
117
|
+
* `type is number or date`, or `null` if it cannot be read.
|
|
118
|
+
*/
|
|
119
|
+
function readTest(test) {
|
|
120
|
+
if (test.expression) {
|
|
121
|
+
const expression = test.expression;
|
|
122
|
+
// `&&` remains unreadable. Unlike `!==`, it changes the SHAPE of the rule
|
|
123
|
+
// from "any of these" to "all of these", and every collapse below assumes
|
|
124
|
+
// alternatives — reading one as a disjunction would state the opposite of
|
|
125
|
+
// what the editor enforces.
|
|
126
|
+
if (expression.includes('&&')) {
|
|
127
|
+
UNREADABLE_EXPRESSIONS.push(expression);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const comparisons = expression.split('||').map(readComparison);
|
|
131
|
+
if (comparisons.some((c) => c === null)) {
|
|
132
|
+
UNREADABLE_EXPRESSIONS.push(expression);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const read = comparisons;
|
|
136
|
+
const phrase = (negated) => (negated ? 'is not' : 'is');
|
|
137
|
+
// Every clause in the observed grammar tests the same key against
|
|
138
|
+
// alternatives, which reads far better collapsed than repeated. Collapsing
|
|
139
|
+
// requires a shared operator as well as a shared key: `a === x || a !== y`
|
|
140
|
+
// states two different things about `a` and must stay spelled out.
|
|
141
|
+
const [first] = read;
|
|
142
|
+
const uniform = read.every((c) => c.key === first.key && c.negated === first.negated);
|
|
143
|
+
if (uniform) {
|
|
144
|
+
const values = read.map((c) => c.value);
|
|
145
|
+
const list = values.length === 1
|
|
146
|
+
? values[0]
|
|
147
|
+
: `${values.slice(0, -1).join(', ')} or ${values[values.length - 1]}`;
|
|
148
|
+
return `${first.key} ${phrase(first.negated)} ${list}`;
|
|
149
|
+
}
|
|
150
|
+
return read
|
|
151
|
+
.map((c) => `${c.key} ${phrase(c.negated)} ${c.value}`)
|
|
152
|
+
.join(', or ');
|
|
153
|
+
}
|
|
154
|
+
if (test.testType === 'exists' && test.deepBind?.length) {
|
|
155
|
+
return `${pathKey(test.deepBind)} is set`;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Secondary-editor rows, indexed by opening path and then by their own path.
|
|
161
|
+
* See {@link collectSecondaryRows} for why these are kept out of
|
|
162
|
+
* {@link rowsByPath}.
|
|
163
|
+
*/
|
|
164
|
+
const secondaryRowsByParent = collectSecondaryRows();
|
|
165
|
+
/** The applicability sentence for a property, if the editor declares one. */
|
|
166
|
+
function applicabilityIn(rows) {
|
|
167
|
+
if (!rows?.length)
|
|
168
|
+
return undefined;
|
|
169
|
+
const clauses = rows
|
|
170
|
+
.flatMap((row) => row.additionalTest ?? [])
|
|
171
|
+
.map(readTest)
|
|
172
|
+
.filter((clause) => clause !== null);
|
|
173
|
+
const unique = [...new Set(clauses)];
|
|
174
|
+
if (!unique.length)
|
|
175
|
+
return undefined;
|
|
176
|
+
return `Only applies when ${unique.join(', or when ')}.`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Collapses the whitespace a hint picked up from its source.
|
|
180
|
+
*
|
|
181
|
+
* Several hints are written as indented multi-line template literals, so they
|
|
182
|
+
* arrive carrying newlines and runs of leading spaces. Rendered into a schema
|
|
183
|
+
* description those become blank lines and stray gaps before punctuation, which
|
|
184
|
+
* is exactly the kind of noise that makes a long system prompt harder to read.
|
|
185
|
+
*/
|
|
186
|
+
const collapseWhitespace = (text) => text
|
|
187
|
+
.replace(/\s+/g, ' ')
|
|
188
|
+
.replace(/\s+([.,;:])/g, '$1')
|
|
189
|
+
// Pulling a hint flush can bring a stray trailing full stop up against the
|
|
190
|
+
// one ending the previous sentence — at least one hint is written with a
|
|
191
|
+
// lone `.` on its own line. Collapse the pair back to a single stop.
|
|
192
|
+
.replace(/([.!?])[\s]*\1+/g, '$1')
|
|
193
|
+
.trim();
|
|
194
|
+
/** The authored hint for a property, if the editor carries one. */
|
|
195
|
+
function hintIn(rows) {
|
|
196
|
+
const hint = rows
|
|
197
|
+
?.map((row) => row.hint)
|
|
198
|
+
.find((h) => !!h?.trim());
|
|
199
|
+
return hint ? collapseWhitespace(hint) : undefined;
|
|
200
|
+
}
|
|
201
|
+
/** Ends a fragment with a full stop so composed sentences read cleanly. */
|
|
202
|
+
const terminate = (text) => /[.!?]$/.test(text) ? text : `${text}.`;
|
|
203
|
+
/**
|
|
204
|
+
* Composes the description for a property: the authored sentence, then the
|
|
205
|
+
* editor's hint where it adds something, then when the property applies.
|
|
206
|
+
*
|
|
207
|
+
* @param base authored, model-facing description of the property
|
|
208
|
+
* @param path the property's `deepBind` path — a single key for a top-level
|
|
209
|
+
* member, a longer path for a member of a nested config
|
|
210
|
+
*/
|
|
211
|
+
export function describe(base, ...path) {
|
|
212
|
+
return compose(base, rowsByPath.get(pathKey(path)));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Composes a description for a member of a record edited by a SECONDARY
|
|
216
|
+
* editor — a preset filter within `workflowPickerConfig.presetFilters`, say.
|
|
217
|
+
*
|
|
218
|
+
* @param base authored, model-facing description of the property
|
|
219
|
+
* @param opensAt the `deepBind` path of the row that opens the editor
|
|
220
|
+
* @param path the property's path within the record being edited
|
|
221
|
+
*/
|
|
222
|
+
export function describeIn(base, opensAt, ...path) {
|
|
223
|
+
return compose(base, secondaryRowsByParent.get(pathKey(opensAt))?.get(pathKey(path)));
|
|
224
|
+
}
|
|
225
|
+
/** Shared body of {@link describe} and {@link describeIn}. */
|
|
226
|
+
function compose(base, rows) {
|
|
227
|
+
const parts = [terminate(base)];
|
|
228
|
+
const hint = hintIn(rows);
|
|
229
|
+
// Skip a hint the authored text already covers, so composition cannot
|
|
230
|
+
// produce the same guidance twice in one description.
|
|
231
|
+
if (hint && !base.toLowerCase().includes(hint.toLowerCase())) {
|
|
232
|
+
parts.push(terminate(hint));
|
|
233
|
+
}
|
|
234
|
+
const applicability = applicabilityIn(rows);
|
|
235
|
+
if (applicability)
|
|
236
|
+
parts.push(applicability);
|
|
237
|
+
return parts.join(' ');
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Numeric bounds the editor enforces on a property, ready to spread into a
|
|
241
|
+
* Skillet numeric node so the schema constrains what the settings panel does.
|
|
242
|
+
*/
|
|
243
|
+
export function rangeFor(...path) {
|
|
244
|
+
const rows = rowsByPath.get(pathKey(path));
|
|
245
|
+
const row = rows?.find((r) => r.min !== undefined || r.max !== undefined);
|
|
246
|
+
if (!row)
|
|
247
|
+
return undefined;
|
|
248
|
+
const range = {};
|
|
249
|
+
if (typeof row.min === 'number')
|
|
250
|
+
range.minimum = row.min;
|
|
251
|
+
if (typeof row.max === 'number')
|
|
252
|
+
range.maximum = row.max;
|
|
253
|
+
return Object.keys(range).length ? range : undefined;
|
|
254
|
+
}
|
|
255
|
+
/** Property paths the editor config carries guidance for. Used by tests. */
|
|
256
|
+
export const GUIDED_PATHS = [...rowsByPath.keys()];
|
|
257
|
+
/**
|
|
258
|
+
* The top-level members the settings panel edits on EVERY element.
|
|
259
|
+
*
|
|
260
|
+
* A row named `SpecialElementKeys.Default` is not looked up in an element's
|
|
261
|
+
* `properties` at all: it is rendered for every element, gated only by its own
|
|
262
|
+
* `additionalTest` and `disabled` tests. So the members those rows edit are
|
|
263
|
+
* part of every element's surface in the builder while appearing in no
|
|
264
|
+
* element's property list — `readonly`, `onlySetTempErrorOnTouch` and
|
|
265
|
+
* `tourContent` among them. Read here so variant assembly can offer what the
|
|
266
|
+
* builder offers, from the same source the builder reads.
|
|
267
|
+
*
|
|
268
|
+
* Only ungated rows qualify. A gated row is conditional on a VALUE (`min`
|
|
269
|
+
* needs a numeric `type`; `calculatedFieldRules` needs the flag), which is not
|
|
270
|
+
* "always shown" and is handled elsewhere. Nested bindings (`script.onChange`,
|
|
271
|
+
* `matOptions.fetch.value.source`) are members of a nested object, not of the
|
|
272
|
+
* column, and are left to the modules that own those objects.
|
|
273
|
+
*/
|
|
274
|
+
export const ALWAYS_SHOWN_MEMBERS = [
|
|
275
|
+
...new Set(collectRows()
|
|
276
|
+
.filter((row) => row.name === SpecialElementKeys.Default &&
|
|
277
|
+
row.deepBind?.length === 1 &&
|
|
278
|
+
!row.additionalTest?.length &&
|
|
279
|
+
!row.disabled?.length)
|
|
280
|
+
.map((row) => row.deepBind[0])),
|
|
281
|
+
];
|
|
282
|
+
/**
|
|
283
|
+
* Secondary-editor paths, as `openingPath -> propertyPath`. Used by tests to
|
|
284
|
+
* assert the nested editors are actually being reached: before these were
|
|
285
|
+
* collected, every hint inside a record-list editor was invisible here, and
|
|
286
|
+
* nothing failed to say so.
|
|
287
|
+
*/
|
|
288
|
+
export const SECONDARY_GUIDED_PATHS = new Map([...secondaryRowsByParent].map(([parent, inner]) => [
|
|
289
|
+
parent,
|
|
290
|
+
[...inner.keys()],
|
|
291
|
+
]));
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import type { CalculatedFieldRules } from '../../interfaces/formInput/calculatedFieldRules.js';
|
|
3
|
+
import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `calculatedFieldRules` member: a value computed from other fields rather
|
|
6
|
+
* than typed by the user.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this is a draft
|
|
9
|
+
*
|
|
10
|
+
* `calculationVariableInterface` requires both `id` and `inputId` on every
|
|
11
|
+
* variable. Those are builder-assigned: `inputId` is the `id` of another input
|
|
12
|
+
* in the form, stamped when that input was created, and a model has no way to
|
|
13
|
+
* know it — the form it is authoring does not exist yet, so the ids do not
|
|
14
|
+
* either. Asked for one it will invent a plausible string, and the formula then
|
|
15
|
+
* references a field that is not there. The failure is quiet: the form saves,
|
|
16
|
+
* renders, and computes nothing.
|
|
17
|
+
*
|
|
18
|
+
* So the model binds by `formControlName`, which it genuinely does author, and
|
|
19
|
+
* the application resolves each one to that input's `id` on expansion. Both
|
|
20
|
+
* names are already on the interface — `formControlName` and
|
|
21
|
+
* `parentInputFormControl` sit alongside `inputId` and `parentInputId` — so the
|
|
22
|
+
* draft is not inventing a vocabulary. It fills in the half of each pair a
|
|
23
|
+
* model can actually know.
|
|
24
|
+
*
|
|
25
|
+
* ## The two ways a variable binds
|
|
26
|
+
*
|
|
27
|
+
* A variable reads EITHER one field's value directly, OR an aggregate over one
|
|
28
|
+
* column of a repeatable list. The second needs a set of members the first has
|
|
29
|
+
* no use for — `function`, `parentInputFormControl`, `applyFunctionToCol` — and
|
|
30
|
+
* Joi accepts them all on the same flat object, so nothing in the stored shape
|
|
31
|
+
* says which belong together. Offered flat, a model fills in
|
|
32
|
+
* `applyFunctionToCol` for a plain field and `function` for something with
|
|
33
|
+
* nothing to aggregate.
|
|
34
|
+
*
|
|
35
|
+
* They are therefore two branches of an `anyOf`, discriminated by an explicit
|
|
36
|
+
* `bindingType`. That key is NOT a member of `calculationVariableInterface`;
|
|
37
|
+
* like `endpointId` in `members/mat-options.ts` it exists only in the draft and
|
|
38
|
+
* is dropped on expansion. It earns the extra expansion step because an
|
|
39
|
+
* undiscriminated union of two objects sharing three of their keys is exactly
|
|
40
|
+
* the shape a model picks wrongly.
|
|
41
|
+
*
|
|
42
|
+
* The grouping is not invented here: `schemas/customValidationSchema.ts`
|
|
43
|
+
* annotates the same `parentInputId` + `function` pair as "Multiple-input
|
|
44
|
+
* (list) bindings", which is the distinction drawn.
|
|
45
|
+
*/
|
|
46
|
+
/** `roundingMode` is a string union with no runtime enum to derive from. */
|
|
47
|
+
export declare const ROUNDING_MODE_VALUES: readonly ["FLOOR", "CEIL", "ROUND"];
|
|
48
|
+
interface VariableCommonDraft {
|
|
49
|
+
/** The token this variable is referenced by inside `formula`. */
|
|
50
|
+
variable: string;
|
|
51
|
+
label: string;
|
|
52
|
+
formControlName: string;
|
|
53
|
+
}
|
|
54
|
+
export interface FieldVariableDraft extends VariableCommonDraft {
|
|
55
|
+
bindingType: 'field';
|
|
56
|
+
}
|
|
57
|
+
export interface ListVariableDraft extends VariableCommonDraft {
|
|
58
|
+
bindingType: 'listAggregate';
|
|
59
|
+
parentInputFormControl: string;
|
|
60
|
+
function: CalculationFunctions;
|
|
61
|
+
applyFunctionToCol: string;
|
|
62
|
+
applyFunctionToLabel: string | null;
|
|
63
|
+
filterValuesByCol: string | null;
|
|
64
|
+
filterValuesByThisColLabel: string | null;
|
|
65
|
+
tableCell: boolean | null;
|
|
66
|
+
}
|
|
67
|
+
export type CalculationVariableDraft = FieldVariableDraft | ListVariableDraft;
|
|
68
|
+
export interface CalculatedFieldRulesDraft {
|
|
69
|
+
formula: string;
|
|
70
|
+
variables: CalculationVariableDraft[];
|
|
71
|
+
decimalPlaces: number | null;
|
|
72
|
+
roundingMode: (typeof ROUNDING_MODE_VALUES)[number] | null;
|
|
73
|
+
getFormulaFromAFormInput: boolean | null;
|
|
74
|
+
formControlWithFormula: string | null;
|
|
75
|
+
}
|
|
76
|
+
export declare const calculationVariableDraftSchema: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
77
|
+
bindingType: "field";
|
|
78
|
+
variable: string;
|
|
79
|
+
label: string;
|
|
80
|
+
formControlName: string;
|
|
81
|
+
} | {
|
|
82
|
+
bindingType: "listAggregate";
|
|
83
|
+
variable: string;
|
|
84
|
+
label: string;
|
|
85
|
+
formControlName: string;
|
|
86
|
+
parentInputFormControl: string;
|
|
87
|
+
function: CalculationFunctions;
|
|
88
|
+
applyFunctionToCol: string;
|
|
89
|
+
applyFunctionToLabel: string | null;
|
|
90
|
+
filterValuesByCol: string | null;
|
|
91
|
+
filterValuesByThisColLabel: string | null;
|
|
92
|
+
tableCell: boolean | null;
|
|
93
|
+
}>;
|
|
94
|
+
/**
|
|
95
|
+
* The editor's applicability text for this row is deliberately NOT composed in.
|
|
96
|
+
*
|
|
97
|
+
* `describe(..., 'calculatedFieldRules')` resolves to "Only applies when
|
|
98
|
+
* isCalculatedField is true", which is correct in the settings panel and
|
|
99
|
+
* incoherent here: `variants.ts` encodes that rule as the shape of the union
|
|
100
|
+
* and drops `isCalculatedField` from the schema entirely, so the sentence would
|
|
101
|
+
* point a model at a member it is never shown — an invitation to invent one.
|
|
102
|
+
*
|
|
103
|
+
* This is the same judgement `internal/editor-guidance.ts` records for `label`
|
|
104
|
+
* and `options`: derived text earns its place by telling a model something it
|
|
105
|
+
* does not already have, and a rule the structure already makes unbreakable is
|
|
106
|
+
* not that.
|
|
107
|
+
*/
|
|
108
|
+
export declare const calculatedFieldRulesDraftSchema: s.ObjectType<{
|
|
109
|
+
formula: s.StringType;
|
|
110
|
+
variables: s.ArrayType<import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
|
|
111
|
+
bindingType: "field";
|
|
112
|
+
variable: string;
|
|
113
|
+
label: string;
|
|
114
|
+
formControlName: string;
|
|
115
|
+
} | {
|
|
116
|
+
bindingType: "listAggregate";
|
|
117
|
+
variable: string;
|
|
118
|
+
label: string;
|
|
119
|
+
formControlName: string;
|
|
120
|
+
parentInputFormControl: string;
|
|
121
|
+
function: CalculationFunctions;
|
|
122
|
+
applyFunctionToCol: string;
|
|
123
|
+
applyFunctionToLabel: string | null;
|
|
124
|
+
filterValuesByCol: string | null;
|
|
125
|
+
filterValuesByThisColLabel: string | null;
|
|
126
|
+
tableCell: boolean | null;
|
|
127
|
+
}>>;
|
|
128
|
+
decimalPlaces: import("@hashbrownai/core/src/schema/base").SchemaForUnion<number | null>;
|
|
129
|
+
roundingMode: import("@hashbrownai/core/src/schema/base").SchemaForUnion<"FLOOR" | "CEIL" | "ROUND" | null>;
|
|
130
|
+
getFormulaFromAFormInput: import("@hashbrownai/core/src/schema/base").SchemaForUnion<boolean | null>;
|
|
131
|
+
formControlWithFormula: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | null>;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* The draft is checked against its declared shape, not against
|
|
135
|
+
* `CalculatedFieldRules`: it deliberately differs, carrying `bindingType` and
|
|
136
|
+
* omitting the two identifiers the application supplies. What the compiler can
|
|
137
|
+
* still hold is that the node produces the draft it claims to.
|
|
138
|
+
*/
|
|
139
|
+
export type _NoCalculatedFieldDraftDrift = [
|
|
140
|
+
s.Infer<typeof calculatedFieldRulesDraftSchema>
|
|
141
|
+
] extends [CalculatedFieldRulesDraft] ? never : ['calculatedFieldRulesDraftSchema drifted from CalculatedFieldRulesDraft'];
|
|
142
|
+
/**
|
|
143
|
+
* The parts of the draft stored verbatim stay pinned to the real interface, so
|
|
144
|
+
* retyping `roundingMode` upstream surfaces here rather than in a form that
|
|
145
|
+
* silently rounds the wrong way.
|
|
146
|
+
*/
|
|
147
|
+
export type _RoundingModeMatchesInterface = [
|
|
148
|
+
(typeof ROUNDING_MODE_VALUES)[number]
|
|
149
|
+
] extends [NonNullable<CalculatedFieldRules['roundingMode']>] ? never : ['ROUNDING_MODE_VALUES drifted from CalculatedFieldRules'];
|
|
150
|
+
/**
|
|
151
|
+
* Members of a generated `calculatedFieldRules` the application must supply
|
|
152
|
+
* before Joi will accept it.
|
|
153
|
+
*/
|
|
154
|
+
export declare const CALCULATED_FIELD_DRAFT_COMPLETION: readonly ["calculatedFieldRules.variables[].id", "calculatedFieldRules.variables[].inputId (resolved from formControlName)", "calculatedFieldRules.variables[].parentInputId (resolved from parentInputFormControl)", "calculatedFieldRules.variables[].bindingType (draft-only; dropped on expansion)"];
|
|
155
|
+
export {};
|