@fogpipe/forma-core 0.15.0 → 0.17.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/dist/{chunk-COAUJ3IL.js → chunk-K7STGGGJ.js} +81 -1
- package/dist/chunk-K7STGGGJ.js.map +1 -0
- package/dist/engine/index.cjs +78 -0
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/validate.d.ts.map +1 -1
- package/dist/engine/visibility.d.ts.map +1 -1
- package/dist/index.cjs +81 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/types.d.ts +46 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/matrix.test.ts +379 -0
- package/src/engine/validate.ts +97 -0
- package/src/engine/visibility.ts +26 -1
- package/src/index.ts +4 -0
- package/src/types.ts +58 -3
- package/dist/chunk-COAUJ3IL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isDisplayField,
|
|
20
20
|
isEnabled,
|
|
21
21
|
isFieldVisible,
|
|
22
|
+
isMatrixField,
|
|
22
23
|
isReadonly,
|
|
23
24
|
isRequired,
|
|
24
25
|
isSelectionField,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
parseDecimalFormat,
|
|
27
28
|
validate,
|
|
28
29
|
validateSingleField
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-K7STGGGJ.js";
|
|
30
31
|
import {
|
|
31
32
|
evaluate,
|
|
32
33
|
evaluateBoolean,
|
|
@@ -62,6 +63,7 @@ export {
|
|
|
62
63
|
isDisplayField,
|
|
63
64
|
isEnabled,
|
|
64
65
|
isFieldVisible,
|
|
66
|
+
isMatrixField,
|
|
65
67
|
isReadonly,
|
|
66
68
|
isRequired,
|
|
67
69
|
isSelectionField,
|
package/dist/types.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface JSONSchemaString extends JSONSchemaBase {
|
|
|
51
51
|
minLength?: number;
|
|
52
52
|
maxLength?: number;
|
|
53
53
|
pattern?: string;
|
|
54
|
-
format?: "date" | "date-time" | "email" | "uri" | "uuid";
|
|
54
|
+
format?: "date" | "date-time" | "email" | "uri" | "uuid" | "phone";
|
|
55
55
|
enum?: string[];
|
|
56
56
|
default?: string;
|
|
57
57
|
}
|
|
@@ -69,6 +69,7 @@ export interface JSONSchemaInteger extends JSONSchemaBase {
|
|
|
69
69
|
minimum?: number;
|
|
70
70
|
maximum?: number;
|
|
71
71
|
multipleOf?: number;
|
|
72
|
+
enum?: number[];
|
|
72
73
|
default?: number;
|
|
73
74
|
}
|
|
74
75
|
export interface JSONSchemaBoolean extends JSONSchemaBase {
|
|
@@ -94,7 +95,7 @@ export interface JSONSchemaEnum extends JSONSchemaBase {
|
|
|
94
95
|
/**
|
|
95
96
|
* Field type enumeration - maps to UI component types
|
|
96
97
|
*/
|
|
97
|
-
export type FieldType = "text" | "password" | "number" | "integer" | "boolean" | "select" | "multiselect" | "date" | "datetime" | "email" | "url" | "textarea" | "array" | "object" | "computed" | "display";
|
|
98
|
+
export type FieldType = "text" | "phone" | "password" | "number" | "integer" | "boolean" | "select" | "multiselect" | "date" | "datetime" | "email" | "url" | "textarea" | "array" | "object" | "computed" | "display" | "matrix";
|
|
98
99
|
/**
|
|
99
100
|
* Validation rule with FEEL expression
|
|
100
101
|
*/
|
|
@@ -167,7 +168,7 @@ export interface FieldDefinitionBase {
|
|
|
167
168
|
* rather than inside variantConfig.
|
|
168
169
|
*/
|
|
169
170
|
export interface AdornableFieldDefinition extends FieldDefinitionBase {
|
|
170
|
-
type: "text" | "email" | "url" | "password" | "textarea" | "number" | "integer";
|
|
171
|
+
type: "text" | "phone" | "email" | "url" | "password" | "textarea" | "number" | "integer";
|
|
171
172
|
/** Text/symbol displayed before input (e.g., "$", "https://") */
|
|
172
173
|
prefix?: string;
|
|
173
174
|
/** Text/symbol displayed after input (e.g., "USD", "kg", "%") */
|
|
@@ -226,6 +227,45 @@ export interface DisplayFieldDefinition extends FieldDefinitionBase {
|
|
|
226
227
|
/** Display format (e.g., "currency", "percent", "decimal(2)", or template "{value} Nm") */
|
|
227
228
|
format?: string;
|
|
228
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Row definition for matrix fields.
|
|
232
|
+
*/
|
|
233
|
+
export interface MatrixRow {
|
|
234
|
+
/** Unique row identifier (snake_case, used in data keys and FEEL expressions) */
|
|
235
|
+
id: string;
|
|
236
|
+
/** Display label for this row */
|
|
237
|
+
label: string;
|
|
238
|
+
/** When to show this row */
|
|
239
|
+
visibleWhen?: FEELExpression;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Column definition for matrix fields.
|
|
243
|
+
*/
|
|
244
|
+
export interface MatrixColumn {
|
|
245
|
+
/** Column value — use numbers for numeric scales, strings for categorical */
|
|
246
|
+
value: string | number;
|
|
247
|
+
/** Display label for this column */
|
|
248
|
+
label: string;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Matrix/grid fields — a 2D grid where rows are items/statements and
|
|
252
|
+
* columns are shared response options. Each row produces a single value
|
|
253
|
+
* (or multiple values in multi-select mode).
|
|
254
|
+
*
|
|
255
|
+
* Data shape (single-select): `{ speed: 4, quality: 5, support: 3 }`
|
|
256
|
+
* Data shape (multi-select): `{ comm: ["beginner", "intermediate"], lead: ["advanced"] }`
|
|
257
|
+
*
|
|
258
|
+
* FEEL access: `fieldId.rowId` (e.g., `service_rating.speed >= 4`)
|
|
259
|
+
*/
|
|
260
|
+
export interface MatrixFieldDefinition extends FieldDefinitionBase {
|
|
261
|
+
type: "matrix";
|
|
262
|
+
/** Row definitions (items/statements to rate) */
|
|
263
|
+
rows: MatrixRow[];
|
|
264
|
+
/** Column definitions (shared response options) */
|
|
265
|
+
columns: MatrixColumn[];
|
|
266
|
+
/** Allow multiple selections per row (default: false) */
|
|
267
|
+
multiSelect?: boolean;
|
|
268
|
+
}
|
|
229
269
|
/**
|
|
230
270
|
* Computed fields (read-only calculated values).
|
|
231
271
|
* Note: Computed field definitions live in Forma.computed, not Forma.fields.
|
|
@@ -250,7 +290,7 @@ export interface ComputedFieldDefinition extends FieldDefinitionBase {
|
|
|
250
290
|
* field.options; // SelectOption[] | undefined
|
|
251
291
|
* }
|
|
252
292
|
*/
|
|
253
|
-
export type FieldDefinition = AdornableFieldDefinition | SelectionFieldDefinition | SimpleFieldDefinition | ArrayFieldDefinition | ObjectFieldDefinition | DisplayFieldDefinition | ComputedFieldDefinition;
|
|
293
|
+
export type FieldDefinition = AdornableFieldDefinition | SelectionFieldDefinition | SimpleFieldDefinition | ArrayFieldDefinition | ObjectFieldDefinition | DisplayFieldDefinition | ComputedFieldDefinition | MatrixFieldDefinition;
|
|
254
294
|
/** Check if field supports prefix/suffix adorners */
|
|
255
295
|
export declare function isAdornableField(field: FieldDefinition): field is AdornableFieldDefinition;
|
|
256
296
|
/** Check if field is a display-only field (no data) */
|
|
@@ -259,6 +299,8 @@ export declare function isDisplayField(field: FieldDefinition): field is Display
|
|
|
259
299
|
export declare function isSelectionField(field: FieldDefinition): field is SelectionFieldDefinition;
|
|
260
300
|
/** Check if field is an array field */
|
|
261
301
|
export declare function isArrayField(field: FieldDefinition): field is ArrayFieldDefinition;
|
|
302
|
+
/** Check if field is a matrix field */
|
|
303
|
+
export declare function isMatrixField(field: FieldDefinition): field is MatrixFieldDefinition;
|
|
262
304
|
/** Check if field collects data (not display) */
|
|
263
305
|
export declare function isDataField(field: FieldDefinition): boolean;
|
|
264
306
|
/**
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAMpC;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAMpC;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;IACnE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD;;GAEG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,GACR,aAAa,GACb,MAAM,GACN,UAAU,GACV,OAAO,GACP,KAAK,GACL,UAAU,GACV,OAAO,GACP,QAAQ,GACR,UAAU,GACV,SAAS,GACT,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,IAAI,EAAE,cAAc,CAAC;IACrB,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAMD;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,IAAI,EAAE,SAAS,CAAC;IAChB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IAIrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,qEAAqE;IACrE,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,+DAA+D;IAC/D,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,mFAAmF;IACnF,YAAY,CAAC,EAAE,cAAc,CAAC;IAI9B,qDAAqD;IACrD,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAI/B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAIxC;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,EACA,MAAM,GACN,OAAO,GACP,OAAO,GACP,KAAK,GACL,UAAU,GACV,UAAU,GACV,QAAQ,GACR,SAAS,CAAC;IACd,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,EAAE,QAAQ,GAAG,aAAa,CAAC;IAC/B,sCAAsC;IACtC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,UAAU,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,OAAO,CAAC;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,IAAI,EAAE,SAAS,CAAC;IAChB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,iFAAiF;IACjF,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,IAAI,EAAE,QAAQ,CAAC;IACf,iDAAiD;IACjD,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,mDAAmD;IACnD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,yDAAyD;IACzD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,mBAAmB;IAClE,IAAI,EAAE,UAAU,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GACvB,wBAAwB,GACxB,wBAAwB,GACxB,qBAAqB,GACrB,oBAAoB,GACpB,qBAAqB,GACrB,sBAAsB,GACtB,uBAAuB,GACvB,qBAAqB,CAAC;AAkB1B,qDAAqD;AACrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,GACrB,KAAK,IAAI,wBAAwB,CAEnC;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,eAAe,GACrB,KAAK,IAAI,sBAAsB,CAEjC;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,GACrB,KAAK,IAAI,wBAAwB,CAEnC;AAED,uCAAuC;AACvC,wBAAgB,YAAY,CAC1B,KAAK,EAAE,eAAe,GACrB,KAAK,IAAI,oBAAoB,CAE/B;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAC3B,KAAK,EAAE,eAAe,GACrB,KAAK,IAAI,qBAAqB,CAEhC;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAE3D;AAMD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,UAAU,EAAE,cAAc,CAAC;IAC3B,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAMD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,6BAA6B;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,OAAO,EAAE,KAAK,CAAC;IACf,oBAAoB;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,8CAA8C;IAC9C,MAAM,EAAE,UAAU,CAAC;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACzC,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxC,gDAAgD;IAChD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;CAC1B;AAMD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,gCAAgC;IAChC,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,4CAA4C;IAC5C,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for matrix field type - validation and visibility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { validate } from "../engine/validate.js";
|
|
7
|
+
import { getVisibility } from "../engine/visibility.js";
|
|
8
|
+
import type { Forma, MatrixFieldDefinition } from "../types.js";
|
|
9
|
+
import { isMatrixField, isDataField } from "../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to create a minimal Forma spec with a matrix field
|
|
13
|
+
*/
|
|
14
|
+
function createMatrixSpec(options: {
|
|
15
|
+
fieldDef?: Partial<MatrixFieldDefinition>;
|
|
16
|
+
schemaProperties?: Record<string, unknown>;
|
|
17
|
+
required?: string[];
|
|
18
|
+
data?: Record<string, unknown>;
|
|
19
|
+
}): Forma {
|
|
20
|
+
const {
|
|
21
|
+
fieldDef = {},
|
|
22
|
+
schemaProperties,
|
|
23
|
+
required = [],
|
|
24
|
+
} = options;
|
|
25
|
+
|
|
26
|
+
const matrixField: MatrixFieldDefinition = {
|
|
27
|
+
type: "matrix",
|
|
28
|
+
label: "Service Rating",
|
|
29
|
+
rows: [
|
|
30
|
+
{ id: "speed", label: "Speed" },
|
|
31
|
+
{ id: "quality", label: "Quality" },
|
|
32
|
+
{ id: "support", label: "Support" },
|
|
33
|
+
],
|
|
34
|
+
columns: [
|
|
35
|
+
{ value: 1, label: "Poor" },
|
|
36
|
+
{ value: 2, label: "Fair" },
|
|
37
|
+
{ value: 3, label: "Good" },
|
|
38
|
+
{ value: 4, label: "Very Good" },
|
|
39
|
+
{ value: 5, label: "Excellent" },
|
|
40
|
+
],
|
|
41
|
+
...fieldDef,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const schema = schemaProperties ?? {
|
|
45
|
+
service_rating: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
speed: { type: "integer", enum: [1, 2, 3, 4, 5] },
|
|
49
|
+
quality: { type: "integer", enum: [1, 2, 3, 4, 5] },
|
|
50
|
+
support: { type: "integer", enum: [1, 2, 3, 4, 5] },
|
|
51
|
+
},
|
|
52
|
+
required: ["speed", "quality", "support"],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
version: "1.0",
|
|
58
|
+
meta: { id: "test", title: "Test" },
|
|
59
|
+
schema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: schema as Forma["schema"]["properties"],
|
|
62
|
+
required: required.length > 0 ? required : undefined,
|
|
63
|
+
},
|
|
64
|
+
fields: {
|
|
65
|
+
service_rating: matrixField,
|
|
66
|
+
} as Forma["fields"],
|
|
67
|
+
fieldOrder: ["service_rating"],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Type Guards
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe("type guards", () => {
|
|
76
|
+
it("isMatrixField should return true for matrix fields", () => {
|
|
77
|
+
const field: MatrixFieldDefinition = {
|
|
78
|
+
type: "matrix",
|
|
79
|
+
rows: [{ id: "a", label: "A" }],
|
|
80
|
+
columns: [{ value: 1, label: "1" }],
|
|
81
|
+
};
|
|
82
|
+
expect(isMatrixField(field)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("isMatrixField should return false for non-matrix fields", () => {
|
|
86
|
+
expect(isMatrixField({ type: "text" })).toBe(false);
|
|
87
|
+
expect(isMatrixField({ type: "select" })).toBe(false);
|
|
88
|
+
expect(isMatrixField({ type: "array" })).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("isDataField should return true for matrix fields", () => {
|
|
92
|
+
const field: MatrixFieldDefinition = {
|
|
93
|
+
type: "matrix",
|
|
94
|
+
rows: [{ id: "a", label: "A" }],
|
|
95
|
+
columns: [{ value: 1, label: "1" }],
|
|
96
|
+
};
|
|
97
|
+
expect(isDataField(field)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Validation
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
describe("matrix validation", () => {
|
|
106
|
+
describe("valid data", () => {
|
|
107
|
+
it("should pass with valid numeric column values", () => {
|
|
108
|
+
const spec = createMatrixSpec({});
|
|
109
|
+
const data = {
|
|
110
|
+
service_rating: { speed: 4, quality: 5, support: 3 },
|
|
111
|
+
};
|
|
112
|
+
const result = validate(data, spec);
|
|
113
|
+
expect(result.valid).toBe(true);
|
|
114
|
+
expect(result.errors).toHaveLength(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should pass with valid string column values", () => {
|
|
118
|
+
const spec = createMatrixSpec({
|
|
119
|
+
fieldDef: {
|
|
120
|
+
columns: [
|
|
121
|
+
{ value: "agree", label: "Agree" },
|
|
122
|
+
{ value: "disagree", label: "Disagree" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
schemaProperties: {
|
|
126
|
+
service_rating: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
speed: { type: "string", enum: ["agree", "disagree"] },
|
|
130
|
+
quality: { type: "string", enum: ["agree", "disagree"] },
|
|
131
|
+
support: { type: "string", enum: ["agree", "disagree"] },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const data = {
|
|
137
|
+
service_rating: { speed: "agree", quality: "disagree", support: "agree" },
|
|
138
|
+
};
|
|
139
|
+
const result = validate(data, spec);
|
|
140
|
+
expect(result.valid).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should pass with partial answers when not required", () => {
|
|
144
|
+
const spec = createMatrixSpec({});
|
|
145
|
+
const data = {
|
|
146
|
+
service_rating: { speed: 4 },
|
|
147
|
+
};
|
|
148
|
+
const result = validate(data, spec);
|
|
149
|
+
expect(result.valid).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should pass with null value when not required", () => {
|
|
153
|
+
const spec = createMatrixSpec({});
|
|
154
|
+
const result = validate({ service_rating: null }, spec);
|
|
155
|
+
expect(result.valid).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("invalid data", () => {
|
|
160
|
+
it("should fail with invalid column values", () => {
|
|
161
|
+
const spec = createMatrixSpec({});
|
|
162
|
+
const data = {
|
|
163
|
+
service_rating: { speed: 99, quality: 5, support: 3 },
|
|
164
|
+
};
|
|
165
|
+
const result = validate(data, spec);
|
|
166
|
+
expect(result.valid).toBe(false);
|
|
167
|
+
expect(result.errors).toHaveLength(1);
|
|
168
|
+
expect(result.errors[0].field).toBe("service_rating.speed");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should fail when required and empty", () => {
|
|
172
|
+
const spec = createMatrixSpec({ required: ["service_rating"] });
|
|
173
|
+
const result = validate({}, spec);
|
|
174
|
+
expect(result.valid).toBe(false);
|
|
175
|
+
expect(result.errors.some((e) => e.field === "service_rating")).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should fail when required and not all visible rows answered", () => {
|
|
179
|
+
const spec = createMatrixSpec({ required: ["service_rating"] });
|
|
180
|
+
const data = {
|
|
181
|
+
service_rating: { speed: 4 }, // quality and support missing
|
|
182
|
+
};
|
|
183
|
+
const result = validate(data, spec);
|
|
184
|
+
expect(result.valid).toBe(false);
|
|
185
|
+
expect(result.errors).toHaveLength(2);
|
|
186
|
+
expect(result.errors[0].field).toBe("service_rating.quality");
|
|
187
|
+
expect(result.errors[1].field).toBe("service_rating.support");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should pass when required and all visible rows answered", () => {
|
|
191
|
+
const spec = createMatrixSpec({ required: ["service_rating"] });
|
|
192
|
+
const data = {
|
|
193
|
+
service_rating: { speed: 4, quality: 5, support: 3 },
|
|
194
|
+
};
|
|
195
|
+
const result = validate(data, spec);
|
|
196
|
+
expect(result.valid).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("multi-select mode", () => {
|
|
201
|
+
it("should pass with array values in multi-select mode", () => {
|
|
202
|
+
const spec = createMatrixSpec({
|
|
203
|
+
fieldDef: {
|
|
204
|
+
multiSelect: true,
|
|
205
|
+
columns: [
|
|
206
|
+
{ value: "beginner", label: "Beginner" },
|
|
207
|
+
{ value: "intermediate", label: "Intermediate" },
|
|
208
|
+
{ value: "advanced", label: "Advanced" },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
schemaProperties: {
|
|
212
|
+
service_rating: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
speed: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
|
|
218
|
+
},
|
|
219
|
+
quality: {
|
|
220
|
+
type: "array",
|
|
221
|
+
items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
|
|
222
|
+
},
|
|
223
|
+
support: {
|
|
224
|
+
type: "array",
|
|
225
|
+
items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const data = {
|
|
232
|
+
service_rating: {
|
|
233
|
+
speed: ["beginner", "intermediate"],
|
|
234
|
+
quality: ["advanced"],
|
|
235
|
+
support: [],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const result = validate(data, spec);
|
|
239
|
+
expect(result.valid).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should fail with invalid values in multi-select array", () => {
|
|
243
|
+
const spec = createMatrixSpec({
|
|
244
|
+
fieldDef: {
|
|
245
|
+
multiSelect: true,
|
|
246
|
+
columns: [
|
|
247
|
+
{ value: "beginner", label: "Beginner" },
|
|
248
|
+
{ value: "advanced", label: "Advanced" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
schemaProperties: {
|
|
252
|
+
service_rating: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
speed: {
|
|
256
|
+
type: "array",
|
|
257
|
+
items: { type: "string", enum: ["beginner", "advanced"] },
|
|
258
|
+
},
|
|
259
|
+
quality: {
|
|
260
|
+
type: "array",
|
|
261
|
+
items: { type: "string", enum: ["beginner", "advanced"] },
|
|
262
|
+
},
|
|
263
|
+
support: {
|
|
264
|
+
type: "array",
|
|
265
|
+
items: { type: "string", enum: ["beginner", "advanced"] },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const data = {
|
|
272
|
+
service_rating: {
|
|
273
|
+
speed: ["beginner", "INVALID"],
|
|
274
|
+
quality: ["advanced"],
|
|
275
|
+
support: [],
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
const result = validate(data, spec);
|
|
279
|
+
expect(result.valid).toBe(false);
|
|
280
|
+
expect(result.errors[0].field).toBe("service_rating.speed");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should fail when multi-select row has non-array value", () => {
|
|
284
|
+
const spec = createMatrixSpec({
|
|
285
|
+
fieldDef: {
|
|
286
|
+
multiSelect: true,
|
|
287
|
+
columns: [
|
|
288
|
+
{ value: "a", label: "A" },
|
|
289
|
+
{ value: "b", label: "B" },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
schemaProperties: {
|
|
293
|
+
service_rating: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
speed: { type: "array", items: { type: "string" } },
|
|
297
|
+
quality: { type: "array", items: { type: "string" } },
|
|
298
|
+
support: { type: "array", items: { type: "string" } },
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
const data = {
|
|
304
|
+
service_rating: {
|
|
305
|
+
speed: "a", // should be array
|
|
306
|
+
quality: ["a"],
|
|
307
|
+
support: ["b"],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const result = validate(data, spec);
|
|
311
|
+
expect(result.valid).toBe(false);
|
|
312
|
+
expect(result.errors[0].field).toBe("service_rating.speed");
|
|
313
|
+
expect(result.errors[0].message).toContain("list");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Visibility
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
describe("matrix visibility", () => {
|
|
323
|
+
it("should compute visibility for all matrix rows", () => {
|
|
324
|
+
const spec = createMatrixSpec({});
|
|
325
|
+
const visibility = getVisibility({}, spec);
|
|
326
|
+
expect(visibility["service_rating"]).toBe(true);
|
|
327
|
+
expect(visibility["service_rating.speed"]).toBe(true);
|
|
328
|
+
expect(visibility["service_rating.quality"]).toBe(true);
|
|
329
|
+
expect(visibility["service_rating.support"]).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should hide matrix field when visibleWhen evaluates to false", () => {
|
|
333
|
+
const spec = createMatrixSpec({
|
|
334
|
+
fieldDef: { visibleWhen: "show_rating = true" },
|
|
335
|
+
});
|
|
336
|
+
const visibility = getVisibility({ show_rating: false }, spec);
|
|
337
|
+
expect(visibility["service_rating"]).toBe(false);
|
|
338
|
+
// Row visibility should not be computed for hidden fields
|
|
339
|
+
expect(visibility["service_rating.speed"]).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should hide individual rows with visibleWhen", () => {
|
|
343
|
+
const spec = createMatrixSpec({
|
|
344
|
+
fieldDef: {
|
|
345
|
+
rows: [
|
|
346
|
+
{ id: "speed", label: "Speed" },
|
|
347
|
+
{ id: "quality", label: "Quality", visibleWhen: "show_quality = true" },
|
|
348
|
+
{ id: "support", label: "Support" },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const visibilityHidden = getVisibility({ show_quality: false }, spec);
|
|
354
|
+
expect(visibilityHidden["service_rating.speed"]).toBe(true);
|
|
355
|
+
expect(visibilityHidden["service_rating.quality"]).toBe(false);
|
|
356
|
+
expect(visibilityHidden["service_rating.support"]).toBe(true);
|
|
357
|
+
|
|
358
|
+
const visibilityShown = getVisibility({ show_quality: true }, spec);
|
|
359
|
+
expect(visibilityShown["service_rating.quality"]).toBe(true);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("should skip hidden rows during validation", () => {
|
|
363
|
+
const spec = createMatrixSpec({
|
|
364
|
+
fieldDef: {
|
|
365
|
+
rows: [
|
|
366
|
+
{ id: "speed", label: "Speed" },
|
|
367
|
+
{ id: "quality", label: "Quality", visibleWhen: "show_quality = true" },
|
|
368
|
+
{ id: "support", label: "Support" },
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
// quality row is hidden but has an invalid value — should NOT produce error
|
|
373
|
+
const data = {
|
|
374
|
+
service_rating: { speed: 4, quality: 99, support: 3 },
|
|
375
|
+
};
|
|
376
|
+
const result = validate(data, spec, { onlyVisible: true });
|
|
377
|
+
expect(result.valid).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
});
|
package/src/engine/validate.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
Forma,
|
|
14
14
|
FieldDefinition,
|
|
15
15
|
ArrayFieldDefinition,
|
|
16
|
+
MatrixFieldDefinition,
|
|
16
17
|
ValidationRule,
|
|
17
18
|
EvaluationContext,
|
|
18
19
|
ValidationResult,
|
|
@@ -199,6 +200,24 @@ function validateField(
|
|
|
199
200
|
errors.push(...arrayErrors);
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
// 5. Matrix validation
|
|
204
|
+
if (
|
|
205
|
+
fieldDef.type === "matrix" &&
|
|
206
|
+
value !== null &&
|
|
207
|
+
value !== undefined &&
|
|
208
|
+
typeof value === "object" &&
|
|
209
|
+
!Array.isArray(value)
|
|
210
|
+
) {
|
|
211
|
+
const matrixErrors = validateMatrix(
|
|
212
|
+
path,
|
|
213
|
+
value as Record<string, unknown>,
|
|
214
|
+
fieldDef,
|
|
215
|
+
visibility,
|
|
216
|
+
required,
|
|
217
|
+
);
|
|
218
|
+
errors.push(...matrixErrors);
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
return errors;
|
|
203
222
|
}
|
|
204
223
|
|
|
@@ -687,6 +706,84 @@ function validateArrayItem(
|
|
|
687
706
|
return errors;
|
|
688
707
|
}
|
|
689
708
|
|
|
709
|
+
// ============================================================================
|
|
710
|
+
// Matrix Validation
|
|
711
|
+
// ============================================================================
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Validate matrix field values against row/column definitions.
|
|
715
|
+
* Each row value must be a valid column value (or array of column values if multiSelect).
|
|
716
|
+
*/
|
|
717
|
+
function validateMatrix(
|
|
718
|
+
path: string,
|
|
719
|
+
value: Record<string, unknown>,
|
|
720
|
+
fieldDef: MatrixFieldDefinition,
|
|
721
|
+
visibility: Record<string, boolean>,
|
|
722
|
+
required: boolean,
|
|
723
|
+
): FieldError[] {
|
|
724
|
+
const errors: FieldError[] = [];
|
|
725
|
+
const label = fieldDef.label ?? path;
|
|
726
|
+
const validColumnValues = new Set(fieldDef.columns.map((c) => c.value));
|
|
727
|
+
|
|
728
|
+
for (const row of fieldDef.rows) {
|
|
729
|
+
// Skip hidden rows
|
|
730
|
+
const rowPath = `${path}.${row.id}`;
|
|
731
|
+
if (visibility[rowPath] === false) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const rowValue = value[row.id];
|
|
736
|
+
|
|
737
|
+
// Required: all visible rows must be answered
|
|
738
|
+
if (required && (rowValue === null || rowValue === undefined)) {
|
|
739
|
+
errors.push({
|
|
740
|
+
field: rowPath,
|
|
741
|
+
message: `${row.label} is required`,
|
|
742
|
+
severity: "error",
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Skip empty row values for non-required fields
|
|
748
|
+
if (rowValue === null || rowValue === undefined) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (fieldDef.multiSelect) {
|
|
753
|
+
// Multi-select: value should be an array of column values
|
|
754
|
+
if (!Array.isArray(rowValue)) {
|
|
755
|
+
errors.push({
|
|
756
|
+
field: rowPath,
|
|
757
|
+
message: `${row.label} must be a list of selections`,
|
|
758
|
+
severity: "error",
|
|
759
|
+
});
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
for (const item of rowValue) {
|
|
763
|
+
if (!validColumnValues.has(item as string | number)) {
|
|
764
|
+
errors.push({
|
|
765
|
+
field: rowPath,
|
|
766
|
+
message: `${row.label} contains an invalid selection`,
|
|
767
|
+
severity: "error",
|
|
768
|
+
});
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
// Single-select: value should be a single column value
|
|
774
|
+
if (!validColumnValues.has(rowValue as string | number)) {
|
|
775
|
+
errors.push({
|
|
776
|
+
field: rowPath,
|
|
777
|
+
message: `${row.label} has an invalid value for ${label}`,
|
|
778
|
+
severity: "error",
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return errors;
|
|
785
|
+
}
|
|
786
|
+
|
|
690
787
|
// ============================================================================
|
|
691
788
|
// Single Field Validation
|
|
692
789
|
// ============================================================================
|