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.
Files changed (63) 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/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
  8. package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
  9. package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
  10. package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
  11. package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
  12. package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
  13. package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
  14. package/dist/schemas/FormInputSchema.js +41 -2
  15. package/dist/schemas/MatOptionsSchema.js +7 -0
  16. package/dist/schemas/MscoaConfigSchema.js +1 -0
  17. package/dist/schemas/index.d.ts +2 -1
  18. package/dist/schemas/index.js +2 -1
  19. package/dist/skillet/authoring-rules.d.ts +41 -0
  20. package/dist/skillet/authoring-rules.js +61 -0
  21. package/dist/skillet/extra-members.d.ts +143 -0
  22. package/dist/skillet/extra-members.js +79 -0
  23. package/dist/skillet/form.d.ts +170 -0
  24. package/dist/skillet/form.js +139 -0
  25. package/dist/skillet/index.d.ts +43 -0
  26. package/dist/skillet/index.js +46 -0
  27. package/dist/skillet/input-members.d.ts +375 -0
  28. package/dist/skillet/input-members.js +186 -0
  29. package/dist/skillet/internal/coupling.d.ts +63 -0
  30. package/dist/skillet/internal/coupling.js +69 -0
  31. package/dist/skillet/internal/editor-guidance.d.ts +64 -0
  32. package/dist/skillet/internal/editor-guidance.js +291 -0
  33. package/dist/skillet/members/calculated-field.d.ts +155 -0
  34. package/dist/skillet/members/calculated-field.js +105 -0
  35. package/dist/skillet/members/conditional.d.ts +69 -0
  36. package/dist/skillet/members/conditional.js +16 -0
  37. package/dist/skillet/members/document-picker.d.ts +182 -0
  38. package/dist/skillet/members/document-picker.js +111 -0
  39. package/dist/skillet/members/mat-options.d.ts +187 -0
  40. package/dist/skillet/members/mat-options.js +92 -0
  41. package/dist/skillet/members/mscoa.d.ts +182 -0
  42. package/dist/skillet/members/mscoa.js +129 -0
  43. package/dist/skillet/members/pagination.d.ts +59 -0
  44. package/dist/skillet/members/pagination.js +16 -0
  45. package/dist/skillet/members/table.d.ts +178 -0
  46. package/dist/skillet/members/table.js +105 -0
  47. package/dist/skillet/members/validators.d.ts +197 -0
  48. package/dist/skillet/members/validators.js +74 -0
  49. package/dist/skillet/members/value.d.ts +154 -0
  50. package/dist/skillet/members/value.js +97 -0
  51. package/dist/skillet/shared-types.d.ts +57 -0
  52. package/dist/skillet/shared-types.js +69 -0
  53. package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
  54. package/dist/skillet/tests/house-rules.spec.js +162 -0
  55. package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
  56. package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
  57. package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
  58. package/dist/skillet/tests/variant-assembly.spec.js +643 -0
  59. package/dist/skillet/variants.d.ts +138 -0
  60. package/dist/skillet/variants.js +536 -0
  61. package/dist/skillet/workflow-context.d.ts +95 -0
  62. package/dist/skillet/workflow-context.js +128 -0
  63. package/package.json +78 -60
@@ -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 {};
@@ -0,0 +1,191 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { AllFormInputPrimaryKeys, FormInputKeys, SpecialElementKeys, } from '../../interfaces/FormBuilder/FormInputKeys.js';
3
+ import { DRAFT_COMPLETION_REQUIRED } from '../form.js';
4
+ import { ElementTypes } from '../../interfaces/formInput/index.js';
5
+ import { DataSources } from '../../interfaces/formInput/APIDataFetchingConfigurationInterface.js';
6
+ import { ASSEMBLED_INPUT_MEMBERS, DEPRECATED_INPUT_MEMBERS, DRAFT_INPUT_MEMBERS, draftMemberSchemas, PENDING_INPUT_MEMBERS, SYSTEM_OWNED_INPUT_MEMBERS, inputMemberSchemas, } from '../input-members.js';
7
+ import { ALWAYS_SHOWN_MEMBERS, SECONDARY_GUIDED_PATHS, UNREADABLE_EXPRESSIONS, describeIn, } from '../internal/editor-guidance.js';
8
+ import { createMatOptionsSchema } from '../members/mat-options.js';
9
+ import { createWorkflowPickerSchema } from '../members/document-picker.js';
10
+ import { createValidatorsSchema } from '../members/validators.js';
11
+ import { elementTypeSchema } from '../shared-types.js';
12
+ /**
13
+ * Covers the invariants the compiler cannot reach.
14
+ *
15
+ * The type-level assertions in `input-members.ts` already prove that every key
16
+ * is classified and that no node produces a value its interface cannot hold —
17
+ * those fail the build, so they need no test. What is left is everything that
18
+ * depends on runtime values: whether the derivation actually found the guidance
19
+ * it was pointed at, and whether the enum-derived nodes really carry the
20
+ * members their enums declare.
21
+ */
22
+ describe('skillet coupling', () => {
23
+ it('reads every applicability expression in the editor config', () => {
24
+ // The expression grammar is closed today — `key === value` joined by `||`.
25
+ // Reaching for `&&` or `!==` in ElementEditConfig would leave the rule
26
+ // unparsed and silently drop that guidance from the schema, so the failure
27
+ // has to surface somewhere. It surfaces here.
28
+ expect(UNREADABLE_EXPRESSIONS).toEqual([]);
29
+ });
30
+ it('classifies every key exactly once', () => {
31
+ const categories = [
32
+ Object.keys(inputMemberSchemas),
33
+ [...PENDING_INPUT_MEMBERS],
34
+ [...SYSTEM_OWNED_INPUT_MEMBERS],
35
+ [...DEPRECATED_INPUT_MEMBERS],
36
+ [...DRAFT_INPUT_MEMBERS],
37
+ [...ASSEMBLED_INPUT_MEMBERS],
38
+ ].flat();
39
+ const enumKeys = Object.values(AllFormInputPrimaryKeys);
40
+ expect(categories.length).toBe(enumKeys.length);
41
+ expect(new Set(categories).size).toBe(categories.length);
42
+ expect([...categories].sort()).toEqual([...enumKeys].sort());
43
+ });
44
+ it('gives every modelled member a description', () => {
45
+ const undescribed = Object.entries(inputMemberSchemas)
46
+ .filter(([, node]) => {
47
+ const description = s.getDescription(node);
48
+ // Union nodes carry their descriptions on the branches rather than the
49
+ // node itself, so an empty description there is expected.
50
+ return description !== undefined && description.trim() === '';
51
+ })
52
+ .map(([key]) => key);
53
+ expect(undescribed).toEqual([]);
54
+ });
55
+ it('derives element options from the live enum, not a copied list', () => {
56
+ const json = s.toJsonSchema(elementTypeSchema);
57
+ const entries = json.properties.__wrappedPrimitive.enum;
58
+ expect([...entries].sort()).toEqual([...Object.values(ElementTypes)].sort());
59
+ // Members commented out of `ElementTypes` must not reappear. A hand-copied
60
+ // list is exactly how they would.
61
+ expect(entries).not.toContain('qrCodeScanner');
62
+ expect(entries).not.toContain('matrixTable');
63
+ });
64
+ it('excludes deprecated and system-owned members from generation', () => {
65
+ const modelled = Object.keys(inputMemberSchemas);
66
+ for (const key of DEPRECATED_INPUT_MEMBERS) {
67
+ expect(modelled).not.toContain(key);
68
+ }
69
+ for (const key of SYSTEM_OWNED_INPUT_MEMBERS) {
70
+ expect(modelled).not.toContain(key);
71
+ }
72
+ });
73
+ it('never offers an api source unless endpoints were supplied', () => {
74
+ // The whole point of the factory. With no endpoint list there is nothing
75
+ // for a model to select, so the branch must not exist — otherwise it
76
+ // invents an `httpEndPoint`, and a form pointing at a URL that does not
77
+ // exist looks completely correct until someone opens it.
78
+ const json = JSON.stringify(s.toJsonSchema(draftMemberSchemas[AllFormInputPrimaryKeys.MatOptions]));
79
+ expect(json).not.toContain('httpEndPoint');
80
+ expect(json).not.toContain('endpointId');
81
+ expect(json).toContain(DataSources.Local);
82
+ expect(json).toContain(DataSources.CustomOptions);
83
+ });
84
+ it('offers an api source once endpoints are supplied', () => {
85
+ const json = JSON.stringify(s.toJsonSchema(createMatOptionsSchema({
86
+ apiEndpoints: [{ id: 'abc123', name: 'Departments' }],
87
+ })));
88
+ expect(json).toContain('endpointId');
89
+ // The model selects a known id; it is never asked to author a URL.
90
+ expect(json).toContain('abc123');
91
+ expect(json).not.toContain('httpEndPoint');
92
+ });
93
+ it('takes the colSize bounds from the editor range control', () => {
94
+ const json = s.toJsonSchema(inputMemberSchemas[AllFormInputPrimaryKeys.ColSize]);
95
+ const bounds = json.properties.__wrappedPrimitive;
96
+ expect(bounds.minimum).toBe(1);
97
+ expect(bounds.maximum).toBe(12);
98
+ });
99
+ it('reaches the guidance inside secondary editors', () => {
100
+ // Rows nested in a `secondaryElementEditorConfig` were invisible to the
101
+ // derivation until it was taught to walk into them, and nothing failed to
102
+ // say so — the hints simply were not there. Both editors that exist today
103
+ // must be found, or that silent loss is back.
104
+ expect([...SECONDARY_GUIDED_PATHS.keys()].sort()).toEqual([
105
+ 'mscoaConfig',
106
+ 'workflowPickerConfig.presetFilters',
107
+ ]);
108
+ const filterPaths = SECONDARY_GUIDED_PATHS.get('workflowPickerConfig.presetFilters');
109
+ expect(filterPaths).toContain('valueSource');
110
+ expect(filterPaths).toContain('fromInputId');
111
+ });
112
+ it('derives the rows the settings panel shows on every element', () => {
113
+ // A `Default` row is rendered for every element and listed in no
114
+ // element's `properties`, so the members it edits were invisible to
115
+ // assembly. The derivation must find the ungated ones and leave the
116
+ // gated ones — `min` needs a numeric type, the rules need the flag — to
117
+ // the structural handling they already have.
118
+ expect(ALWAYS_SHOWN_MEMBERS).toContain(AllFormInputPrimaryKeys.Readonly);
119
+ expect(ALWAYS_SHOWN_MEMBERS).toContain(AllFormInputPrimaryKeys.OnlySetTempErrorOnTouch);
120
+ expect(ALWAYS_SHOWN_MEMBERS).toContain(AllFormInputPrimaryKeys.TourContent);
121
+ expect(ALWAYS_SHOWN_MEMBERS).toContain(AllFormInputPrimaryKeys.HintLabel);
122
+ expect(ALWAYS_SHOWN_MEMBERS).not.toContain(AllFormInputPrimaryKeys.Min);
123
+ expect(ALWAYS_SHOWN_MEMBERS).not.toContain(AllFormInputPrimaryKeys.IsCalculatedField);
124
+ expect(ALWAYS_SHOWN_MEMBERS).not.toContain(AllFormInputPrimaryKeys.CalculatedFieldRules);
125
+ // A nested binding belongs to the object that owns it, not to the column.
126
+ expect(ALWAYS_SHOWN_MEMBERS).not.toContain(AllFormInputPrimaryKeys.Script);
127
+ });
128
+ it('reads a negated applicability test as an exclusion', () => {
129
+ // `valueSource !== input` used to be rejected outright. Reading it as a
130
+ // POSITIVE rule would be worse than dropping it, so the polarity is what
131
+ // this pins down.
132
+ const fixed = describeIn('Base.', ['workflowPickerConfig', 'presetFilters'], 'value');
133
+ expect(fixed).toContain('valueSource is not input');
134
+ expect(fixed).not.toContain('valueSource is input.');
135
+ const bound = describeIn('Base.', ['workflowPickerConfig', 'presetFilters'], 'fromInputId');
136
+ expect(bound).toContain('valueSource is input');
137
+ });
138
+ it('keeps a secondary editor from leaking onto its opening path', () => {
139
+ // The mSCOA composite editor opens at `mscoaConfig` but edits dual-cash
140
+ // exclusion RULES, so prefixing its rows with the opening path would file
141
+ // `pattern` under `mscoaConfig.pattern` — a path nothing has. Guidance for
142
+ // it must be reachable through the nested lookup and nowhere else.
143
+ expect(SECONDARY_GUIDED_PATHS.get('mscoaConfig')).toContain('pattern');
144
+ expect(describeIn('Base.', ['mscoaConfig'], 'pattern')).not.toBe('Base.');
145
+ });
146
+ it('offers a validator as either a known name or a rule written inline', () => {
147
+ // The member is `Array<FormControlCustomValidatorsInterface | string>` and
148
+ // Joi mirrors it with `alternatives().try(string, object)`. Collapsing that
149
+ // to one branch is the failure this guards: only the object, and every
150
+ // built-in has to be rewritten by hand; only the string, and no
151
+ // form-specific rule can be expressed at all.
152
+ // The named branch exists only over names the caller supplied — with none,
153
+ // the item is the inline rule alone; see the house-rules spec.
154
+ // A schema whose root is not an object comes back wrapped, the same way
155
+ // the colSize bounds above are read.
156
+ const json = s.toJsonSchema(createValidatorsSchema({ namedValidators: [{ name: 'email' }] }));
157
+ const branches = json.properties.__wrappedPrimitive.items.anyOf.map((branch) => branch.type);
158
+ expect(branches).toContain('string');
159
+ expect(branches).toContain('object');
160
+ });
161
+ it('states every completion path against a member that exists', () => {
162
+ // `DRAFT_COMPLETION_REQUIRED` prefixes each module's entries with the column
163
+ // path they hang off, so a module has to start its own entries at the member
164
+ // it owns. `calculated-field` began at `variables[].id` instead of
165
+ // `calculatedFieldRules.variables[].id`, and the aggregate came out as
166
+ // `slides[].columns[].variables[].id` — a confident-looking path to nowhere,
167
+ // which is worse than an absent one for whoever writes the expansion.
168
+ const COLUMN = 'slides[].columns[].';
169
+ const known = new Set([
170
+ ...Object.values(AllFormInputPrimaryKeys),
171
+ ...Object.values(FormInputKeys),
172
+ ...Object.values(SpecialElementKeys),
173
+ ]);
174
+ const bad = DRAFT_COMPLETION_REQUIRED.filter((path) => path.startsWith(COLUMN))
175
+ .map((path) => path.slice(COLUMN.length).split(/[.[\s]/)[0])
176
+ .filter((member) => !known.has(member));
177
+ expect([...new Set(bad)]).toEqual([]);
178
+ });
179
+ it('never offers a document picker unless workflows were supplied', () => {
180
+ // Same reasoning as the api source above: `workflowId` is required, and a
181
+ // fabricated one produces a picker that looks right and resolves to
182
+ // nothing.
183
+ expect(createWorkflowPickerSchema()).toBeNull();
184
+ expect(createWorkflowPickerSchema({ workflows: [] })).toBeNull();
185
+ const offered = createWorkflowPickerSchema({
186
+ workflows: [{ id: 'wf-1', name: 'Purchase requisition' }],
187
+ });
188
+ expect(offered).not.toBeNull();
189
+ expect(JSON.stringify(s.toJsonSchema(offered))).toContain('wf-1');
190
+ });
191
+ });
@@ -0,0 +1 @@
1
+ export {};