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.
Files changed (68) hide show
  1. package/dist/interfaces/Form/formSubmissionHandleInterface.d.ts +6 -0
  2. package/dist/interfaces/FormBuilder/DefaultEelement.js +76 -0
  3. package/dist/interfaces/FormBuilder/DefaultInputConfigInterface.d.ts +13 -0
  4. package/dist/interfaces/FormBuilder/FormInputKeys.d.ts +6 -0
  5. package/dist/interfaces/FormBuilder/FormInputKeys.js +6 -0
  6. package/dist/interfaces/FormBuilder/inputConfig/ElementEditConfig.js +188 -3
  7. package/dist/interfaces/Import/ImportIdentity.d.ts +32 -0
  8. package/dist/interfaces/Import/ImportIdentity.js +1 -0
  9. package/dist/interfaces/Import/ImportProgress.d.ts +12 -1
  10. package/dist/interfaces/Import/ImportRowState.d.ts +52 -1
  11. package/dist/interfaces/Import/index.d.ts +2 -1
  12. package/dist/interfaces/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
  13. package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
  14. package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
  15. package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
  16. package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
  17. package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
  18. package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
  19. package/dist/schemas/FormInputSchema.js +41 -2
  20. package/dist/schemas/MatOptionsSchema.js +7 -0
  21. package/dist/schemas/MscoaConfigSchema.js +1 -0
  22. package/dist/schemas/index.d.ts +2 -1
  23. package/dist/schemas/index.js +2 -1
  24. package/dist/skillet/authoring-rules.d.ts +41 -0
  25. package/dist/skillet/authoring-rules.js +61 -0
  26. package/dist/skillet/extra-members.d.ts +143 -0
  27. package/dist/skillet/extra-members.js +79 -0
  28. package/dist/skillet/form.d.ts +170 -0
  29. package/dist/skillet/form.js +139 -0
  30. package/dist/skillet/index.d.ts +43 -0
  31. package/dist/skillet/index.js +46 -0
  32. package/dist/skillet/input-members.d.ts +375 -0
  33. package/dist/skillet/input-members.js +186 -0
  34. package/dist/skillet/internal/coupling.d.ts +63 -0
  35. package/dist/skillet/internal/coupling.js +69 -0
  36. package/dist/skillet/internal/editor-guidance.d.ts +64 -0
  37. package/dist/skillet/internal/editor-guidance.js +291 -0
  38. package/dist/skillet/members/calculated-field.d.ts +155 -0
  39. package/dist/skillet/members/calculated-field.js +105 -0
  40. package/dist/skillet/members/conditional.d.ts +69 -0
  41. package/dist/skillet/members/conditional.js +16 -0
  42. package/dist/skillet/members/document-picker.d.ts +182 -0
  43. package/dist/skillet/members/document-picker.js +111 -0
  44. package/dist/skillet/members/mat-options.d.ts +187 -0
  45. package/dist/skillet/members/mat-options.js +92 -0
  46. package/dist/skillet/members/mscoa.d.ts +182 -0
  47. package/dist/skillet/members/mscoa.js +129 -0
  48. package/dist/skillet/members/pagination.d.ts +59 -0
  49. package/dist/skillet/members/pagination.js +16 -0
  50. package/dist/skillet/members/table.d.ts +178 -0
  51. package/dist/skillet/members/table.js +105 -0
  52. package/dist/skillet/members/validators.d.ts +197 -0
  53. package/dist/skillet/members/validators.js +74 -0
  54. package/dist/skillet/members/value.d.ts +154 -0
  55. package/dist/skillet/members/value.js +97 -0
  56. package/dist/skillet/shared-types.d.ts +57 -0
  57. package/dist/skillet/shared-types.js +69 -0
  58. package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
  59. package/dist/skillet/tests/house-rules.spec.js +162 -0
  60. package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
  61. package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
  62. package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
  63. package/dist/skillet/tests/variant-assembly.spec.js +643 -0
  64. package/dist/skillet/variants.d.ts +138 -0
  65. package/dist/skillet/variants.js +536 -0
  66. package/dist/skillet/workflow-context.d.ts +95 -0
  67. package/dist/skillet/workflow-context.js +128 -0
  68. 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 {};