ngx-t-forms-types 0.0.30 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interfaces/Form/formSubmissionHandleInterface.d.ts +6 -0
- package/dist/interfaces/FormBuilder/DefaultEelement.js +76 -0
- package/dist/interfaces/FormBuilder/DefaultInputConfigInterface.d.ts +13 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.d.ts +6 -0
- package/dist/interfaces/FormBuilder/FormInputKeys.js +6 -0
- package/dist/interfaces/FormBuilder/inputConfig/ElementEditConfig.js +188 -3
- package/dist/interfaces/Import/ImportIdentity.d.ts +32 -0
- package/dist/interfaces/Import/ImportIdentity.js +1 -0
- package/dist/interfaces/Import/ImportProgress.d.ts +12 -1
- package/dist/interfaces/Import/ImportRowState.d.ts +52 -1
- package/dist/interfaces/Import/index.d.ts +2 -1
- package/dist/interfaces/formInput/APIDataFetchingConfigurationInterface.d.ts +34 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.d.ts +1 -0
- package/dist/interfaces/formInput/BasicFormInputInterface.js +1 -0
- package/dist/interfaces/formInput/IMscoaAccount.d.ts +19 -3
- package/dist/interfaces/formInput/ISelectInputInterface.d.ts +12 -0
- package/dist/interfaces/formInput/MultipleInterface.d.ts +10 -0
- package/dist/interfaces/formInput/WorkflowDocumentPicker.d.ts +2 -0
- package/dist/schemas/FormInputSchema.js +41 -2
- package/dist/schemas/MatOptionsSchema.js +7 -0
- package/dist/schemas/MscoaConfigSchema.js +1 -0
- package/dist/schemas/index.d.ts +2 -1
- package/dist/schemas/index.js +2 -1
- package/dist/skillet/authoring-rules.d.ts +41 -0
- package/dist/skillet/authoring-rules.js +61 -0
- package/dist/skillet/extra-members.d.ts +143 -0
- package/dist/skillet/extra-members.js +79 -0
- package/dist/skillet/form.d.ts +170 -0
- package/dist/skillet/form.js +139 -0
- package/dist/skillet/index.d.ts +43 -0
- package/dist/skillet/index.js +46 -0
- package/dist/skillet/input-members.d.ts +375 -0
- package/dist/skillet/input-members.js +186 -0
- package/dist/skillet/internal/coupling.d.ts +63 -0
- package/dist/skillet/internal/coupling.js +69 -0
- package/dist/skillet/internal/editor-guidance.d.ts +64 -0
- package/dist/skillet/internal/editor-guidance.js +291 -0
- package/dist/skillet/members/calculated-field.d.ts +155 -0
- package/dist/skillet/members/calculated-field.js +105 -0
- package/dist/skillet/members/conditional.d.ts +69 -0
- package/dist/skillet/members/conditional.js +16 -0
- package/dist/skillet/members/document-picker.d.ts +182 -0
- package/dist/skillet/members/document-picker.js +111 -0
- package/dist/skillet/members/mat-options.d.ts +187 -0
- package/dist/skillet/members/mat-options.js +92 -0
- package/dist/skillet/members/mscoa.d.ts +182 -0
- package/dist/skillet/members/mscoa.js +129 -0
- package/dist/skillet/members/pagination.d.ts +59 -0
- package/dist/skillet/members/pagination.js +16 -0
- package/dist/skillet/members/table.d.ts +178 -0
- package/dist/skillet/members/table.js +105 -0
- package/dist/skillet/members/validators.d.ts +197 -0
- package/dist/skillet/members/validators.js +74 -0
- package/dist/skillet/members/value.d.ts +154 -0
- package/dist/skillet/members/value.js +97 -0
- package/dist/skillet/shared-types.d.ts +57 -0
- package/dist/skillet/shared-types.js +69 -0
- package/dist/skillet/tests/house-rules.spec.d.ts +1 -0
- package/dist/skillet/tests/house-rules.spec.js +162 -0
- package/dist/skillet/tests/skillet-coupling.spec.d.ts +1 -0
- package/dist/skillet/tests/skillet-coupling.spec.js +191 -0
- package/dist/skillet/tests/variant-assembly.spec.d.ts +1 -0
- package/dist/skillet/tests/variant-assembly.spec.js +643 -0
- package/dist/skillet/variants.d.ts +138 -0
- package/dist/skillet/variants.js +536 -0
- package/dist/skillet/workflow-context.d.ts +95 -0
- package/dist/skillet/workflow-context.js +128 -0
- package/package.json +78 -60
|
@@ -0,0 +1,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
|
+
];
|