ngx-t-forms-types 0.0.30 → 0.0.33
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/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,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 {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
|
|
3
|
+
import { enumValues, exhaustive, optional } from '../internal/coupling.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 const ROUNDING_MODE_VALUES = exhaustive()(['FLOOR', 'CEIL', 'ROUND']);
|
|
48
|
+
// --- schema -----------------------------------------------------------------
|
|
49
|
+
const variableToken = s.string('The name this variable is referenced by inside the formula. Keep it short and lower case, and spell it identically in the formula.', { pattern: '^[a-zA-Z][a-zA-Z0-9_]*$' });
|
|
50
|
+
const variableLabel = s.string('A human-readable name for this variable, shown when the formula is explained back to an administrator.');
|
|
51
|
+
const fieldVariableSchema = s.object("Reads one field's value directly.", {
|
|
52
|
+
bindingType: s.literal('field'),
|
|
53
|
+
variable: variableToken,
|
|
54
|
+
label: variableLabel,
|
|
55
|
+
formControlName: s.string('The formControlName of the field on this form supplying the value. It must be a field that actually exists in the form being authored.'),
|
|
56
|
+
});
|
|
57
|
+
const listVariableSchema = s.object('Aggregates one column across every row of a repeatable item list.', {
|
|
58
|
+
bindingType: s.literal('listAggregate'),
|
|
59
|
+
variable: variableToken,
|
|
60
|
+
label: variableLabel,
|
|
61
|
+
formControlName: s.string('The formControlName of the column being aggregated, as it is named inside the list.'),
|
|
62
|
+
parentInputFormControl: s.string('The formControlName of the item-list field whose rows are aggregated. It must be a multipleInput field in this form.'),
|
|
63
|
+
function: s.enumeration('How the column is reduced to a single value across the rows.', enumValues(CalculationFunctions)),
|
|
64
|
+
applyFunctionToCol: s.string('The column within each row the function is applied to.'),
|
|
65
|
+
applyFunctionToLabel: optional(s.string('Display label for the aggregated column.')),
|
|
66
|
+
filterValuesByCol: optional(s.string('Restricts the aggregate to rows matching a value in this column. Leave unset to aggregate every row.')),
|
|
67
|
+
filterValuesByThisColLabel: optional(s.string('Display label for the column the rows are filtered by.')),
|
|
68
|
+
tableCell: optional(s.boolean('Whether this variable resolves to a single table cell.')),
|
|
69
|
+
});
|
|
70
|
+
export const calculationVariableDraftSchema = s.anyOf([
|
|
71
|
+
fieldVariableSchema,
|
|
72
|
+
listVariableSchema,
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
75
|
+
* The editor's applicability text for this row is deliberately NOT composed in.
|
|
76
|
+
*
|
|
77
|
+
* `describe(..., 'calculatedFieldRules')` resolves to "Only applies when
|
|
78
|
+
* isCalculatedField is true", which is correct in the settings panel and
|
|
79
|
+
* incoherent here: `variants.ts` encodes that rule as the shape of the union
|
|
80
|
+
* and drops `isCalculatedField` from the schema entirely, so the sentence would
|
|
81
|
+
* point a model at a member it is never shown — an invitation to invent one.
|
|
82
|
+
*
|
|
83
|
+
* This is the same judgement `internal/editor-guidance.ts` records for `label`
|
|
84
|
+
* and `options`: derived text earns its place by telling a model something it
|
|
85
|
+
* does not already have, and a rule the structure already makes unbreakable is
|
|
86
|
+
* not that.
|
|
87
|
+
*/
|
|
88
|
+
export const calculatedFieldRulesDraftSchema = s.object('How this field computes its value from other fields on the form, so the user never types a figure the system can work out: a line total, VAT, an age. Whenever a total, average, minimum, maximum or count over an item list is used anywhere else — a decision gate, a validator, another formula, a list column, the next person — it must be a calculated field like this one on the form itself, aggregating the list; the roll-up shown beneath the list is for the user only.', {
|
|
89
|
+
formula: s.string('The arithmetic expression, written using the variable names declared below and nothing else. Every name in the formula must be declared as a variable, and every declared variable should appear in the formula.'),
|
|
90
|
+
variables: s.array('The fields the formula reads, one entry per variable name used in it.', calculationVariableDraftSchema, { minItems: 1 }),
|
|
91
|
+
decimalPlaces: optional(s.integer('How many decimal places the result is rounded to: 2 for money, 0 for a count.', { minimum: 0 })),
|
|
92
|
+
roundingMode: optional(s.enumeration("How the result is rounded at that precision: 'ROUND' to the nearest, which is right for money; 'FLOOR' or 'CEIL' only when the business rule says round down or up.", [...ROUNDING_MODE_VALUES])),
|
|
93
|
+
getFormulaFromAFormInput: optional(s.boolean('Whether the formula is read from another field on the form at runtime instead of being fixed here. Leave unset unless the form genuinely lets a user supply the formula.')),
|
|
94
|
+
formControlWithFormula: optional(s.string('The formControlName of the field supplying the formula. Only meaningful when the formula is read from a form input.')),
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Members of a generated `calculatedFieldRules` the application must supply
|
|
98
|
+
* before Joi will accept it.
|
|
99
|
+
*/
|
|
100
|
+
export const CALCULATED_FIELD_DRAFT_COMPLETION = [
|
|
101
|
+
'calculatedFieldRules.variables[].id',
|
|
102
|
+
'calculatedFieldRules.variables[].inputId (resolved from formControlName)',
|
|
103
|
+
'calculatedFieldRules.variables[].parentInputId (resolved from parentInputFormControl)',
|
|
104
|
+
'calculatedFieldRules.variables[].bindingType (draft-only; dropped on expansion)',
|
|
105
|
+
];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import type { ConditionalInputRule } from '../../interfaces/formInput/ConditionalInputRule.js';
|
|
3
|
+
import { type ObservedInputDraft } from './validators.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `conditionalInputConfig` member: when this control is shown at all.
|
|
6
|
+
*
|
|
7
|
+
* ## Only half of the interface is offered
|
|
8
|
+
*
|
|
9
|
+
* `ConditionalInputRule` carries two shapes at once. The `expression` half is
|
|
10
|
+
* read by the engine; `formControlName`, `label`, `testType` and `valueMatch`
|
|
11
|
+
* are a legacy descriptive half, marked `@deprecated` on every member and never
|
|
12
|
+
* read by the runtime. Only the expression half is modelled, for the reason
|
|
13
|
+
* `DEPRECATED_INPUT_MEMBERS` gives in `input-members.ts`: a deprecated property
|
|
14
|
+
* left in a generation schema comes back on every form the model writes, and
|
|
15
|
+
* these four would come back describing a rule that does nothing.
|
|
16
|
+
*
|
|
17
|
+
* The consequence is worth being explicit about, because it is the one case
|
|
18
|
+
* here where narrowing changes behaviour rather than just shape: a rule with no
|
|
19
|
+
* `expression` is inert. Since the deprecated members are the only thing a rule
|
|
20
|
+
* could otherwise carry, every rule this schema produces is an active one.
|
|
21
|
+
*
|
|
22
|
+
* ## Polarity and combination come from the interface
|
|
23
|
+
*
|
|
24
|
+
* `ConditionalInputRule`'s own documentation states two things a model cannot
|
|
25
|
+
* infer from the types and gets wrong by default:
|
|
26
|
+
*
|
|
27
|
+
* - `true` means the input is REVEALED — the opposite polarity to a custom
|
|
28
|
+
* validator's expression, where `true` means invalid. The two share a DSL and
|
|
29
|
+
* are trivially confused.
|
|
30
|
+
* - Multiple rules combine with AND, and an empty list means always visible.
|
|
31
|
+
*
|
|
32
|
+
* Both are restated in the descriptions rather than paraphrased loosely, on the
|
|
33
|
+
* same principle that `internal/editor-guidance.ts` harvests the builder's
|
|
34
|
+
* hints instead of rewriting them.
|
|
35
|
+
*
|
|
36
|
+
* ## Why this is a draft
|
|
37
|
+
*
|
|
38
|
+
* `id` is a builder-assigned rule identifier, and the observed inputs bind by
|
|
39
|
+
* `inputId`. Both are handled exactly as in `members/validators.ts` — the model
|
|
40
|
+
* binds by `formControlName` and the application resolves it.
|
|
41
|
+
*/
|
|
42
|
+
export interface ConditionalRuleDraft {
|
|
43
|
+
expression: string;
|
|
44
|
+
inputsObservedForChanges: ObservedInputDraft[];
|
|
45
|
+
}
|
|
46
|
+
export declare const conditionalInputConfigDraftSchema: s.ArrayType<s.ObjectType<{
|
|
47
|
+
expression: s.StringType;
|
|
48
|
+
inputsObservedForChanges: s.ArrayType<s.ObjectType<{
|
|
49
|
+
formControlName: s.StringType;
|
|
50
|
+
variable: s.StringType;
|
|
51
|
+
parentInputFormControl: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | null>;
|
|
52
|
+
function: import("@hashbrownai/core/src/schema/base").SchemaForUnion<import("../../index.js").CalculationFunctions | null>;
|
|
53
|
+
}>>;
|
|
54
|
+
}>>;
|
|
55
|
+
/** The node produces the draft it declares. */
|
|
56
|
+
export type _NoConditionalDraftDrift = [
|
|
57
|
+
s.Infer<typeof conditionalInputConfigDraftSchema>
|
|
58
|
+
] extends [ConditionalRuleDraft[]] ? never : ['conditionalInputConfigDraftSchema drifted from ConditionalRuleDraft[]'];
|
|
59
|
+
/**
|
|
60
|
+
* `expression` is carried through expansion untouched, so it stays pinned to
|
|
61
|
+
* the stored interface.
|
|
62
|
+
*/
|
|
63
|
+
export type _ExpressionMatchesStoredShape = [
|
|
64
|
+
ConditionalRuleDraft['expression']
|
|
65
|
+
] extends [NonNullable<ConditionalInputRule['expression']>] ? never : ['ConditionalRuleDraft.expression drifted from ConditionalInputRule'];
|
|
66
|
+
/**
|
|
67
|
+
* Members of a generated rule the application must supply.
|
|
68
|
+
*/
|
|
69
|
+
export declare const CONDITIONAL_DRAFT_COMPLETION: readonly ["conditionalInputConfig[].id", "conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)", "conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)"];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { describe } from '../internal/editor-guidance.js';
|
|
3
|
+
import { observedInputSchema } from './validators.js';
|
|
4
|
+
const conditionalRuleSchema = s.object('One rule deciding whether this control is shown.', {
|
|
5
|
+
expression: s.string("An expression in the form engine's comparison language — NOT JavaScript — that is TRUE when this control should be SHOWN. Like a validator expression, it is true when the condition it names holds — here that condition is the reason to reveal the field. The grammar is a comparison `variable OP value`, with OP one of ===, !==, ==, !=, >, <, >=, <=, or a keyword operator includes, in, startsWith, endsWith or matches; a bare variable tests truthiness; ! negates; comparisons combine with && and || and group with parentheses; a variable may be a dotted path (reason.length > 15). There is no arithmetic, no function or method call and no ternary: to compare against a computed figure, make the figure a calculated field and compare against that field. A date is compared as an ISO string. Reference other fields only by the variable names declared below, compare against the stored option value rather than its label (urgency === 'urgent'), and put quotes around every string literal. For 'either of two conditions' write one rule with || inside it."),
|
|
6
|
+
inputsObservedForChanges: s.array('The fields the expression reads, one entry per variable name used in it.', observedInputSchema, { minItems: 1 }),
|
|
7
|
+
});
|
|
8
|
+
export const conditionalInputConfigDraftSchema = s.array(describe('Rules controlling when this control is visible: a follow-up question, an exception, a branch, a rejection reason when a decision toggle is off. Every rule must hold for it to be shown; leave empty to show it always. Never hide a field the engine reads.', 'conditionalInputConfig'), conditionalRuleSchema);
|
|
9
|
+
/**
|
|
10
|
+
* Members of a generated rule the application must supply.
|
|
11
|
+
*/
|
|
12
|
+
export const CONDITIONAL_DRAFT_COMPLETION = [
|
|
13
|
+
'conditionalInputConfig[].id',
|
|
14
|
+
'conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)',
|
|
15
|
+
'conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)',
|
|
16
|
+
];
|