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,154 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import type { IBasicFormInput } from '../../interfaces/formInput/BasicFormInputInterface.js';
|
|
3
|
+
import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
|
|
4
|
+
import type { IMultipleInputCal } from '../../interfaces/formInput/MultipleInterface.js';
|
|
5
|
+
import { AssertNever } from '../internal/coupling.js';
|
|
6
|
+
/**
|
|
7
|
+
* Three members that carry a value rather than describe a control: the value an
|
|
8
|
+
* input starts with, the script hook it fires, and the roll-ups a repeatable
|
|
9
|
+
* group computes over its own rows.
|
|
10
|
+
*
|
|
11
|
+
* They are grouped because they share one problem. Each is declared in the
|
|
12
|
+
* interfaces with a type wide enough to hold anything the running application
|
|
13
|
+
* might put there — a `File`, an open `Record`, a builder-assigned id — and in
|
|
14
|
+
* every case the widest part is exactly the part a model cannot produce. So
|
|
15
|
+
* each node here is narrower than its member, and one of them is a draft.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The `value` member: what the control holds before the user touches it.
|
|
19
|
+
*
|
|
20
|
+
* ## Deliberate narrowing
|
|
21
|
+
*
|
|
22
|
+
* The interface declares
|
|
23
|
+
* `number | string | boolean | Date | File | {[key: string]: any} | Array<{[key: string]: any}>`,
|
|
24
|
+
* and the Joi schema accepts the same set (with `''` explicitly allowed on the
|
|
25
|
+
* string branch). This node offers three of those seven branches:
|
|
26
|
+
*
|
|
27
|
+
* - `Date` — a model emits JSON, and the best JSON carries is an ISO string. A
|
|
28
|
+
* string is not assignable to `Date`, so a "date default" authored here would
|
|
29
|
+
* be a mistyped value that only fails somewhere far from this schema. A real
|
|
30
|
+
* date default needs the draft-and-expand treatment, not a quiet coercion,
|
|
31
|
+
* and until something does that it is better refused.
|
|
32
|
+
* - `File` — a browser runtime object. It has no JSON representation at all; it
|
|
33
|
+
* exists only once a user has picked something off their disk.
|
|
34
|
+
* - `{[key: string]: any}` and `Array<{[key: string]: any}>` — index
|
|
35
|
+
* signatures. Skillet has no record node, so the shape cannot be expressed,
|
|
36
|
+
* and a model asked for an open object has no basis for inventing keys. What
|
|
37
|
+
* it would produce is plausible-looking structure that nothing reads.
|
|
38
|
+
*
|
|
39
|
+
* The three that remain — number, string, boolean — ARE members of the declared
|
|
40
|
+
* union, so the node stays assignable to the member: narrower than the
|
|
41
|
+
* interface, never wider. That is the direction `internal/coupling.ts` fails
|
|
42
|
+
* on. A schema may refuse a value the interface tolerates; it may never admit
|
|
43
|
+
* one the interface cannot hold, because that is the drift that produces a
|
|
44
|
+
* value the rest of the codebase has nowhere to put.
|
|
45
|
+
*
|
|
46
|
+
* ## Why this one is a union at all
|
|
47
|
+
*
|
|
48
|
+
* Most members have a single type. This one genuinely does not: the same
|
|
49
|
+
* property holds `0` on a numeric input, `'Pretoria'` on a text input and
|
|
50
|
+
* `false` on a toggle, and which is correct is decided entirely by the element
|
|
51
|
+
* the model has already chosen. So the branches are described by the CONTROL
|
|
52
|
+
* each belongs to rather than by its JavaScript type — a model choosing between
|
|
53
|
+
* "a number" and "a string" is choosing on the wrong axis, and will reach for a
|
|
54
|
+
* string on a numeric field because the label sounded textual.
|
|
55
|
+
*
|
|
56
|
+
* A union node has no description slot of its own, so the rule that governs all
|
|
57
|
+
* three branches — agree with `dataType` — is stated on each of them.
|
|
58
|
+
*/
|
|
59
|
+
export declare const valueSchema: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | number | boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* The `script` member: application behaviour attached to the control.
|
|
62
|
+
*
|
|
63
|
+
* Declared as `Record<string, unknown>` and validated as a bare `Joi.object()`,
|
|
64
|
+
* so at the type level it is an open map and nothing constrains its keys.
|
|
65
|
+
* Skillet has no record node and an open map is not expressible in it — but
|
|
66
|
+
* modelling this as "some object" would be worse than not modelling it at all,
|
|
67
|
+
* because a model handed an open slot fills it. Every key it invented would be
|
|
68
|
+
* accepted by the interface, accepted by Joi, stored on the input, and then
|
|
69
|
+
* ignored at runtime by an application that only reads keys it already knows.
|
|
70
|
+
*
|
|
71
|
+
* The form builder answers what those keys are: its editor declares exactly one
|
|
72
|
+
* row for this member, at the `deepBind` path `['script', 'onChange']`. That is
|
|
73
|
+
* the entire surface a human author is offered, so it is the entire surface
|
|
74
|
+
* offered here. A closed object carrying that single key is still assignable to
|
|
75
|
+
* `Record<string, unknown>`, which keeps the coupling honest while leaving the
|
|
76
|
+
* model no room to invent hooks that do not exist.
|
|
77
|
+
*/
|
|
78
|
+
export declare const scriptSchema: s.ObjectType<{
|
|
79
|
+
onChange: s.StringType;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* The `calculateListFunctions` member of a repeatable group: the roll-ups shown
|
|
83
|
+
* across its rows.
|
|
84
|
+
*
|
|
85
|
+
* ## Why this produces a draft
|
|
86
|
+
*
|
|
87
|
+
* `IMultipleInputCal` is
|
|
88
|
+
* `{ id: string; inputId: string; func: CalculationFunctions }`, and Joi
|
|
89
|
+
* requires all three. Two of the three are identifiers the builder assigns:
|
|
90
|
+
* `id` names the roll-up itself, and `inputId` is the stored `id` of the column
|
|
91
|
+
* being aggregated — a value that does not exist until that column has been
|
|
92
|
+
* created and persisted. A model asked for either produces a convincing uuid
|
|
93
|
+
* pointing at nothing, and the failure is silent: the roll-up renders, resolves
|
|
94
|
+
* no column, and totals nothing.
|
|
95
|
+
*
|
|
96
|
+
* What the model does author, and authors reliably, is `formControlName` — it
|
|
97
|
+
* chose the name itself, a few properties earlier in the same form. So the
|
|
98
|
+
* draft names the target column and the application resolves that name to an
|
|
99
|
+
* `inputId` and stamps an `id`, the same trade already made for `inputSourceId`
|
|
100
|
+
* on a local `matOptions` source, where the model names a field and the
|
|
101
|
+
* application turns the name into a reference.
|
|
102
|
+
*
|
|
103
|
+
* The resolution step is the caller's and there is no way around it. What stays
|
|
104
|
+
* checked here is that the draft keeps the shape it declares.
|
|
105
|
+
*/
|
|
106
|
+
export interface CalculateListFunctionDraft {
|
|
107
|
+
/**
|
|
108
|
+
* The `formControlName` of the column inside this group to aggregate. It must
|
|
109
|
+
* name a field that actually exists in the group being authored.
|
|
110
|
+
*/
|
|
111
|
+
formControlName: string;
|
|
112
|
+
/** Which aggregate to compute over that column. */
|
|
113
|
+
func: CalculationFunctions;
|
|
114
|
+
}
|
|
115
|
+
/** What the model produces in place of `Array<IMultipleInputCal>`. */
|
|
116
|
+
export type CalculateListFunctionsDraft = CalculateListFunctionDraft[];
|
|
117
|
+
export declare const calculateListFunctionsSchema: s.ArrayType<s.ObjectType<{
|
|
118
|
+
formControlName: s.StringType;
|
|
119
|
+
func: s.EnumType<CalculationFunctions[]>;
|
|
120
|
+
}>>;
|
|
121
|
+
/**
|
|
122
|
+
* The `value` node still produces something its member can hold. Narrowing is
|
|
123
|
+
* the point of that node, and this is what proves the narrowing stayed INSIDE
|
|
124
|
+
* the declared union rather than drifting out of it.
|
|
125
|
+
*/
|
|
126
|
+
export type _NoValueMemberDrift = AssertNever<[
|
|
127
|
+
s.Infer<typeof valueSchema>
|
|
128
|
+
] extends [NonNullable<IBasicFormInput['value']>] ? never : 'value'>;
|
|
129
|
+
/** The closed `script` object is still assignable to the open record. */
|
|
130
|
+
export type _NoScriptMemberDrift = AssertNever<[
|
|
131
|
+
s.Infer<typeof scriptSchema>
|
|
132
|
+
] extends [NonNullable<IBasicFormInput['script']>] ? never : 'script'>;
|
|
133
|
+
/**
|
|
134
|
+
* The roll-up node produces the draft it declares. The expansion from draft to
|
|
135
|
+
* `IMultipleInputCal` is the caller's, but the draft itself stays checked.
|
|
136
|
+
*/
|
|
137
|
+
export type _NoCalculateListFunctionsDraftDrift = AssertNever<[
|
|
138
|
+
s.Infer<typeof calculateListFunctionsSchema>
|
|
139
|
+
] extends [
|
|
140
|
+
CalculateListFunctionsDraft
|
|
141
|
+
] ? never : 'calculateListFunctions'>;
|
|
142
|
+
/**
|
|
143
|
+
* Every member of the stored `IMultipleInputCal` is accounted for: `func` is
|
|
144
|
+
* authored, `id` and `inputId` are assigned by the application. A fourth field
|
|
145
|
+
* added to that interface fails here, which forces the question of whether a
|
|
146
|
+
* model can author it — rather than letting the draft quietly stop covering the
|
|
147
|
+
* shape it is expanded into.
|
|
148
|
+
*/
|
|
149
|
+
export type _CalculateListFunctionShapeAccountedFor = AssertNever<Exclude<keyof IMultipleInputCal, 'id' | 'inputId' | 'func'>>;
|
|
150
|
+
/**
|
|
151
|
+
* Members of a generated `calculateListFunctions` the application must supply.
|
|
152
|
+
* Both are required by Joi, so an unexpanded draft fails validation.
|
|
153
|
+
*/
|
|
154
|
+
export declare const CALCULATE_LIST_DRAFT_COMPLETION: readonly ["calculateListFunctions[].id", "calculateListFunctions[].inputId (resolved from formControlName)"];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
|
|
3
|
+
import { enumValues } from '../internal/coupling.js';
|
|
4
|
+
import { describe } from '../internal/editor-guidance.js';
|
|
5
|
+
/**
|
|
6
|
+
* Three members that carry a value rather than describe a control: the value an
|
|
7
|
+
* input starts with, the script hook it fires, and the roll-ups a repeatable
|
|
8
|
+
* group computes over its own rows.
|
|
9
|
+
*
|
|
10
|
+
* They are grouped because they share one problem. Each is declared in the
|
|
11
|
+
* interfaces with a type wide enough to hold anything the running application
|
|
12
|
+
* might put there — a `File`, an open `Record`, a builder-assigned id — and in
|
|
13
|
+
* every case the widest part is exactly the part a model cannot produce. So
|
|
14
|
+
* each node here is narrower than its member, and one of them is a draft.
|
|
15
|
+
*/
|
|
16
|
+
// --- value ------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* The `value` member: what the control holds before the user touches it.
|
|
19
|
+
*
|
|
20
|
+
* ## Deliberate narrowing
|
|
21
|
+
*
|
|
22
|
+
* The interface declares
|
|
23
|
+
* `number | string | boolean | Date | File | {[key: string]: any} | Array<{[key: string]: any}>`,
|
|
24
|
+
* and the Joi schema accepts the same set (with `''` explicitly allowed on the
|
|
25
|
+
* string branch). This node offers three of those seven branches:
|
|
26
|
+
*
|
|
27
|
+
* - `Date` — a model emits JSON, and the best JSON carries is an ISO string. A
|
|
28
|
+
* string is not assignable to `Date`, so a "date default" authored here would
|
|
29
|
+
* be a mistyped value that only fails somewhere far from this schema. A real
|
|
30
|
+
* date default needs the draft-and-expand treatment, not a quiet coercion,
|
|
31
|
+
* and until something does that it is better refused.
|
|
32
|
+
* - `File` — a browser runtime object. It has no JSON representation at all; it
|
|
33
|
+
* exists only once a user has picked something off their disk.
|
|
34
|
+
* - `{[key: string]: any}` and `Array<{[key: string]: any}>` — index
|
|
35
|
+
* signatures. Skillet has no record node, so the shape cannot be expressed,
|
|
36
|
+
* and a model asked for an open object has no basis for inventing keys. What
|
|
37
|
+
* it would produce is plausible-looking structure that nothing reads.
|
|
38
|
+
*
|
|
39
|
+
* The three that remain — number, string, boolean — ARE members of the declared
|
|
40
|
+
* union, so the node stays assignable to the member: narrower than the
|
|
41
|
+
* interface, never wider. That is the direction `internal/coupling.ts` fails
|
|
42
|
+
* on. A schema may refuse a value the interface tolerates; it may never admit
|
|
43
|
+
* one the interface cannot hold, because that is the drift that produces a
|
|
44
|
+
* value the rest of the codebase has nowhere to put.
|
|
45
|
+
*
|
|
46
|
+
* ## Why this one is a union at all
|
|
47
|
+
*
|
|
48
|
+
* Most members have a single type. This one genuinely does not: the same
|
|
49
|
+
* property holds `0` on a numeric input, `'Pretoria'` on a text input and
|
|
50
|
+
* `false` on a toggle, and which is correct is decided entirely by the element
|
|
51
|
+
* the model has already chosen. So the branches are described by the CONTROL
|
|
52
|
+
* each belongs to rather than by its JavaScript type — a model choosing between
|
|
53
|
+
* "a number" and "a string" is choosing on the wrong axis, and will reach for a
|
|
54
|
+
* string on a numeric field because the label sounded textual.
|
|
55
|
+
*
|
|
56
|
+
* A union node has no description slot of its own, so the rule that governs all
|
|
57
|
+
* three branches — agree with `dataType` — is stated on each of them.
|
|
58
|
+
*/
|
|
59
|
+
export const valueSchema = s.anyOf([
|
|
60
|
+
s.number('A numeric starting value, for a control whose dataType is number: a quantity, a rate, an amount. Write it as a JSON number, never as a quoted string. Use 0 only where zero is a meaningful default rather than a stand-in for "empty" — an untouched numeric field is better left with no value at all than pre-filled with a figure the user may not notice and may not mean.'),
|
|
61
|
+
s.string('A text starting value, for a control whose dataType is string: a default selection, a standard reference, a boilerplate line. This is also the branch for a date control, whose value must then be written as an ISO date string. The empty string is permitted and means "starts blank", which is the same as omitting the member; prefer omitting it.'),
|
|
62
|
+
s.boolean('A starting state for a toggle, checkbox or slide toggle, whose dataType is boolean. Set it to the state the user should not have to change: false where the affirmative is the exception, true where consent or inclusion is the norm for this form.'),
|
|
63
|
+
]);
|
|
64
|
+
// --- script -----------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* The `script` member: application behaviour attached to the control.
|
|
67
|
+
*
|
|
68
|
+
* Declared as `Record<string, unknown>` and validated as a bare `Joi.object()`,
|
|
69
|
+
* so at the type level it is an open map and nothing constrains its keys.
|
|
70
|
+
* Skillet has no record node and an open map is not expressible in it — but
|
|
71
|
+
* modelling this as "some object" would be worse than not modelling it at all,
|
|
72
|
+
* because a model handed an open slot fills it. Every key it invented would be
|
|
73
|
+
* accepted by the interface, accepted by Joi, stored on the input, and then
|
|
74
|
+
* ignored at runtime by an application that only reads keys it already knows.
|
|
75
|
+
*
|
|
76
|
+
* The form builder answers what those keys are: its editor declares exactly one
|
|
77
|
+
* row for this member, at the `deepBind` path `['script', 'onChange']`. That is
|
|
78
|
+
* the entire surface a human author is offered, so it is the entire surface
|
|
79
|
+
* offered here. A closed object carrying that single key is still assignable to
|
|
80
|
+
* `Record<string, unknown>`, which keeps the coupling honest while leaving the
|
|
81
|
+
* model no room to invent hooks that do not exist.
|
|
82
|
+
*/
|
|
83
|
+
export const scriptSchema = s.object('Application behaviour attached to this control. Set this only where the form genuinely has to react to this field changing; most inputs carry no script at all.', {
|
|
84
|
+
onChange: s.string(describe('The name of the handler the host application runs when this value changes. It must be a handler the application already registers — an unrecognised name is ignored at runtime, so inventing one produces a field that looks wired up and does nothing.', 'script', 'onChange')),
|
|
85
|
+
});
|
|
86
|
+
export const calculateListFunctionsSchema = s.array('Totals and averages computed across the rows of this repeatable group and shown beneath it. Add one entry per figure the user should see summarised; leave the list empty when the rows do not roll up to anything meaningful. These are for the user only: a figure used anywhere else — a decision gate, a validator, a list column — must also be a calculated field on the form itself, aggregating this list.', s.object('One roll-up over one column of the group.', {
|
|
87
|
+
formControlName: s.string('The formControlName of the column to aggregate. It must be a field that exists inside this repeatable group, and — except for a count — a field that holds a number.'),
|
|
88
|
+
func: s.enumeration('Which aggregate to compute. Use count to report how many rows were captured: it is the only function that does not need a numeric column.', enumValues(CalculationFunctions)),
|
|
89
|
+
}));
|
|
90
|
+
/**
|
|
91
|
+
* Members of a generated `calculateListFunctions` the application must supply.
|
|
92
|
+
* Both are required by Joi, so an unexpanded draft fails validation.
|
|
93
|
+
*/
|
|
94
|
+
export const CALCULATE_LIST_DRAFT_COMPLETION = [
|
|
95
|
+
'calculateListFunctions[].id',
|
|
96
|
+
'calculateListFunctions[].inputId (resolved from formControlName)',
|
|
97
|
+
];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { AdjudicationSteps, AutocompleteOptions, AutocapitalizeOptions, ElementTypes, InputDataTypes, InputPipeTypes, InputTypes } from '../interfaces/formInput/index.js';
|
|
3
|
+
import { DataSources } from '../interfaces/formInput/APIDataFetchingConfigurationInterface.js';
|
|
4
|
+
import { InputViewTypes, OptionSelectTypes } from '../interfaces/formInput/BasicFormInputInterface.js';
|
|
5
|
+
import { InputFileType } from '../interfaces/formInput/FileUploadInputInterface.js';
|
|
6
|
+
import { AccountingBasis } from '../interfaces/formInput/IMscoaAccount.js';
|
|
7
|
+
import { MultipleInputAvailableOperations } from '../interfaces/formInput/MultipleInterface.js';
|
|
8
|
+
import { RichTextEditorType } from '../interfaces/formInput/RichTextEditorInput.js';
|
|
9
|
+
import { LabelPosition } from '../interfaces/formInput/ToggleInputInterface.js';
|
|
10
|
+
/**
|
|
11
|
+
* Skillet nodes for the shared vocabulary — every closed set of values a form
|
|
12
|
+
* input can carry.
|
|
13
|
+
*
|
|
14
|
+
* Enum-backed nodes call {@link enumValues}, so their entries are read off the
|
|
15
|
+
* live enum at module evaluation. There is no list here to fall out of date:
|
|
16
|
+
* adding `ElementTypes.Foo` puts `'foo'` in the schema and widens `s.Infer`
|
|
17
|
+
* automatically. Only the DESCRIPTION is authored, and the description is the
|
|
18
|
+
* part that actually steers the model.
|
|
19
|
+
*
|
|
20
|
+
* Union-backed nodes (types with no runtime object to read) go through
|
|
21
|
+
* {@link exhaustive}, which fails to compile if the restated tuple drifts from
|
|
22
|
+
* the union.
|
|
23
|
+
*/
|
|
24
|
+
export declare const elementTypeSchema: s.EnumType<ElementTypes[]>;
|
|
25
|
+
export declare const inputTypeSchema: s.EnumType<InputTypes[]>;
|
|
26
|
+
export declare const inputDataTypeSchema: s.EnumType<InputDataTypes[]>;
|
|
27
|
+
export declare const inputViewTypeSchema: s.EnumType<InputViewTypes.ViewOnly[]>;
|
|
28
|
+
/**
|
|
29
|
+
* `MatFormFieldAppearance` is a string-union type, not an enum, so its members
|
|
30
|
+
* are restated here under {@link exhaustive} rather than derived.
|
|
31
|
+
*/
|
|
32
|
+
export declare const APPEARANCE_VALUES: readonly ["fill", "outline"];
|
|
33
|
+
export declare const appearanceSchema: s.EnumType<["fill", "outline"]>;
|
|
34
|
+
export declare const labelPositionSchema: s.EnumType<LabelPosition[]>;
|
|
35
|
+
export declare const optionSelectTypeSchema: s.EnumType<OptionSelectTypes[]>;
|
|
36
|
+
/**
|
|
37
|
+
* `AutocompleteOptions` is a two-member enum — `on` and `off`.
|
|
38
|
+
*
|
|
39
|
+
* The description used to tell the model to "use a specific token such as email
|
|
40
|
+
* or tel", which is what the HTML `autocomplete` attribute accepts but NOT what
|
|
41
|
+
* this enum offers. A model following it emitted `email`, which is not a member,
|
|
42
|
+
* so the value was rejected — an instruction the schema itself made impossible
|
|
43
|
+
* to obey. Derived entries and authored prose have to agree, and here only the
|
|
44
|
+
* prose could be wrong.
|
|
45
|
+
*/
|
|
46
|
+
export declare const autocompleteSchema: s.EnumType<AutocompleteOptions[]>;
|
|
47
|
+
export declare const autocapitalizeSchema: s.EnumType<AutocapitalizeOptions[]>;
|
|
48
|
+
/** `wrap` is declared inline on the textarea interface as a string union. */
|
|
49
|
+
export declare const TEXTAREA_WRAP_VALUES: readonly ["hard", "soft"];
|
|
50
|
+
export declare const textareaWrapSchema: s.EnumType<["hard", "soft"]>;
|
|
51
|
+
export declare const inputPipeTypeSchema: s.EnumType<InputPipeTypes[]>;
|
|
52
|
+
export declare const richTextEditorTypeSchema: s.EnumType<RichTextEditorType[]>;
|
|
53
|
+
export declare const inputFileTypeSchema: s.EnumType<InputFileType[]>;
|
|
54
|
+
export declare const dataSourceSchema: s.EnumType<DataSources[]>;
|
|
55
|
+
export declare const multipleInputOperationSchema: s.EnumType<MultipleInputAvailableOperations[]>;
|
|
56
|
+
export declare const adjudicationStepSchema: s.EnumType<AdjudicationSteps[]>;
|
|
57
|
+
export declare const accountingBasisSchema: s.EnumType<AccountingBasis[]>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { AdjudicationSteps, AutocompleteOptions, AutocapitalizeOptions, ElementTypes, InputDataTypes, InputPipeTypes, InputTypes, } from '../interfaces/formInput/index.js';
|
|
3
|
+
import { DataSources } from '../interfaces/formInput/APIDataFetchingConfigurationInterface.js';
|
|
4
|
+
import { InputViewTypes, OptionSelectTypes, } from '../interfaces/formInput/BasicFormInputInterface.js';
|
|
5
|
+
import { InputFileType } from '../interfaces/formInput/FileUploadInputInterface.js';
|
|
6
|
+
import { AccountingBasis } from '../interfaces/formInput/IMscoaAccount.js';
|
|
7
|
+
import { MultipleInputAvailableOperations } from '../interfaces/formInput/MultipleInterface.js';
|
|
8
|
+
import { RichTextEditorType } from '../interfaces/formInput/RichTextEditorInput.js';
|
|
9
|
+
import { LabelPosition } from '../interfaces/formInput/ToggleInputInterface.js';
|
|
10
|
+
import { enumValues, exhaustive } from './internal/coupling.js';
|
|
11
|
+
import { describe } from './internal/editor-guidance.js';
|
|
12
|
+
/**
|
|
13
|
+
* Skillet nodes for the shared vocabulary — every closed set of values a form
|
|
14
|
+
* input can carry.
|
|
15
|
+
*
|
|
16
|
+
* Enum-backed nodes call {@link enumValues}, so their entries are read off the
|
|
17
|
+
* live enum at module evaluation. There is no list here to fall out of date:
|
|
18
|
+
* adding `ElementTypes.Foo` puts `'foo'` in the schema and widens `s.Infer`
|
|
19
|
+
* automatically. Only the DESCRIPTION is authored, and the description is the
|
|
20
|
+
* part that actually steers the model.
|
|
21
|
+
*
|
|
22
|
+
* Union-backed nodes (types with no runtime object to read) go through
|
|
23
|
+
* {@link exhaustive}, which fails to compile if the restated tuple drifts from
|
|
24
|
+
* the union.
|
|
25
|
+
*/
|
|
26
|
+
// --- Element identity -------------------------------------------------------
|
|
27
|
+
export const elementTypeSchema = s.enumeration(describe('The kind of control to render. This determines which other properties are meaningful, so choose it first and let it drive the rest.', 'element'), enumValues(ElementTypes));
|
|
28
|
+
export const inputTypeSchema = s.enumeration(describe("The native HTML input type, for elements that render an <input>. Match it to the data: 'number' for a quantity, an amount or a percentage; 'email'; 'tel' for a phone number; 'url'; 'password' for a one-time code; 'date' for a memorable date typed from memory such as a date of birth; 'time' or 'month'; 'text' for names, references and codes.", 'type'), enumValues(InputTypes));
|
|
29
|
+
export const inputDataTypeSchema = s.enumeration(describe("The JavaScript type the captured value is stored as: 'number' for a numeric input or a calculated field, 'boolean' for a toggle, 'date' for a date picker, 'array' for an item list, 'object' for a file, a location or an account picker, 'string' for everything else.", 'dataType'), enumValues(InputDataTypes));
|
|
30
|
+
export const inputViewTypeSchema = s.enumeration(describe('Renders the control as read-only presentation rather than an editable field.', 'viewType'), enumValues(InputViewTypes));
|
|
31
|
+
// --- Presentation -----------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* `MatFormFieldAppearance` is a string-union type, not an enum, so its members
|
|
34
|
+
* are restated here under {@link exhaustive} rather than derived.
|
|
35
|
+
*/
|
|
36
|
+
export const APPEARANCE_VALUES = exhaustive()([
|
|
37
|
+
'fill',
|
|
38
|
+
'outline',
|
|
39
|
+
]);
|
|
40
|
+
export const appearanceSchema = s.enumeration(describe("The Material form-field style. Use 'outline' unless the surrounding form already uses 'fill'; do not mix the two within one form.", 'appearance'), [...APPEARANCE_VALUES]);
|
|
41
|
+
export const labelPositionSchema = s.enumeration(describe("Which side of a toggle its label sits on. Use 'after'.", 'labelPosition'), enumValues(LabelPosition));
|
|
42
|
+
export const optionSelectTypeSchema = s.enumeration(describe("How a set of options is presented: 'radioButton' for one choice from two to five fixed options, so the user can scan them all at once; 'dropDown' for six or more, sorted alphabetically unless there is a natural order; 'chipSelect' when several options are chosen together from a short list. A short list left as a dropdown is a common mistake.", 'optionSelectType'), enumValues(OptionSelectTypes));
|
|
43
|
+
/**
|
|
44
|
+
* `AutocompleteOptions` is a two-member enum — `on` and `off`.
|
|
45
|
+
*
|
|
46
|
+
* The description used to tell the model to "use a specific token such as email
|
|
47
|
+
* or tel", which is what the HTML `autocomplete` attribute accepts but NOT what
|
|
48
|
+
* this enum offers. A model following it emitted `email`, which is not a member,
|
|
49
|
+
* so the value was rejected — an instruction the schema itself made impossible
|
|
50
|
+
* to obey. Derived entries and authored prose have to agree, and here only the
|
|
51
|
+
* prose could be wrong.
|
|
52
|
+
*/
|
|
53
|
+
export const autocompleteSchema = s.enumeration(describe('Whether the browser may autofill this field from the user profile.', 'autocomplete'), enumValues(AutocompleteOptions));
|
|
54
|
+
export const autocapitalizeSchema = s.enumeration(describe("How the on-screen keyboard capitalises typed text. Use 'words' for names and 'characters' for reference codes.", 'autocapitalize'), enumValues(AutocapitalizeOptions));
|
|
55
|
+
/** `wrap` is declared inline on the textarea interface as a string union. */
|
|
56
|
+
export const TEXTAREA_WRAP_VALUES = exhaustive()([
|
|
57
|
+
'hard',
|
|
58
|
+
'soft',
|
|
59
|
+
]);
|
|
60
|
+
export const textareaWrapSchema = s.enumeration(describe("How a textarea wraps long lines when the form is submitted. 'soft' leaves the text unchanged; 'hard' inserts real newlines at the wrap points.", 'wrap'), [...TEXTAREA_WRAP_VALUES]);
|
|
61
|
+
export const inputPipeTypeSchema = s.enumeration(describe('A display transform applied to the value when it is shown back to the user. This is presentation only and never changes the stored value.', 'pipe', 'pipeType'), enumValues(InputPipeTypes));
|
|
62
|
+
export const richTextEditorTypeSchema = s.enumeration(describe('Which rich-text editor library backs an editor element.', 'richTextEditorLibrary'), enumValues(RichTextEditorType));
|
|
63
|
+
export const inputFileTypeSchema = s.enumeration(describe("The kind of upload a file control accepts: 'file' for a document such as a quote, an invoice or a certificate; 'captureImage' for a photo taken on the phone camera; 'image' for a picture from disk.", 'fileType'), enumValues(InputFileType));
|
|
64
|
+
// --- Data sourcing ----------------------------------------------------------
|
|
65
|
+
export const dataSourceSchema = s.enumeration("Where a control's options come from: values typed into the form definition, an HTTP endpoint, a MongoDB aggregation pipeline, or a custom option set.", enumValues(DataSources));
|
|
66
|
+
// --- Domain-specific --------------------------------------------------------
|
|
67
|
+
export const multipleInputOperationSchema = s.enumeration(describe('An operation a user may perform on the rows of a repeatable multiple-input group.', 'multipleInputAvailableOperations'), enumValues(MultipleInputAvailableOperations));
|
|
68
|
+
export const adjudicationStepSchema = s.enumeration(describe('Which stage of the tender adjudication workflow this control belongs to.', 'adjudicationStep'), enumValues(AdjudicationSteps));
|
|
69
|
+
export const accountingBasisSchema = s.enumeration("The accounting basis an mSCOA account selection is resolved against. 'accrual' is the usual choice for a budget capture; 'dual' only when the form genuinely captures the cash side as well.", enumValues(AccountingBasis));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { s } from '@hashbrownai/core';
|
|
2
|
+
import { AllFormInputPrimaryKeys } from '../../interfaces/FormBuilder/FormInputKeys.js';
|
|
3
|
+
import { ElementTypes } from '../../interfaces/formInput/index.js';
|
|
4
|
+
import { AUTHORING_RULES, CONTROL_NAME_PATTERN } from '../authoring-rules.js';
|
|
5
|
+
import { DRAFT_COMPLETION_REQUIRED, createFormDraftSchema } from '../form.js';
|
|
6
|
+
import { inputMemberSchemas } from '../input-members.js';
|
|
7
|
+
import { createValidatorsSchema, validatorsDraftSchema, } from '../members/validators.js';
|
|
8
|
+
import { COMMON_ELEMENTS, createColumnSchema } from '../variants.js';
|
|
9
|
+
import { RESERVED_CONTROL_NAMES, describeWorkflowStep, } from '../workflow-context.js';
|
|
10
|
+
/**
|
|
11
|
+
* The rules the schemas carry from the Form Creation Guide
|
|
12
|
+
* (`doc/guides/form-creation-guide.md`) that a test can hold them to.
|
|
13
|
+
*
|
|
14
|
+
* Most of the guide arrives as prose in descriptions, which no test can judge.
|
|
15
|
+
* What can be pinned is the handful of places where the prose and a structure
|
|
16
|
+
* have to agree — the reserved names against the control-name pattern, the
|
|
17
|
+
* step context against the form description, an element the description
|
|
18
|
+
* names against the elements on offer — and the two rules the guide overturned
|
|
19
|
+
* outright: the autocomplete note and the handle-label description.
|
|
20
|
+
*/
|
|
21
|
+
describe('house rules from the Form Creation Guide', () => {
|
|
22
|
+
it('names every reserved control name the pattern would let through', () => {
|
|
23
|
+
// The pattern stops `_id` and the SYSTEM_* inputs on shape alone. The
|
|
24
|
+
// ones it lets through — `reference`, `status`, `data` — have to be named
|
|
25
|
+
// in the rules, or the model has no way to know them. Filtering at
|
|
26
|
+
// evaluation rather than listing by hand is what this pins: add a
|
|
27
|
+
// lowerCamelCase name to RESERVED_CONTROL_NAMES and it appears here.
|
|
28
|
+
const pattern = new RegExp(CONTROL_NAME_PATTERN);
|
|
29
|
+
const rules = AUTHORING_RULES.join(' ');
|
|
30
|
+
const uncovered = RESERVED_CONTROL_NAMES.filter((name) => pattern.test(name) && !new RegExp(`\\b${name}\\b`).test(rules));
|
|
31
|
+
expect(uncovered).toEqual([]);
|
|
32
|
+
expect(pattern.test('_id')).toBe(false);
|
|
33
|
+
expect(pattern.test('SYSTEM_TAGS')).toBe(false);
|
|
34
|
+
expect(pattern.test('CURRENT_FINANCIAL_CYCLE')).toBe(false);
|
|
35
|
+
expect(rules).toContain('reference');
|
|
36
|
+
});
|
|
37
|
+
it('states the rules once, at the root of the form', () => {
|
|
38
|
+
// Hashbrown names `$defs` entries by description and repeats the name at
|
|
39
|
+
// every `$ref`, so a rule on a member every element shares is paid once
|
|
40
|
+
// per element. The root is emitted once, so that is where the rules go —
|
|
41
|
+
// and the member the rule concerns must NOT repeat it.
|
|
42
|
+
const json = s.toJsonSchema(createFormDraftSchema({ elements: COMMON_ELEMENTS }));
|
|
43
|
+
expect(json.description).toContain('never one of the names');
|
|
44
|
+
expect(json.description).toContain('reference');
|
|
45
|
+
const member = s.getDescription(inputMemberSchemas[AllFormInputPrimaryKeys.FormControlName]) ?? '';
|
|
46
|
+
expect(member).not.toContain('reference,');
|
|
47
|
+
expect(member.length).toBeLessThan(400);
|
|
48
|
+
});
|
|
49
|
+
it('withdraws the named-validator branch until names are supplied', () => {
|
|
50
|
+
// Same reasoning as the api source and the document picker: a free string
|
|
51
|
+
// invites a validator name that looks authoritative and resolves to
|
|
52
|
+
// nothing. With no names the item is the inline rule alone; with names it
|
|
53
|
+
// is a union whose string branch enumerates exactly those names.
|
|
54
|
+
const items = (json) => json
|
|
55
|
+
.properties.__wrappedPrimitive.items;
|
|
56
|
+
const bare = items(s.toJsonSchema(validatorsDraftSchema));
|
|
57
|
+
expect(bare.anyOf).toBeUndefined();
|
|
58
|
+
expect(bare.type).toBe('object');
|
|
59
|
+
const named = items(s.toJsonSchema(createValidatorsSchema({
|
|
60
|
+
namedValidators: [
|
|
61
|
+
{ name: 'email', description: 'a well-formed email address' },
|
|
62
|
+
],
|
|
63
|
+
})));
|
|
64
|
+
expect(Array.isArray(named.anyOf)).toBe(true);
|
|
65
|
+
const text = JSON.stringify(named.anyOf);
|
|
66
|
+
expect(text).toContain('"enum"');
|
|
67
|
+
expect(text).toContain('"email"');
|
|
68
|
+
expect(text).toContain('a well-formed email address');
|
|
69
|
+
expect(named.anyOf.some((b) => b.type === 'object')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('threads the named validators through the column options', () => {
|
|
72
|
+
const json = JSON.stringify(s.toJsonSchema(createColumnSchema({
|
|
73
|
+
elements: [ElementTypes.Input],
|
|
74
|
+
namedValidators: [{ name: 'southAfricanId' }],
|
|
75
|
+
})));
|
|
76
|
+
expect(json).toContain('southAfricanId');
|
|
77
|
+
const bare = JSON.stringify(s.toJsonSchema(createColumnSchema({ elements: [ElementTypes.Input] })));
|
|
78
|
+
expect(bare).not.toContain('southAfricanId');
|
|
79
|
+
});
|
|
80
|
+
it('tells the model which step the form serves', () => {
|
|
81
|
+
const json = JSON.stringify(s.toJsonSchema(createFormDraftSchema({
|
|
82
|
+
elements: COMMON_ELEMENTS,
|
|
83
|
+
step: {
|
|
84
|
+
type: 'review',
|
|
85
|
+
controlName: 'managerReview',
|
|
86
|
+
expectedControls: [
|
|
87
|
+
{
|
|
88
|
+
name: 'totalAmount',
|
|
89
|
+
purpose: 'decision gate compares it against 30000',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
prefilledControlNames: ['department'],
|
|
93
|
+
},
|
|
94
|
+
})));
|
|
95
|
+
expect(json).toContain('serves the review step');
|
|
96
|
+
expect(json).toContain("'managerReview'");
|
|
97
|
+
expect(json).toContain('totalAmount');
|
|
98
|
+
expect(json).toContain('decision gate compares it against 30000');
|
|
99
|
+
expect(json).toContain("'department'");
|
|
100
|
+
});
|
|
101
|
+
it('says nothing about a workflow step unless one was given', () => {
|
|
102
|
+
const json = JSON.stringify(s.toJsonSchema(createFormDraftSchema({ elements: COMMON_ELEMENTS })));
|
|
103
|
+
expect(json).not.toContain('serves the');
|
|
104
|
+
});
|
|
105
|
+
it('asks for the review control name when it was not supplied', () => {
|
|
106
|
+
// A review step routes on one toggle, and its name is the workflow
|
|
107
|
+
// designer's. Without it the model must be told to get it from the
|
|
108
|
+
// request rather than left to pick one.
|
|
109
|
+
expect(describeWorkflowStep({ type: 'review' })).toContain('which the request must state');
|
|
110
|
+
expect(describeWorkflowStep({ type: 'initiate' })).toContain('never ask the user for it');
|
|
111
|
+
});
|
|
112
|
+
it('points the model at section titles only when they are on offer', () => {
|
|
113
|
+
// A sentence naming an element the union does not contain is how a model
|
|
114
|
+
// talks itself into inventing one.
|
|
115
|
+
const withHeadings = JSON.stringify(s.toJsonSchema(createFormDraftSchema({ elements: COMMON_ELEMENTS })));
|
|
116
|
+
expect(withHeadings).toContain('sectionTitle whose hintLabel');
|
|
117
|
+
const without = JSON.stringify(s.toJsonSchema(createFormDraftSchema({ elements: [ElementTypes.Input] })));
|
|
118
|
+
expect(without).not.toContain('sectionTitle whose hintLabel');
|
|
119
|
+
});
|
|
120
|
+
it('offers the always-shown settings on every element that captures a value', () => {
|
|
121
|
+
// `readonly`, `onlySetTempErrorOnTouch` and `tourContent` are edited
|
|
122
|
+
// through `Default` rows, which the settings panel shows on every element
|
|
123
|
+
// and which no element's `properties` lists. The guide treats the first
|
|
124
|
+
// two as settings set on nearly every field — a prefilled value is
|
|
125
|
+
// read-only, errors wait for the user to leave the field — and the step
|
|
126
|
+
// context tells the model to use `readonly`, so it has to be there.
|
|
127
|
+
const json = s.toJsonSchema(createColumnSchema({ elements: COMMON_ELEMENTS }));
|
|
128
|
+
for (const branch of json.anyOf) {
|
|
129
|
+
const keys = Object.keys(branch.properties);
|
|
130
|
+
const element = branch.properties['element'].const;
|
|
131
|
+
const captures = element !== ElementTypes.SectionTitle;
|
|
132
|
+
for (const key of ['readonly', 'onlySetTempErrorOnTouch', 'tourContent']) {
|
|
133
|
+
expect(keys.includes(key))
|
|
134
|
+
.withContext(`${element} ${captures ? 'should' : 'should not'} offer ${key}`)
|
|
135
|
+
.toBe(captures);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// The interface requires the flag and the schema leaves it optional, so
|
|
139
|
+
// the application still owes a default — and the guide says which.
|
|
140
|
+
const entry = DRAFT_COMPLETION_REQUIRED.find((path) => path.startsWith('slides[].columns[].onlySetTempErrorOnTouch'));
|
|
141
|
+
expect(entry).toBeDefined();
|
|
142
|
+
expect(entry).toContain('true');
|
|
143
|
+
});
|
|
144
|
+
it('describes the item-list handle as a row button, not a file control', () => {
|
|
145
|
+
// `handleLabel` is declared by `IMultiple` and listed only by the item
|
|
146
|
+
// list, yet its description used to speak of "the attached file".
|
|
147
|
+
const description = s.getDescription(inputMemberSchemas[AllFormInputPrimaryKeys.HandleLabel]) ??
|
|
148
|
+
'';
|
|
149
|
+
expect(description).toContain('Add item');
|
|
150
|
+
expect(description).not.toContain('file');
|
|
151
|
+
});
|
|
152
|
+
it('steers the autocomplete by whether the user may add a value, not by list length', () => {
|
|
153
|
+
// The guide draws the line the old note did not: an autocomplete stores
|
|
154
|
+
// whatever is typed, so it is for a list the user may add to; a merely
|
|
155
|
+
// long list is a paginated selection table.
|
|
156
|
+
const json = s.toJsonSchema(createColumnSchema({ elements: [ElementTypes.AutoCompleteInput] }));
|
|
157
|
+
const notes = json.anyOf.map((branch) => branch.description ?? '').join(' ');
|
|
158
|
+
expect(notes).toContain('stores whatever the user types');
|
|
159
|
+
expect(notes).toContain('paginatedSelectionTable');
|
|
160
|
+
expect(notes).not.toContain('Prefer it over a select when the list is long');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|