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,105 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { CalculationFunctions } from '../../interfaces/formInput/calculationVariableInterface.js';
3
+ import { enumValues, exhaustive, optional } from '../internal/coupling.js';
4
+ /**
5
+ * The `calculatedFieldRules` member: a value computed from other fields rather
6
+ * than typed by the user.
7
+ *
8
+ * ## Why this is a draft
9
+ *
10
+ * `calculationVariableInterface` requires both `id` and `inputId` on every
11
+ * variable. Those are builder-assigned: `inputId` is the `id` of another input
12
+ * in the form, stamped when that input was created, and a model has no way to
13
+ * know it — the form it is authoring does not exist yet, so the ids do not
14
+ * either. Asked for one it will invent a plausible string, and the formula then
15
+ * references a field that is not there. The failure is quiet: the form saves,
16
+ * renders, and computes nothing.
17
+ *
18
+ * So the model binds by `formControlName`, which it genuinely does author, and
19
+ * the application resolves each one to that input's `id` on expansion. Both
20
+ * names are already on the interface — `formControlName` and
21
+ * `parentInputFormControl` sit alongside `inputId` and `parentInputId` — so the
22
+ * draft is not inventing a vocabulary. It fills in the half of each pair a
23
+ * model can actually know.
24
+ *
25
+ * ## The two ways a variable binds
26
+ *
27
+ * A variable reads EITHER one field's value directly, OR an aggregate over one
28
+ * column of a repeatable list. The second needs a set of members the first has
29
+ * no use for — `function`, `parentInputFormControl`, `applyFunctionToCol` — and
30
+ * Joi accepts them all on the same flat object, so nothing in the stored shape
31
+ * says which belong together. Offered flat, a model fills in
32
+ * `applyFunctionToCol` for a plain field and `function` for something with
33
+ * nothing to aggregate.
34
+ *
35
+ * They are therefore two branches of an `anyOf`, discriminated by an explicit
36
+ * `bindingType`. That key is NOT a member of `calculationVariableInterface`;
37
+ * like `endpointId` in `members/mat-options.ts` it exists only in the draft and
38
+ * is dropped on expansion. It earns the extra expansion step because an
39
+ * undiscriminated union of two objects sharing three of their keys is exactly
40
+ * the shape a model picks wrongly.
41
+ *
42
+ * The grouping is not invented here: `schemas/customValidationSchema.ts`
43
+ * annotates the same `parentInputId` + `function` pair as "Multiple-input
44
+ * (list) bindings", which is the distinction drawn.
45
+ */
46
+ /** `roundingMode` is a string union with no runtime enum to derive from. */
47
+ export const ROUNDING_MODE_VALUES = exhaustive()(['FLOOR', 'CEIL', 'ROUND']);
48
+ // --- schema -----------------------------------------------------------------
49
+ const variableToken = s.string('The name this variable is referenced by inside the formula. Keep it short and lower case, and spell it identically in the formula.', { pattern: '^[a-zA-Z][a-zA-Z0-9_]*$' });
50
+ const variableLabel = s.string('A human-readable name for this variable, shown when the formula is explained back to an administrator.');
51
+ const fieldVariableSchema = s.object("Reads one field's value directly.", {
52
+ bindingType: s.literal('field'),
53
+ variable: variableToken,
54
+ label: variableLabel,
55
+ formControlName: s.string('The formControlName of the field on this form supplying the value. It must be a field that actually exists in the form being authored.'),
56
+ });
57
+ const listVariableSchema = s.object('Aggregates one column across every row of a repeatable item list.', {
58
+ bindingType: s.literal('listAggregate'),
59
+ variable: variableToken,
60
+ label: variableLabel,
61
+ formControlName: s.string('The formControlName of the column being aggregated, as it is named inside the list.'),
62
+ parentInputFormControl: s.string('The formControlName of the item-list field whose rows are aggregated. It must be a multipleInput field in this form.'),
63
+ function: s.enumeration('How the column is reduced to a single value across the rows.', enumValues(CalculationFunctions)),
64
+ applyFunctionToCol: s.string('The column within each row the function is applied to.'),
65
+ applyFunctionToLabel: optional(s.string('Display label for the aggregated column.')),
66
+ filterValuesByCol: optional(s.string('Restricts the aggregate to rows matching a value in this column. Leave unset to aggregate every row.')),
67
+ filterValuesByThisColLabel: optional(s.string('Display label for the column the rows are filtered by.')),
68
+ tableCell: optional(s.boolean('Whether this variable resolves to a single table cell.')),
69
+ });
70
+ export const calculationVariableDraftSchema = s.anyOf([
71
+ fieldVariableSchema,
72
+ listVariableSchema,
73
+ ]);
74
+ /**
75
+ * The editor's applicability text for this row is deliberately NOT composed in.
76
+ *
77
+ * `describe(..., 'calculatedFieldRules')` resolves to "Only applies when
78
+ * isCalculatedField is true", which is correct in the settings panel and
79
+ * incoherent here: `variants.ts` encodes that rule as the shape of the union
80
+ * and drops `isCalculatedField` from the schema entirely, so the sentence would
81
+ * point a model at a member it is never shown — an invitation to invent one.
82
+ *
83
+ * This is the same judgement `internal/editor-guidance.ts` records for `label`
84
+ * and `options`: derived text earns its place by telling a model something it
85
+ * does not already have, and a rule the structure already makes unbreakable is
86
+ * not that.
87
+ */
88
+ export const calculatedFieldRulesDraftSchema = s.object('How this field computes its value from other fields on the form, so the user never types a figure the system can work out: a line total, VAT, an age. Whenever a total, average, minimum, maximum or count over an item list is used anywhere else — a decision gate, a validator, another formula, a list column, the next person — it must be a calculated field like this one on the form itself, aggregating the list; the roll-up shown beneath the list is for the user only.', {
89
+ formula: s.string('The arithmetic expression, written using the variable names declared below and nothing else. Every name in the formula must be declared as a variable, and every declared variable should appear in the formula.'),
90
+ variables: s.array('The fields the formula reads, one entry per variable name used in it.', calculationVariableDraftSchema, { minItems: 1 }),
91
+ decimalPlaces: optional(s.integer('How many decimal places the result is rounded to: 2 for money, 0 for a count.', { minimum: 0 })),
92
+ roundingMode: optional(s.enumeration("How the result is rounded at that precision: 'ROUND' to the nearest, which is right for money; 'FLOOR' or 'CEIL' only when the business rule says round down or up.", [...ROUNDING_MODE_VALUES])),
93
+ getFormulaFromAFormInput: optional(s.boolean('Whether the formula is read from another field on the form at runtime instead of being fixed here. Leave unset unless the form genuinely lets a user supply the formula.')),
94
+ formControlWithFormula: optional(s.string('The formControlName of the field supplying the formula. Only meaningful when the formula is read from a form input.')),
95
+ });
96
+ /**
97
+ * Members of a generated `calculatedFieldRules` the application must supply
98
+ * before Joi will accept it.
99
+ */
100
+ export const CALCULATED_FIELD_DRAFT_COMPLETION = [
101
+ 'calculatedFieldRules.variables[].id',
102
+ 'calculatedFieldRules.variables[].inputId (resolved from formControlName)',
103
+ 'calculatedFieldRules.variables[].parentInputId (resolved from parentInputFormControl)',
104
+ 'calculatedFieldRules.variables[].bindingType (draft-only; dropped on expansion)',
105
+ ];
@@ -0,0 +1,69 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import type { ConditionalInputRule } from '../../interfaces/formInput/ConditionalInputRule.js';
3
+ import { type ObservedInputDraft } from './validators.js';
4
+ /**
5
+ * The `conditionalInputConfig` member: when this control is shown at all.
6
+ *
7
+ * ## Only half of the interface is offered
8
+ *
9
+ * `ConditionalInputRule` carries two shapes at once. The `expression` half is
10
+ * read by the engine; `formControlName`, `label`, `testType` and `valueMatch`
11
+ * are a legacy descriptive half, marked `@deprecated` on every member and never
12
+ * read by the runtime. Only the expression half is modelled, for the reason
13
+ * `DEPRECATED_INPUT_MEMBERS` gives in `input-members.ts`: a deprecated property
14
+ * left in a generation schema comes back on every form the model writes, and
15
+ * these four would come back describing a rule that does nothing.
16
+ *
17
+ * The consequence is worth being explicit about, because it is the one case
18
+ * here where narrowing changes behaviour rather than just shape: a rule with no
19
+ * `expression` is inert. Since the deprecated members are the only thing a rule
20
+ * could otherwise carry, every rule this schema produces is an active one.
21
+ *
22
+ * ## Polarity and combination come from the interface
23
+ *
24
+ * `ConditionalInputRule`'s own documentation states two things a model cannot
25
+ * infer from the types and gets wrong by default:
26
+ *
27
+ * - `true` means the input is REVEALED — the opposite polarity to a custom
28
+ * validator's expression, where `true` means invalid. The two share a DSL and
29
+ * are trivially confused.
30
+ * - Multiple rules combine with AND, and an empty list means always visible.
31
+ *
32
+ * Both are restated in the descriptions rather than paraphrased loosely, on the
33
+ * same principle that `internal/editor-guidance.ts` harvests the builder's
34
+ * hints instead of rewriting them.
35
+ *
36
+ * ## Why this is a draft
37
+ *
38
+ * `id` is a builder-assigned rule identifier, and the observed inputs bind by
39
+ * `inputId`. Both are handled exactly as in `members/validators.ts` — the model
40
+ * binds by `formControlName` and the application resolves it.
41
+ */
42
+ export interface ConditionalRuleDraft {
43
+ expression: string;
44
+ inputsObservedForChanges: ObservedInputDraft[];
45
+ }
46
+ export declare const conditionalInputConfigDraftSchema: s.ArrayType<s.ObjectType<{
47
+ expression: s.StringType;
48
+ inputsObservedForChanges: s.ArrayType<s.ObjectType<{
49
+ formControlName: s.StringType;
50
+ variable: s.StringType;
51
+ parentInputFormControl: import("@hashbrownai/core/src/schema/base").SchemaForUnion<string | null>;
52
+ function: import("@hashbrownai/core/src/schema/base").SchemaForUnion<import("../../index.js").CalculationFunctions | null>;
53
+ }>>;
54
+ }>>;
55
+ /** The node produces the draft it declares. */
56
+ export type _NoConditionalDraftDrift = [
57
+ s.Infer<typeof conditionalInputConfigDraftSchema>
58
+ ] extends [ConditionalRuleDraft[]] ? never : ['conditionalInputConfigDraftSchema drifted from ConditionalRuleDraft[]'];
59
+ /**
60
+ * `expression` is carried through expansion untouched, so it stays pinned to
61
+ * the stored interface.
62
+ */
63
+ export type _ExpressionMatchesStoredShape = [
64
+ ConditionalRuleDraft['expression']
65
+ ] extends [NonNullable<ConditionalInputRule['expression']>] ? never : ['ConditionalRuleDraft.expression drifted from ConditionalInputRule'];
66
+ /**
67
+ * Members of a generated rule the application must supply.
68
+ */
69
+ export declare const CONDITIONAL_DRAFT_COMPLETION: readonly ["conditionalInputConfig[].id", "conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)", "conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)"];
@@ -0,0 +1,16 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { describe } from '../internal/editor-guidance.js';
3
+ import { observedInputSchema } from './validators.js';
4
+ const conditionalRuleSchema = s.object('One rule deciding whether this control is shown.', {
5
+ expression: s.string("An expression in the form engine's comparison language — NOT JavaScript — that is TRUE when this control should be SHOWN. Like a validator expression, it is true when the condition it names holds — here that condition is the reason to reveal the field. The grammar is a comparison `variable OP value`, with OP one of ===, !==, ==, !=, >, <, >=, <=, or a keyword operator includes, in, startsWith, endsWith or matches; a bare variable tests truthiness; ! negates; comparisons combine with && and || and group with parentheses; a variable may be a dotted path (reason.length > 15). There is no arithmetic, no function or method call and no ternary: to compare against a computed figure, make the figure a calculated field and compare against that field. A date is compared as an ISO string. Reference other fields only by the variable names declared below, compare against the stored option value rather than its label (urgency === 'urgent'), and put quotes around every string literal. For 'either of two conditions' write one rule with || inside it."),
6
+ inputsObservedForChanges: s.array('The fields the expression reads, one entry per variable name used in it.', observedInputSchema, { minItems: 1 }),
7
+ });
8
+ export const conditionalInputConfigDraftSchema = s.array(describe('Rules controlling when this control is visible: a follow-up question, an exception, a branch, a rejection reason when a decision toggle is off. Every rule must hold for it to be shown; leave empty to show it always. Never hide a field the engine reads.', 'conditionalInputConfig'), conditionalRuleSchema);
9
+ /**
10
+ * Members of a generated rule the application must supply.
11
+ */
12
+ export const CONDITIONAL_DRAFT_COMPLETION = [
13
+ 'conditionalInputConfig[].id',
14
+ 'conditionalInputConfig[].inputsObservedForChanges[].inputId (resolved from formControlName)',
15
+ 'conditionalInputConfig[].inputsObservedForChanges[].parentInputId (resolved from parentInputFormControl)',
16
+ ];
@@ -0,0 +1,182 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import type { AllFormInputPrimaryKeys } from '../../interfaces/FormBuilder/FormInputKeys.js';
3
+ import { type DocumentPickerFilterOperator, type DocumentPickerFilterValueType } from '../../interfaces/formInput/WorkflowDocumentPicker.js';
4
+ import { AssertNever } from '../internal/coupling.js';
5
+ import type { AvailableWorkflow } from './mat-options.js';
6
+ /**
7
+ * The `workflowPickerConfig` member: which workflow a document picker browses,
8
+ * and how that list is narrowed before the user is shown anything.
9
+ *
10
+ * ## Why this is a factory, and why it may decline to build a schema at all
11
+ *
12
+ * The one field an administrator must supply is `workflowId`, and it is a
13
+ * persisted id. A model asked for one emits a plausible hex string, and what
14
+ * comes back is a picker that renders, opens, and lists nothing — a failure
15
+ * that surfaces to an end user rather than to whoever generated the form.
16
+ *
17
+ * The builder never asks a human to type it either: the editor row for this
18
+ * property is a `WorkflowPicker`, which PICKS from the workflows the tenant
19
+ * actually has. So the model picks too. {@link createWorkflowPickerSchema}
20
+ * takes the workflows available in the caller's context and enumerates their
21
+ * ids, exactly as the `api` branch of `mat-options.ts` enumerates endpoint ids.
22
+ *
23
+ * With no workflows to enumerate, that reasoning goes one step further than it
24
+ * does there. In `mat-options` a missing endpoint list drops one BRANCH and the
25
+ * remaining sources still stand, so a schema is still worth offering. Here the
26
+ * unavailable field is the only required one, and every other member is scoped
27
+ * by it — a step id, a filter path, a document field all mean nothing until the
28
+ * workflow is known — so there is no reduced schema left to offer. The factory
29
+ * returns `null`, and the caller leaves `workflowPickerConfig` off the element
30
+ * rather than letting a model invent the one value that has to be real. That is
31
+ * also why this module exports no ready-made default schema: the no-workflows
32
+ * default is `null`, and an exported `null` reads as an oversight.
33
+ *
34
+ * ## Why this produces a draft
35
+ *
36
+ * `fromInputId` is stored as an input id — the runtime form group is keyed by
37
+ * input id, not by form-control name. A model authoring a form does not have
38
+ * those ids; it has just written the fields by NAME. So the draft carries the
39
+ * `formControlName`, the same trade `inputSourceId` makes in `mat-options.ts`,
40
+ * and the application resolves it against the form it was generated alongside
41
+ * before the config is stored. The member keeps its interface name so that
42
+ * resolution stays a substitution rather than a rename.
43
+ *
44
+ * The absent members are the second reason this is a draft: Skillet has no
45
+ * `optional()`, so every declared key is emitted and optionality is a `| null`
46
+ * branch. {@link WorkflowPickerConfigDraft} therefore describes what the model
47
+ * writes, not what `IWorkflowDocumentPickerConfig` stores, and stripping the
48
+ * nulls is part of the same expansion pass that resolves `fromInputId`.
49
+ *
50
+ * ## Deliberate omissions
51
+ *
52
+ * `primaryIdentifierKey` is `TreeNode[]` — nodes of the key tree the builder
53
+ * derives from a captured sample document. A model has no sample and therefore
54
+ * no tree to select from, and the behaviour it would be overriding (identify
55
+ * the transaction by `_id`) is already the right answer whenever it cannot.
56
+ *
57
+ * `sampleDocument` is captured from a real transaction when the workflow is
58
+ * chosen. It is evidence, not configuration; generated, it would be fiction
59
+ * that the key tree above is then built from.
60
+ *
61
+ * `availableSteps` is a cache of the chosen workflow's steps, refreshed by the
62
+ * builder whenever the workflow changes and never consulted at runtime — the
63
+ * step chooser always reads live steps. Generating it means inventing the very
64
+ * list the step filter is then checked against.
65
+ *
66
+ * A filter's `id` is stamped by the record-list editor the first time the
67
+ * filter is saved, and is the key that editor tracks a row by for edit and for
68
+ * delete. It is an identity, not a decision, so it is left to the editor that
69
+ * owns it.
70
+ *
71
+ * `value` is `unknown` on the interface and is narrowed here to a string,
72
+ * number or boolean — the three shapes `valueType` can actually coerce.
73
+ *
74
+ * ## Known gap: step ids
75
+ *
76
+ * `stepFilter.stepIds` is modelled, but the ids belong to the workflow and
77
+ * {@link AvailableWorkflow} carries only an id and a name, so nothing in this
78
+ * schema can enumerate them. The description says so plainly and tells the
79
+ * model to leave the step filter null unless the request itself names real step
80
+ * ids; an invented id narrows the picker to nothing, which is the same silent
81
+ * failure an invented `workflowId` produces. Widening `AvailableWorkflow` to
82
+ * carry its steps would close this properly, but that type is shared with
83
+ * `mat-options.ts` and is not changed from here.
84
+ */
85
+ export interface WorkflowPickerSchemaOptions {
86
+ /**
87
+ * Workflows the model may point the picker at. Omit, or pass an empty list,
88
+ * and {@link createWorkflowPickerSchema} produces no schema at all — there is
89
+ * no useful subset of this config that does not name a workflow.
90
+ */
91
+ workflows?: readonly AvailableWorkflow[];
92
+ }
93
+ /**
94
+ * The two value sources, addressed by meaning.
95
+ *
96
+ * Each branch of the filter union carries DIFFERENT members, so these literals
97
+ * cannot be taken positionally out of `DOCUMENT_PICKER_FILTER_VALUE_SOURCES`:
98
+ * reordering that array would quietly file `value` and `valueType` under the
99
+ * input-bound source, and produce filters that validate and never match.
100
+ * {@link exhaustive} is this codebase's answer to restating members safely —
101
+ * the union it checks against is itself read off that array, so adding a third
102
+ * source fails to compile here until it is given a branch of its own.
103
+ */
104
+ declare const FIXED_SOURCE: "fixed", INPUT_SOURCE: "input";
105
+ export interface WorkflowStepFilterDraft {
106
+ stepIds: string[];
107
+ }
108
+ /** A clause comparing a document field against a value fixed in the form. */
109
+ export interface FixedValueFilterDraft {
110
+ path: string;
111
+ op: DocumentPickerFilterOperator | null;
112
+ valueSource: typeof FIXED_SOURCE;
113
+ value: string | number | boolean;
114
+ valueType: DocumentPickerFilterValueType | null;
115
+ }
116
+ /** A clause comparing a document field against another field on this form. */
117
+ export interface InputValueFilterDraft {
118
+ path: string;
119
+ op: DocumentPickerFilterOperator | null;
120
+ valueSource: typeof INPUT_SOURCE;
121
+ /** A `formControlName`, which the application resolves to an input id. */
122
+ fromInputId: string;
123
+ omitWhenEmpty: boolean;
124
+ }
125
+ export type DocumentPickerFilterDraft = FixedValueFilterDraft | InputValueFilterDraft;
126
+ export interface WorkflowPickerConfigDraft {
127
+ workflowId: string;
128
+ stepFilter: WorkflowStepFilterDraft | null;
129
+ presetFilters: DocumentPickerFilterDraft[];
130
+ }
131
+ /**
132
+ * Builds the `workflowPickerConfig` schema over the workflows the caller has.
133
+ *
134
+ * @param options the workflows the model may choose between. Empty — the
135
+ * default — yields `null` rather than a schema whose only required field
136
+ * would have to be invented.
137
+ * @returns the schema, or `null` when there is no workflow to point it at.
138
+ */
139
+ export declare function createWorkflowPickerSchema(options?: WorkflowPickerSchemaOptions): s.ObjectType<{
140
+ workflowId: s.EnumType<string[]>;
141
+ stepFilter: import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
142
+ stepIds: string[];
143
+ } | null>;
144
+ presetFilters: s.ArrayType<import("@hashbrownai/core/src/schema/base").SchemaForUnion<{
145
+ path: string;
146
+ op: "eq" | "ne" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "regex" | "exists" | null;
147
+ valueSource: "fixed";
148
+ value: string | number | boolean;
149
+ valueType: "string" | "number" | "boolean" | null;
150
+ } | {
151
+ path: string;
152
+ op: "eq" | "ne" | "in" | "nin" | "gt" | "gte" | "lt" | "lte" | "regex" | "exists" | null;
153
+ valueSource: "input";
154
+ fromInputId: string;
155
+ omitWhenEmpty: boolean;
156
+ }>>;
157
+ }> | null;
158
+ /** The schema the factory builds, independent of which workflows it was given. */
159
+ type WorkflowPickerSchema = NonNullable<ReturnType<typeof createWorkflowPickerSchema>>;
160
+ /**
161
+ * The drafted node produces the draft it declares. Loosening a branch, or
162
+ * retyping a member of {@link WorkflowPickerConfigDraft}, surfaces here by key
163
+ * name — the guarantee `_NoDraftDrift` gives `matOptions` in
164
+ * `input-members.ts`, applied to the one member this module owns. What no
165
+ * assertion can check is the expansion from draft to stored config; that
166
+ * contract lives with the caller.
167
+ */
168
+ export type _NoWorkflowPickerDraftDrift = AssertNever<[
169
+ s.Infer<WorkflowPickerSchema>
170
+ ] extends [WorkflowPickerConfigDraft] ? never : AllFormInputPrimaryKeys.WorkflowPickerConfig>;
171
+ /**
172
+ * Members of a generated `workflowPickerConfig` the application must resolve.
173
+ *
174
+ * Only one, and it is not a Joi requirement — `presetFilters[].id` is optional
175
+ * there, so an unexpanded filter still validates. It is listed because it would
176
+ * still be WRONG: `fromInputId` is keyed by input id, and a draft carries a
177
+ * formControlName in that slot. A filter left unresolved passes validation and
178
+ * then matches nothing, which is the failure mode this whole layer is most
179
+ * concerned with.
180
+ */
181
+ export declare const WORKFLOW_PICKER_DRAFT_COMPLETION: readonly ["workflowPickerConfig.presetFilters[].fromInputId (resolved from formControlName)"];
182
+ export {};
@@ -0,0 +1,111 @@
1
+ import { s } from '@hashbrownai/core';
2
+ import { DOCUMENT_PICKER_FILTER_OPERATORS, DOCUMENT_PICKER_FILTER_VALUE_TYPES, } from '../../interfaces/formInput/WorkflowDocumentPicker.js';
3
+ import { exhaustive, optional } from '../internal/coupling.js';
4
+ import { describe, describeIn } from '../internal/editor-guidance.js';
5
+ // --- the value-source discriminator -----------------------------------------
6
+ /**
7
+ * The two value sources, addressed by meaning.
8
+ *
9
+ * Each branch of the filter union carries DIFFERENT members, so these literals
10
+ * cannot be taken positionally out of `DOCUMENT_PICKER_FILTER_VALUE_SOURCES`:
11
+ * reordering that array would quietly file `value` and `valueType` under the
12
+ * input-bound source, and produce filters that validate and never match.
13
+ * {@link exhaustive} is this codebase's answer to restating members safely —
14
+ * the union it checks against is itself read off that array, so adding a third
15
+ * source fails to compile here until it is given a branch of its own.
16
+ */
17
+ const [FIXED_SOURCE, INPUT_SOURCE] = exhaustive()([
18
+ 'fixed',
19
+ 'input',
20
+ ]);
21
+ // --- schema -----------------------------------------------------------------
22
+ /**
23
+ * The `deepBind` of the row that opens the preset-filter editor. The filter
24
+ * members below bind relative to the record that editor edits, so their
25
+ * guidance is looked up within it rather than at the top level — see
26
+ * `internal/editor-guidance.ts` for why the two indexes are kept apart.
27
+ */
28
+ const FILTER_EDITOR = ['workflowPickerConfig', 'presetFilters'];
29
+ // Shared by both branches, so declared once: a clause targets a field and
30
+ // compares it the same way whichever side supplies the value.
31
+ const filterPath = s.string(describeIn('The document field this clause compares. It must be a field the transactions of the chosen workflow actually carry — a field of their form, or one of the properties the document itself exposes.', FILTER_EDITOR, 'path'));
32
+ const filterOp = optional(s.enumeration(describeIn("How the field is compared. Leave it null for the default, which is 'eq'. 'in' and 'nin' read the fixed value as a comma-separated list, and 'exists' ignores the comparison entirely and only tests whether the field is present, so pair it with a boolean value.", FILTER_EDITOR, 'op'), [...DOCUMENT_PICKER_FILTER_OPERATORS]));
33
+ /**
34
+ * The fixed branch.
35
+ *
36
+ * Its value node is a union and so has no description slot of its own. The
37
+ * editor's guidance is composed onto the string branch, the only one the comma
38
+ * rule for `in` / `nin` can apply to; repeating it on the numeric and boolean
39
+ * branches would put the same sentence in one prompt three times for no gain,
40
+ * because the choice being made there is the type, not the format.
41
+ */
42
+ const fixedValueFilter = s.object(describeIn('Compares the field against a value written into the form definition itself. Use this for a restriction that is always true of this picker, such as only ever offering approved transactions.', FILTER_EDITOR, 'valueSource'), {
43
+ path: filterPath,
44
+ op: filterOp,
45
+ valueSource: s.literal(FIXED_SOURCE),
46
+ value: s.anyOf([
47
+ s.string(describeIn('The fixed value, as text. Write it as the field is stored, not as it is labelled on screen.', FILTER_EDITOR, 'value')),
48
+ s.number('The fixed value, as a number. Set valueType to number to match.'),
49
+ s.boolean('The fixed value, as a boolean. Set valueType to boolean to match.'),
50
+ ]),
51
+ valueType: optional(s.enumeration(describeIn("How the fixed value is read before it is compared. Leave it null for the default, which is 'string'.", FILTER_EDITOR, 'valueType'), [...DOCUMENT_PICKER_FILTER_VALUE_TYPES])),
52
+ });
53
+ /** The input-bound branch. */
54
+ const inputValueFilter = s.object(describeIn('Compares the field against whatever the user has entered in another field of this form, read when the picker is opened. Use this to tie the picker to a choice the user makes earlier in the same form.', FILTER_EDITOR, 'valueSource'), {
55
+ path: filterPath,
56
+ op: filterOp,
57
+ valueSource: s.literal(INPUT_SOURCE),
58
+ // Named for the interface member it becomes, but carrying a
59
+ // formControlName: the model knows the fields it just wrote by name and has
60
+ // no way to know the ids the runtime keys them by. The application performs
61
+ // that lookup — see the draft note in this file's header.
62
+ fromInputId: s.string(describeIn('The formControlName of the field on this form to take the value from. It must be a field that actually exists in the form being authored, and one the user reaches before this picker. The application resolves the name to that field’s input id.', FILTER_EDITOR, 'fromInputId')),
63
+ omitWhenEmpty: s.boolean(describeIn('Whether to drop this clause while that field is still empty. Prefer true: false filters on the empty value literally, so the user opens the picker, is shown nothing, and has no way to tell that the cause is a field further up the form.', FILTER_EDITOR, 'omitWhenEmpty')),
64
+ });
65
+ /**
66
+ * One preset filter, as a union over the two value sources.
67
+ *
68
+ * The exclusivity is real rather than conventional: the runtime uses one source
69
+ * and ignores the other, so a clause carrying both is not a richer clause — it
70
+ * is a clause half of whose configuration silently does nothing. Modelling it
71
+ * as a union makes the model choose the source BEFORE it is offered the members
72
+ * that source uses, instead of handing it a flat record of six optional keys
73
+ * and leaving it to infer which combinations mean anything.
74
+ */
75
+ const presetFilter = s.anyOf([fixedValueFilter, inputValueFilter]);
76
+ /**
77
+ * Builds the `workflowPickerConfig` schema over the workflows the caller has.
78
+ *
79
+ * @param options the workflows the model may choose between. Empty — the
80
+ * default — yields `null` rather than a schema whose only required field
81
+ * would have to be invented.
82
+ * @returns the schema, or `null` when there is no workflow to point it at.
83
+ */
84
+ export function createWorkflowPickerSchema(options = {}) {
85
+ const workflows = options.workflows ?? [];
86
+ if (!workflows.length)
87
+ return null;
88
+ // Rendered into prose so the choice is made on the workflow's meaning rather
89
+ // than on the shape of its id, which carries none.
90
+ const candidates = workflows.map((w) => `${w.id} (${w.name})`).join('; ');
91
+ return s.object('Which workflow a document picker browses, and how its list of transactions is narrowed before the user sees it. Set this only on a control that picks an existing transaction.', {
92
+ workflowId: s.enumeration(describe(`Whose transactions this picker lists. Everything else here is scoped by it. Choose by meaning, not by position: ${candidates}.`, 'workflowPickerConfig', 'workflowId'), workflows.map((w) => w.id)),
93
+ stepFilter: optional(s.object('Restricts the picker to transactions currently sitting on particular workflow steps.', {
94
+ stepIds: s.array(describe('Workflow step ids the picker is limited to. These ids belong to the workflow and are not listed here, so leave the whole step filter null unless the request names real ones — an id that does not exist narrows the picker to nothing, and nothing says so. An empty list means the same as no filter: every step is eligible, and the user chooses one while browsing.', 'workflowPickerConfig', 'stepFilter', 'stepIds'), s.string('A workflow step id, exactly as the workflow defines it.')),
95
+ })),
96
+ presetFilters: s.array(describe('Filters applied before the user searches, each one narrowing which transactions can be picked at all. Leave this empty unless a restriction was actually asked for: every clause here is one the user can neither see nor undo.', 'workflowPickerConfig', 'presetFilters'), presetFilter),
97
+ });
98
+ }
99
+ /**
100
+ * Members of a generated `workflowPickerConfig` the application must resolve.
101
+ *
102
+ * Only one, and it is not a Joi requirement — `presetFilters[].id` is optional
103
+ * there, so an unexpanded filter still validates. It is listed because it would
104
+ * still be WRONG: `fromInputId` is keyed by input id, and a draft carries a
105
+ * formControlName in that slot. A filter left unresolved passes validation and
106
+ * then matches nothing, which is the failure mode this whole layer is most
107
+ * concerned with.
108
+ */
109
+ export const WORKFLOW_PICKER_DRAFT_COMPLETION = [
110
+ 'workflowPickerConfig.presetFilters[].fromInputId (resolved from formControlName)',
111
+ ];