@astroapps/forms-core 1.0.0
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/TODO.txt +3 -0
- package/lib/controlDefinition.d.ts +436 -0
- package/lib/defaultSchemaInterface.d.ts +28 -0
- package/lib/entityExpression.d.ts +33 -0
- package/lib/evalExpression.d.ts +15 -0
- package/lib/formNode.d.ts +45 -0
- package/lib/formState.d.ts +44 -0
- package/lib/index.cjs +1813 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +1521 -0
- package/lib/index.js.map +1 -0
- package/lib/schemaBuilder.d.ts +67 -0
- package/lib/schemaDataNode.d.ts +36 -0
- package/lib/schemaField.d.ts +122 -0
- package/lib/schemaInterface.d.ts +102 -0
- package/lib/schemaNode.d.ts +54 -0
- package/lib/schemaValidator.d.ts +27 -0
- package/lib/util.d.ts +14 -0
- package/lib/validators.d.ts +23 -0
- package/package.json +60 -0
- package/src/controlDefinition.ts +704 -0
- package/src/defaultSchemaInterface.ts +201 -0
- package/src/entityExpression.ts +39 -0
- package/src/evalExpression.ts +118 -0
- package/src/formNode.ts +249 -0
- package/src/formState.ts +491 -0
- package/src/index.ts +12 -0
- package/src/schemaBuilder.ts +318 -0
- package/src/schemaDataNode.ts +188 -0
- package/src/schemaField.ts +155 -0
- package/src/schemaInterface.ts +135 -0
- package/src/schemaNode.ts +285 -0
- package/src/schemaValidator.ts +32 -0
- package/src/util.ts +50 -0
- package/src/validators.ts +220 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EqualityFunc,
|
|
3
|
+
FieldOption,
|
|
4
|
+
FieldType,
|
|
5
|
+
SchemaField,
|
|
6
|
+
ValidationMessageType,
|
|
7
|
+
} from "./schemaField";
|
|
8
|
+
import { SchemaInterface } from "./schemaInterface";
|
|
9
|
+
import { SchemaDataNode } from "./schemaDataNode";
|
|
10
|
+
import { SchemaNode } from "./schemaNode";
|
|
11
|
+
import { Control, ControlSetup } from "@astroapps/controls";
|
|
12
|
+
|
|
13
|
+
export class DefaultSchemaInterface implements SchemaInterface {
|
|
14
|
+
constructor(
|
|
15
|
+
protected boolStrings: [string, string] = ["No", "Yes"],
|
|
16
|
+
protected parseDateTime: (s: string) => number = (s) => Date.parse(s),
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
parseToMillis(field: SchemaField, v: string): number {
|
|
20
|
+
return this.parseDateTime(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
validationMessageText(
|
|
24
|
+
field: SchemaField,
|
|
25
|
+
messageType: ValidationMessageType,
|
|
26
|
+
actual: any,
|
|
27
|
+
expected: any,
|
|
28
|
+
): string {
|
|
29
|
+
switch (messageType) {
|
|
30
|
+
case ValidationMessageType.NotEmpty:
|
|
31
|
+
return "Please enter a value";
|
|
32
|
+
case ValidationMessageType.MinLength:
|
|
33
|
+
return "Length must be at least " + expected;
|
|
34
|
+
case ValidationMessageType.MaxLength:
|
|
35
|
+
return "Length must be less than " + expected;
|
|
36
|
+
case ValidationMessageType.NotBeforeDate:
|
|
37
|
+
return `Date must not be before ${new Date(expected).toDateString()}`;
|
|
38
|
+
case ValidationMessageType.NotAfterDate:
|
|
39
|
+
return `Date must not be after ${new Date(expected).toDateString()}`;
|
|
40
|
+
default:
|
|
41
|
+
return "Unknown error";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDataOptions(node: SchemaDataNode): FieldOption[] | null | undefined {
|
|
46
|
+
return this.getNodeOptions(node.schema);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getNodeOptions(node: SchemaNode): FieldOption[] | null | undefined {
|
|
50
|
+
return this.getOptions(node.field);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getOptions({ options }: SchemaField): FieldOption[] | null | undefined {
|
|
54
|
+
return options && options.length > 0 ? options : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getFilterOptions(
|
|
58
|
+
array: SchemaDataNode,
|
|
59
|
+
field: SchemaNode,
|
|
60
|
+
): FieldOption[] | undefined | null {
|
|
61
|
+
return this.getNodeOptions(field);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isEmptyValue(f: SchemaField, value: any): boolean {
|
|
65
|
+
if (f.collection)
|
|
66
|
+
return Array.isArray(value) ? value.length === 0 : value == null;
|
|
67
|
+
switch (f.type) {
|
|
68
|
+
case FieldType.String:
|
|
69
|
+
case FieldType.DateTime:
|
|
70
|
+
case FieldType.Date:
|
|
71
|
+
case FieldType.Time:
|
|
72
|
+
return !value;
|
|
73
|
+
default:
|
|
74
|
+
return value == null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
searchText(field: SchemaField, value: any): string {
|
|
79
|
+
return this.textValue(field, value)?.toLowerCase() ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
textValueForData(dataNode: SchemaDataNode): string | undefined {
|
|
83
|
+
const options = this.getDataOptions(dataNode);
|
|
84
|
+
return this.textValue(
|
|
85
|
+
dataNode.schema.field,
|
|
86
|
+
dataNode.control.value,
|
|
87
|
+
dataNode.elementIndex != null,
|
|
88
|
+
options,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
textValue(
|
|
92
|
+
field: SchemaField,
|
|
93
|
+
value: any,
|
|
94
|
+
element?: boolean | undefined,
|
|
95
|
+
options?: FieldOption[] | null,
|
|
96
|
+
): string | undefined {
|
|
97
|
+
const actualOptions = options ?? this.getOptions(field);
|
|
98
|
+
const option = actualOptions?.find((x) => x.value === value);
|
|
99
|
+
if (option) return option.name;
|
|
100
|
+
switch (field.type) {
|
|
101
|
+
case FieldType.Date:
|
|
102
|
+
return value ? new Date(value).toLocaleDateString() : undefined;
|
|
103
|
+
case FieldType.DateTime:
|
|
104
|
+
return value
|
|
105
|
+
? new Date(this.parseToMillis(field, value)).toLocaleString()
|
|
106
|
+
: undefined;
|
|
107
|
+
case FieldType.Time:
|
|
108
|
+
return value
|
|
109
|
+
? new Date("1970-01-01T" + value).toLocaleTimeString()
|
|
110
|
+
: undefined;
|
|
111
|
+
case FieldType.Bool:
|
|
112
|
+
return this.boolStrings[value ? 1 : 0];
|
|
113
|
+
default:
|
|
114
|
+
return value != null ? value.toString() : undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
controlLength(f: SchemaField, control: Control<any>): number {
|
|
119
|
+
return f.collection
|
|
120
|
+
? (control.elements?.length ?? 0)
|
|
121
|
+
: this.valueLength(f, control.value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
valueLength(field: SchemaField, value: any): number {
|
|
125
|
+
return (value && value?.length) ?? 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
compareValue(field: SchemaField, v1: unknown, v2: unknown): number {
|
|
129
|
+
if (v1 == null) return v2 == null ? 0 : 1;
|
|
130
|
+
if (v2 == null) return -1;
|
|
131
|
+
switch (field.type) {
|
|
132
|
+
case FieldType.Date:
|
|
133
|
+
case FieldType.DateTime:
|
|
134
|
+
case FieldType.Time:
|
|
135
|
+
case FieldType.String:
|
|
136
|
+
return (v1 as string).localeCompare(v2 as string);
|
|
137
|
+
case FieldType.Bool:
|
|
138
|
+
return (v1 as boolean) ? ((v2 as boolean) ? 0 : 1) : -1;
|
|
139
|
+
case FieldType.Int:
|
|
140
|
+
case FieldType.Double:
|
|
141
|
+
return (v1 as number) - (v2 as number);
|
|
142
|
+
default:
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
compoundFieldSetup(f: SchemaNode): [string, ControlSetup<any>][] {
|
|
148
|
+
return f.getChildNodes().map((x) => {
|
|
149
|
+
const { field } = x.field;
|
|
150
|
+
return [field, this.makeControlSetup(x)];
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
compoundFieldEquality(f: SchemaNode): [string, EqualityFunc][] {
|
|
155
|
+
return f.getChildNodes().map((x) => {
|
|
156
|
+
const { field } = x.field;
|
|
157
|
+
return [field, (a, b) => this.makeEqualityFunc(x)(a[field], b[field])];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
makeEqualityFunc(field: SchemaNode, element?: boolean): EqualityFunc {
|
|
162
|
+
if (field.field.collection && !element) {
|
|
163
|
+
const elemEqual = this.makeEqualityFunc(field, true);
|
|
164
|
+
return (a, b) => {
|
|
165
|
+
if (a === b) return true;
|
|
166
|
+
if (a == null || b == null) return false;
|
|
167
|
+
if (a.length !== b.length) return false;
|
|
168
|
+
for (let i = 0; i < a.length; i++) {
|
|
169
|
+
if (!elemEqual(a[i], b[i])) return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
switch (field.field.type) {
|
|
175
|
+
case FieldType.Compound:
|
|
176
|
+
const allChecks = this.compoundFieldEquality(field);
|
|
177
|
+
return (a, b) =>
|
|
178
|
+
a === b ||
|
|
179
|
+
(a != null && b != null && allChecks.every((x) => x[1](a, b)));
|
|
180
|
+
default:
|
|
181
|
+
return (a, b) => a === b;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
makeControlSetup(field: SchemaNode, element?: boolean): ControlSetup<any> {
|
|
185
|
+
let setup: ControlSetup<any> = {
|
|
186
|
+
equals: this.makeEqualityFunc(field, element),
|
|
187
|
+
};
|
|
188
|
+
if (field.field.collection && !element) {
|
|
189
|
+
setup.elems = this.makeControlSetup(field, true);
|
|
190
|
+
return setup;
|
|
191
|
+
}
|
|
192
|
+
switch (field.field.type) {
|
|
193
|
+
case FieldType.Compound:
|
|
194
|
+
setup.fields = Object.fromEntries(this.compoundFieldSetup(field));
|
|
195
|
+
}
|
|
196
|
+
return setup;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const defaultSchemaInterface: SchemaInterface =
|
|
201
|
+
new DefaultSchemaInterface();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface EntityExpression {
|
|
2
|
+
type: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export enum ExpressionType {
|
|
6
|
+
Jsonata = "Jsonata",
|
|
7
|
+
Data = "Data",
|
|
8
|
+
DataMatch = "FieldValue",
|
|
9
|
+
UserMatch = "UserMatch",
|
|
10
|
+
NotEmpty = "NotEmpty",
|
|
11
|
+
UUID = "UUID",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonataExpression extends EntityExpression {
|
|
15
|
+
type: ExpressionType.Jsonata;
|
|
16
|
+
expression: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DataExpression extends EntityExpression {
|
|
20
|
+
type: ExpressionType.Data;
|
|
21
|
+
field: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DataMatchExpression extends EntityExpression {
|
|
25
|
+
type: ExpressionType.DataMatch;
|
|
26
|
+
field: string;
|
|
27
|
+
value: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NotEmptyExpression extends EntityExpression {
|
|
31
|
+
type: ExpressionType.NotEmpty;
|
|
32
|
+
field: string;
|
|
33
|
+
empty?: boolean | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UserMatchExpression extends EntityExpression {
|
|
37
|
+
type: ExpressionType.UserMatch;
|
|
38
|
+
userMatch: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataExpression,
|
|
3
|
+
DataMatchExpression,
|
|
4
|
+
EntityExpression,
|
|
5
|
+
ExpressionType,
|
|
6
|
+
JsonataExpression,
|
|
7
|
+
NotEmptyExpression,
|
|
8
|
+
} from "./entityExpression";
|
|
9
|
+
import {
|
|
10
|
+
AsyncEffect,
|
|
11
|
+
CleanupScope,
|
|
12
|
+
collectChanges,
|
|
13
|
+
createAsyncEffect,
|
|
14
|
+
createSyncEffect,
|
|
15
|
+
trackedValue,
|
|
16
|
+
Value,
|
|
17
|
+
} from "@astroapps/controls";
|
|
18
|
+
import { schemaDataForFieldRef, SchemaDataNode } from "./schemaDataNode";
|
|
19
|
+
import { SchemaInterface } from "./schemaInterface";
|
|
20
|
+
import jsonata from "jsonata";
|
|
21
|
+
import { getJsonPath, getRootDataNode } from "./controlDefinition";
|
|
22
|
+
import { v4 as uuidv4 } from "uuid";
|
|
23
|
+
import { createScopedComputed, jsonPathString } from "./util";
|
|
24
|
+
|
|
25
|
+
export interface ExpressionEvalContext {
|
|
26
|
+
scope: CleanupScope;
|
|
27
|
+
returnResult: (k: unknown) => void;
|
|
28
|
+
dataNode: SchemaDataNode;
|
|
29
|
+
schemaInterface: SchemaInterface;
|
|
30
|
+
variables?: Value<Record<string, any> | undefined>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ExpressionEval<T extends EntityExpression> = (
|
|
34
|
+
expr: T,
|
|
35
|
+
context: ExpressionEvalContext,
|
|
36
|
+
) => void;
|
|
37
|
+
|
|
38
|
+
const dataEval: ExpressionEval<DataExpression> = (
|
|
39
|
+
fvExpr,
|
|
40
|
+
{ dataNode: node, returnResult, scope },
|
|
41
|
+
) => {
|
|
42
|
+
createSyncEffect(() => {
|
|
43
|
+
const otherField = schemaDataForFieldRef(fvExpr.field, node);
|
|
44
|
+
returnResult(otherField.control?.value);
|
|
45
|
+
}, scope);
|
|
46
|
+
};
|
|
47
|
+
const dataMatchEval: ExpressionEval<DataMatchExpression> = (
|
|
48
|
+
matchExpr,
|
|
49
|
+
{ dataNode, returnResult, scope },
|
|
50
|
+
) => {
|
|
51
|
+
createSyncEffect(() => {
|
|
52
|
+
const otherField = schemaDataForFieldRef(matchExpr.field, dataNode);
|
|
53
|
+
const fv = otherField?.control.value;
|
|
54
|
+
returnResult(
|
|
55
|
+
Array.isArray(fv) ? fv.includes(matchExpr.value) : fv === matchExpr.value,
|
|
56
|
+
);
|
|
57
|
+
}, scope);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const notEmptyEval: ExpressionEval<NotEmptyExpression> = (
|
|
61
|
+
expr,
|
|
62
|
+
{ returnResult, dataNode, scope, schemaInterface },
|
|
63
|
+
) => {
|
|
64
|
+
createSyncEffect(() => {
|
|
65
|
+
const otherField = schemaDataForFieldRef(expr.field, dataNode);
|
|
66
|
+
const fv = otherField.control?.value;
|
|
67
|
+
const field = otherField.schema.field;
|
|
68
|
+
const empty = !!expr.empty;
|
|
69
|
+
returnResult(field && empty === schemaInterface.isEmptyValue(field, fv));
|
|
70
|
+
}, scope);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const jsonataEval: ExpressionEval<JsonataExpression> = (
|
|
74
|
+
expr,
|
|
75
|
+
{ scope, returnResult, dataNode, variables },
|
|
76
|
+
) => {
|
|
77
|
+
const path = getJsonPath(dataNode);
|
|
78
|
+
const pathString = jsonPathString(path, (x) => `#$i[${x}]`);
|
|
79
|
+
const rootData = getRootDataNode(dataNode).control;
|
|
80
|
+
|
|
81
|
+
const parsedJsonata = createScopedComputed(scope, () => {
|
|
82
|
+
const jExpr = expr.expression;
|
|
83
|
+
const fullExpr = pathString ? pathString + ".(" + jExpr + ")" : jExpr;
|
|
84
|
+
try {
|
|
85
|
+
return { expr: jsonata(fullExpr ? fullExpr : "null"), fullExpr };
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error(e);
|
|
88
|
+
return { expr: jsonata("null"), fullExpr };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
async function runJsonata(effect: AsyncEffect<any>, signal: AbortSignal) {
|
|
93
|
+
const bindings = collectChanges(
|
|
94
|
+
effect.collectUsage,
|
|
95
|
+
() => variables?.value,
|
|
96
|
+
);
|
|
97
|
+
const evalResult = await parsedJsonata.fields.expr.value.evaluate(
|
|
98
|
+
trackedValue(rootData, effect.collectUsage),
|
|
99
|
+
bindings,
|
|
100
|
+
);
|
|
101
|
+
// console.log(parsedJsonata.fields.fullExpr.value, evalResult, bindings);
|
|
102
|
+
collectChanges(effect.collectUsage, () => returnResult(evalResult));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
createAsyncEffect(runJsonata, scope);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const uuidEval: ExpressionEval<EntityExpression> = (_, ctx) => {
|
|
109
|
+
ctx.returnResult(uuidv4());
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const defaultEvaluators: Record<string, ExpressionEval<any>> = {
|
|
113
|
+
[ExpressionType.DataMatch]: dataMatchEval,
|
|
114
|
+
[ExpressionType.Data]: dataEval,
|
|
115
|
+
[ExpressionType.NotEmpty]: notEmptyEval,
|
|
116
|
+
[ExpressionType.Jsonata]: jsonataEval,
|
|
117
|
+
[ExpressionType.UUID]: uuidEval,
|
|
118
|
+
};
|
package/src/formNode.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ControlDefinition,
|
|
3
|
+
ControlDefinitionType,
|
|
4
|
+
DataControlDefinition,
|
|
5
|
+
GroupedControlsDefinition,
|
|
6
|
+
GroupRenderType,
|
|
7
|
+
isDataControl,
|
|
8
|
+
isGroupControl,
|
|
9
|
+
} from "./controlDefinition";
|
|
10
|
+
import { schemaDataForFieldPath, SchemaDataNode } from "./schemaDataNode";
|
|
11
|
+
|
|
12
|
+
export type ControlMap = { [k: string]: ControlDefinition };
|
|
13
|
+
|
|
14
|
+
export class FormNode {
|
|
15
|
+
constructor(
|
|
16
|
+
public id: string,
|
|
17
|
+
public definition: ControlDefinition,
|
|
18
|
+
public tree: FormTree,
|
|
19
|
+
public parent?: FormNode,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
visit<A>(visitFn: (n: FormNode) => A | undefined): A | undefined {
|
|
23
|
+
const res = visitFn(this);
|
|
24
|
+
if (res !== undefined) return res;
|
|
25
|
+
const children = this.getUnresolvedChildNodes();
|
|
26
|
+
for (const child of children) {
|
|
27
|
+
const res = child.visit(visitFn);
|
|
28
|
+
if (res !== undefined) return res;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getResolvedChildren(): ControlDefinition[] {
|
|
34
|
+
const childRefId = this.definition.childRefId;
|
|
35
|
+
const parent = childRefId ? this.tree.getByRefId(childRefId) : undefined;
|
|
36
|
+
return (parent ?? this.definition)?.children ?? [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
createChildNode(childId: string, childDef: ControlDefinition) {
|
|
40
|
+
return new FormNode(
|
|
41
|
+
this.tree.getChildId(this.id, childId, childDef),
|
|
42
|
+
childDef,
|
|
43
|
+
this.tree,
|
|
44
|
+
this,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getChildNodes(): FormNode[] {
|
|
49
|
+
const resolved = this.getResolvedChildren();
|
|
50
|
+
return resolved.map((x, i) => this.createChildNode(i.toString(), x));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getUnresolvedChildNodes(): FormNode[] {
|
|
54
|
+
return (
|
|
55
|
+
this.definition.children?.map((x, i) =>
|
|
56
|
+
this.createChildNode(i.toString(), x),
|
|
57
|
+
) ?? []
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FormTreeLookup {
|
|
63
|
+
getForm(formId: string): FormTree | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export abstract class FormTree implements FormTreeLookup {
|
|
67
|
+
abstract rootNode: FormNode;
|
|
68
|
+
|
|
69
|
+
abstract getByRefId(id: string): ControlDefinition | undefined;
|
|
70
|
+
|
|
71
|
+
abstract getForm(formId: string): FormTree | undefined;
|
|
72
|
+
|
|
73
|
+
getChildId(
|
|
74
|
+
parentId: string,
|
|
75
|
+
childId: string,
|
|
76
|
+
control: ControlDefinition,
|
|
77
|
+
): string {
|
|
78
|
+
return parentId + "/" + childId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getControlIds(
|
|
83
|
+
definition: ControlDefinition,
|
|
84
|
+
): [string, ControlDefinition][] {
|
|
85
|
+
const childEntries = definition.children?.flatMap(getControlIds) ?? [];
|
|
86
|
+
return !definition.id
|
|
87
|
+
? childEntries
|
|
88
|
+
: [[definition.id, definition], ...childEntries];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createControlMap(control: ControlDefinition): ControlMap {
|
|
92
|
+
return Object.fromEntries(getControlIds(control));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class FormTreeImpl extends FormTree {
|
|
96
|
+
controlMap: ControlMap;
|
|
97
|
+
rootNode: FormNode;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
private forms: FormTreeLookup,
|
|
101
|
+
root: ControlDefinition,
|
|
102
|
+
) {
|
|
103
|
+
super();
|
|
104
|
+
this.rootNode = new FormNode("", root, this);
|
|
105
|
+
this.controlMap = createControlMap(root);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getByRefId(id: string): ControlDefinition | undefined {
|
|
109
|
+
return this.controlMap[id];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getForm(formId: string): FormTree | undefined {
|
|
113
|
+
return this.forms.getForm(formId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function legacyFormNode(definition: ControlDefinition) {
|
|
118
|
+
return createFormTree([definition]).rootNode.getChildNodes()[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createFormTree(
|
|
122
|
+
controls: ControlDefinition[],
|
|
123
|
+
getForm: FormTreeLookup = { getForm: () => undefined },
|
|
124
|
+
): FormTree {
|
|
125
|
+
return new FormTreeImpl(getForm, {
|
|
126
|
+
type: ControlDefinitionType.Group,
|
|
127
|
+
children: controls,
|
|
128
|
+
groupOptions: {
|
|
129
|
+
type: GroupRenderType.Standard,
|
|
130
|
+
hideTitle: true,
|
|
131
|
+
},
|
|
132
|
+
} as GroupedControlsDefinition);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createFormLookup<A extends Record<string, ControlDefinition[]>>(
|
|
136
|
+
formMap: A,
|
|
137
|
+
): {
|
|
138
|
+
getForm(formId: keyof A): FormTree;
|
|
139
|
+
} {
|
|
140
|
+
const lookup = {
|
|
141
|
+
getForm,
|
|
142
|
+
};
|
|
143
|
+
const forms = Object.fromEntries(
|
|
144
|
+
Object.entries(formMap).map(([k, v]) => [k, createFormTree(v, lookup)]),
|
|
145
|
+
);
|
|
146
|
+
return lookup;
|
|
147
|
+
|
|
148
|
+
function getForm(formId: keyof A): FormTree {
|
|
149
|
+
return forms[formId as string];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function fieldPathForDefinition(
|
|
154
|
+
c: ControlDefinition,
|
|
155
|
+
): string[] | undefined {
|
|
156
|
+
const fieldName = isGroupControl(c)
|
|
157
|
+
? c.compoundField
|
|
158
|
+
: isDataControl(c)
|
|
159
|
+
? c.field
|
|
160
|
+
: undefined;
|
|
161
|
+
return fieldName?.split("/");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function lookupDataNode(
|
|
165
|
+
c: ControlDefinition,
|
|
166
|
+
parentNode: SchemaDataNode,
|
|
167
|
+
) {
|
|
168
|
+
const fieldNamePath = fieldPathForDefinition(c);
|
|
169
|
+
return fieldNamePath
|
|
170
|
+
? schemaDataForFieldPath(fieldNamePath, parentNode)
|
|
171
|
+
: undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated use visitFormNodeData instead
|
|
176
|
+
*/
|
|
177
|
+
export function visitControlDataArray<A>(
|
|
178
|
+
controls: ControlDefinition[] | undefined | null,
|
|
179
|
+
context: SchemaDataNode,
|
|
180
|
+
cb: (
|
|
181
|
+
definition: DataControlDefinition,
|
|
182
|
+
node: SchemaDataNode,
|
|
183
|
+
) => A | undefined,
|
|
184
|
+
): A | undefined {
|
|
185
|
+
if (!controls) return undefined;
|
|
186
|
+
for (const c of controls) {
|
|
187
|
+
const r = visitControlData(c, context, cb);
|
|
188
|
+
if (r !== undefined) return r;
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @deprecated use visitFormDataInContext instead
|
|
195
|
+
*/
|
|
196
|
+
export function visitControlData<A>(
|
|
197
|
+
definition: ControlDefinition,
|
|
198
|
+
ctx: SchemaDataNode,
|
|
199
|
+
cb: (
|
|
200
|
+
definition: DataControlDefinition,
|
|
201
|
+
field: SchemaDataNode,
|
|
202
|
+
) => A | undefined,
|
|
203
|
+
): A | undefined {
|
|
204
|
+
return visitFormDataInContext(ctx, legacyFormNode(definition), (n, d) =>
|
|
205
|
+
cb(d, n),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type ControlDataVisitor<A> = (
|
|
210
|
+
dataNode: SchemaDataNode,
|
|
211
|
+
definition: DataControlDefinition,
|
|
212
|
+
) => A | undefined;
|
|
213
|
+
|
|
214
|
+
export function visitFormData<A>(
|
|
215
|
+
node: FormNode,
|
|
216
|
+
dataNode: SchemaDataNode,
|
|
217
|
+
cb: ControlDataVisitor<A>,
|
|
218
|
+
notSelf?: boolean,
|
|
219
|
+
): A | undefined {
|
|
220
|
+
const def = node.definition;
|
|
221
|
+
const result = !notSelf && isDataControl(def) ? cb(dataNode, def) : undefined;
|
|
222
|
+
if (result !== undefined) return result;
|
|
223
|
+
if (dataNode.elementIndex == null && dataNode.schema.field.collection) {
|
|
224
|
+
const l = dataNode.control.elements.length;
|
|
225
|
+
for (let i = 0; i < l; i++) {
|
|
226
|
+
const elemChild = dataNode.getChildElement(i);
|
|
227
|
+
const elemResult = visitFormData(node, elemChild, cb);
|
|
228
|
+
if (elemResult !== undefined) return elemResult;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
if (dataNode.control.isNull) return undefined;
|
|
233
|
+
const children = node.getChildNodes();
|
|
234
|
+
const l = children.length;
|
|
235
|
+
for (let i = 0; i < l; i++) {
|
|
236
|
+
const elemResult = visitFormDataInContext(dataNode, children[i], cb);
|
|
237
|
+
if (elemResult !== undefined) return elemResult;
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function visitFormDataInContext<A>(
|
|
243
|
+
parentContext: SchemaDataNode,
|
|
244
|
+
node: FormNode,
|
|
245
|
+
cb: ControlDataVisitor<A>,
|
|
246
|
+
): A | undefined {
|
|
247
|
+
const dataNode = lookupDataNode(node.definition, parentContext);
|
|
248
|
+
return visitFormData(node, dataNode ?? parentContext, cb, !dataNode);
|
|
249
|
+
}
|