@finsys/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/LICENSE +202 -0
- package/README.md +220 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/rhf-generator.d.ts +30 -0
- package/dist/rhf-generator.js +252 -0
- package/dist/rhf-generator.test.d.ts +1 -0
- package/dist/rhf-generator.test.js +568 -0
- package/dist/schema/schema/unified-form.schema.json +266 -0
- package/dist/schema/unified-form.schema.json +266 -0
- package/dist/survey-generator.d.ts +153 -0
- package/dist/survey-generator.js +185 -0
- package/dist/survey-generator.test.d.ts +1 -0
- package/dist/survey-generator.test.js +317 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +41 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +116 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +34 -0
- package/dist/validator.test.d.ts +1 -0
- package/dist/validator.test.js +127 -0
- package/package.json +73 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survey Generator - Converts unified form configurations into SurveyJS-compatible JSON
|
|
3
|
+
*
|
|
4
|
+
* This module converts a unified form-config.json into SurveyJS-compatible JSON,
|
|
5
|
+
* resolving field references, applying dynamic titles, and grouping by category.
|
|
6
|
+
*/
|
|
7
|
+
export type { IQuestion, IPage, ISurvey, IPanel, IElement } from "survey-core";
|
|
8
|
+
export interface Category {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Choice {
|
|
13
|
+
value: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Validator {
|
|
17
|
+
type: "regex" | "email" | "numeric" | "text" | "expression" | "answercount" | "custom";
|
|
18
|
+
text?: string;
|
|
19
|
+
regex?: string;
|
|
20
|
+
minValue?: number;
|
|
21
|
+
maxValue?: number;
|
|
22
|
+
minLength?: number;
|
|
23
|
+
maxLength?: number;
|
|
24
|
+
minCount?: number;
|
|
25
|
+
maxCount?: number;
|
|
26
|
+
expression?: string;
|
|
27
|
+
validator?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* FieldData - Field definition in the unified form config
|
|
31
|
+
*/
|
|
32
|
+
export interface FieldData {
|
|
33
|
+
name?: string;
|
|
34
|
+
displayName?: string;
|
|
35
|
+
type: string;
|
|
36
|
+
inputType?: "text" | "number" | "tel" | "date" | "email" | "password" | "url";
|
|
37
|
+
category?: string;
|
|
38
|
+
maxLength?: number | string;
|
|
39
|
+
min?: number | string;
|
|
40
|
+
max?: number | string;
|
|
41
|
+
step?: number;
|
|
42
|
+
html?: string;
|
|
43
|
+
choices?: Choice[];
|
|
44
|
+
validators?: Validator[];
|
|
45
|
+
visible?: boolean;
|
|
46
|
+
visibleIf?: string;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
isRequired?: boolean;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
defaultValue?: string | number | boolean | string[];
|
|
51
|
+
readOnly?: boolean;
|
|
52
|
+
startWithNewLine?: boolean;
|
|
53
|
+
enableIf?: string;
|
|
54
|
+
titleLocation?: "default" | "top" | "bottom" | "left" | "hidden";
|
|
55
|
+
ihs_column_names?: string[];
|
|
56
|
+
requiredForEvaluation?: boolean;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* FieldReference - How fields are referenced in pages
|
|
61
|
+
*/
|
|
62
|
+
export type FieldReference = string | {
|
|
63
|
+
ref: string;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
} | {
|
|
66
|
+
definition: FieldData & {
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export interface PageConfig {
|
|
71
|
+
id: string;
|
|
72
|
+
title?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
showTOC?: boolean;
|
|
75
|
+
showProgressBar?: boolean;
|
|
76
|
+
showCategoryHeadings?: boolean;
|
|
77
|
+
layout?: "default" | "grid" | "vertical";
|
|
78
|
+
fields: FieldReference[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* UnifiedFormConfig - The main configuration type for the unified form format
|
|
82
|
+
*/
|
|
83
|
+
export interface UnifiedFormConfig {
|
|
84
|
+
$schema?: string;
|
|
85
|
+
schemaVersion?: string;
|
|
86
|
+
displayName?: string;
|
|
87
|
+
templateIcon?: string;
|
|
88
|
+
categories: Category[];
|
|
89
|
+
fields: Record<string, FieldData>;
|
|
90
|
+
pages?: PageConfig[];
|
|
91
|
+
}
|
|
92
|
+
export interface SurveyElementJSON {
|
|
93
|
+
type: string;
|
|
94
|
+
name: string;
|
|
95
|
+
title?: string;
|
|
96
|
+
isRequired?: boolean;
|
|
97
|
+
maxLength?: number;
|
|
98
|
+
storeDataAsText?: boolean;
|
|
99
|
+
category?: string;
|
|
100
|
+
[key: string]: unknown;
|
|
101
|
+
}
|
|
102
|
+
export interface SurveyPageJSON {
|
|
103
|
+
name: string;
|
|
104
|
+
title: string;
|
|
105
|
+
elements: Array<{
|
|
106
|
+
type: string;
|
|
107
|
+
name: string;
|
|
108
|
+
elements: SurveyElementJSON[];
|
|
109
|
+
}>;
|
|
110
|
+
}
|
|
111
|
+
export interface SurveyJSON {
|
|
112
|
+
title: string;
|
|
113
|
+
logoPosition?: string;
|
|
114
|
+
pages: SurveyPageJSON[];
|
|
115
|
+
showQuestionNumbers?: string;
|
|
116
|
+
questionErrorLocation?: string;
|
|
117
|
+
completedHtml?: string;
|
|
118
|
+
showTOC?: boolean;
|
|
119
|
+
completeText?: string;
|
|
120
|
+
showPreviewBeforeComplete?: string;
|
|
121
|
+
showProgressBar?: string;
|
|
122
|
+
widthMode?: string;
|
|
123
|
+
width?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface ResolvedField extends FieldData {
|
|
126
|
+
name: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Apply dynamic title logic (e.g., bank statements with dynamic month labels,
|
|
130
|
+
* financial statements with dynamic year labels)
|
|
131
|
+
*/
|
|
132
|
+
export declare function applyDynamicTitles(field: ResolvedField): ResolvedField;
|
|
133
|
+
/**
|
|
134
|
+
* Generate SurveyJS JSON from unified form config
|
|
135
|
+
*/
|
|
136
|
+
export declare function generateSurveyJson(config: UnifiedFormConfig): SurveyJSON | Record<string, never>;
|
|
137
|
+
/**
|
|
138
|
+
* Resolve all fields from a page to full field definitions
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolvePageFields(page: PageConfig, fields: Record<string, FieldData>): ResolvedField[];
|
|
141
|
+
/**
|
|
142
|
+
* Get category name from category ID (supports string or number IDs)
|
|
143
|
+
*/
|
|
144
|
+
export declare function getCategoryName(categoryId: string | undefined, categories: Category[]): string;
|
|
145
|
+
/**
|
|
146
|
+
* Group fields by their category, maintaining order and creating section breaks
|
|
147
|
+
*/
|
|
148
|
+
export interface FieldGroup {
|
|
149
|
+
category: string;
|
|
150
|
+
categoryName: string;
|
|
151
|
+
fields: ResolvedField[];
|
|
152
|
+
}
|
|
153
|
+
export declare function groupFieldsByCategory(fields: ResolvedField[], categories: Category[]): FieldGroup[];
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Survey Generator - Converts unified form configurations into SurveyJS-compatible JSON
|
|
3
|
+
*
|
|
4
|
+
* This module converts a unified form-config.json into SurveyJS-compatible JSON,
|
|
5
|
+
* resolving field references, applying dynamic titles, and grouping by category.
|
|
6
|
+
*/
|
|
7
|
+
import { getPastMonthLabel, getPastYearLabel } from "./utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a field to ensure backward compatibility
|
|
10
|
+
* - Sets both `required` and `isRequired` to the same value
|
|
11
|
+
*/
|
|
12
|
+
function normalizeField(field) {
|
|
13
|
+
// Spec: Fields are required by default unless explicitly false
|
|
14
|
+
const isRequired = field.isRequired !== false && field.required !== false;
|
|
15
|
+
return {
|
|
16
|
+
...field,
|
|
17
|
+
required: isRequired,
|
|
18
|
+
isRequired: isRequired
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a field reference to a full field definition
|
|
23
|
+
*/
|
|
24
|
+
function resolveFieldReference(ref, fields) {
|
|
25
|
+
if (typeof ref === "string") {
|
|
26
|
+
// Simple string reference
|
|
27
|
+
const fieldDef = fields[ref];
|
|
28
|
+
if (!fieldDef) {
|
|
29
|
+
console.warn(`Field "${ref}" not found in fields definition`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return normalizeField({ name: ref, ...fieldDef });
|
|
33
|
+
}
|
|
34
|
+
if ("definition" in ref) {
|
|
35
|
+
// Inline definition
|
|
36
|
+
return normalizeField(ref.definition);
|
|
37
|
+
}
|
|
38
|
+
if ("ref" in ref) {
|
|
39
|
+
// Reference with overrides
|
|
40
|
+
const fieldDef = fields[ref.ref];
|
|
41
|
+
if (!fieldDef) {
|
|
42
|
+
console.warn(`Field "${ref.ref}" not found in fields definition`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const { ref: fieldName, ...overrides } = ref;
|
|
46
|
+
return normalizeField({ name: fieldName, ...fieldDef, ...overrides });
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply dynamic title logic (e.g., bank statements with dynamic month labels,
|
|
52
|
+
* financial statements with dynamic year labels)
|
|
53
|
+
*/
|
|
54
|
+
export function applyDynamicTitles(field) {
|
|
55
|
+
const bankStatementMatch = field.name.match(/^bank_statement_t(\d+)$/);
|
|
56
|
+
if (bankStatementMatch) {
|
|
57
|
+
const monthsAgo = parseInt(bankStatementMatch[1], 10);
|
|
58
|
+
return {
|
|
59
|
+
...field,
|
|
60
|
+
displayName: `Bank Statement (${getPastMonthLabel(monthsAgo)})`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (field.name === "financials") {
|
|
64
|
+
return {
|
|
65
|
+
...field,
|
|
66
|
+
displayName: `Audited Financial Statement (${getPastYearLabel(1)})`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return field;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Generate SurveyJS JSON from unified form config
|
|
73
|
+
*/
|
|
74
|
+
export function generateSurveyJson(config) {
|
|
75
|
+
if (!config.pages || config.pages.length === 0)
|
|
76
|
+
return {};
|
|
77
|
+
const firstPage = config.pages.find(p => p.id !== "success-message");
|
|
78
|
+
if (!firstPage)
|
|
79
|
+
return {};
|
|
80
|
+
const categories = config.categories || [];
|
|
81
|
+
const categoryMap = categories.reduce((acc, cat) => {
|
|
82
|
+
acc[cat.id] = cat.name;
|
|
83
|
+
return acc;
|
|
84
|
+
}, {});
|
|
85
|
+
// Process all fields from the first page
|
|
86
|
+
const processedFields = firstPage.fields
|
|
87
|
+
.map(ref => resolveFieldReference(ref, config.fields))
|
|
88
|
+
.filter((f) => f !== null)
|
|
89
|
+
.map(f => applyDynamicTitles(f))
|
|
90
|
+
.map(f => mapFieldToSurveyJS(f));
|
|
91
|
+
// Group by Category
|
|
92
|
+
const grouped = processedFields.reduce((acc, field) => {
|
|
93
|
+
const catName = categoryMap[field.category] || "Default";
|
|
94
|
+
if (!acc[catName])
|
|
95
|
+
acc[catName] = [];
|
|
96
|
+
acc[catName].push(field);
|
|
97
|
+
return acc;
|
|
98
|
+
}, {});
|
|
99
|
+
// Build Pages (One Page per Category for Wizard effect)
|
|
100
|
+
const pages = Object.entries(grouped).map(([categoryName, elements]) => ({
|
|
101
|
+
name: categoryName.toLowerCase().replace(/\s+/g, "-"),
|
|
102
|
+
title: firstPage.showCategoryHeadings === false ? "" : categoryName,
|
|
103
|
+
elements: [
|
|
104
|
+
{
|
|
105
|
+
type: "panel",
|
|
106
|
+
name: `${categoryName.toLowerCase().replace(/\s+/g, "-")}-panel`,
|
|
107
|
+
elements: elements,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
title: config.displayName || "Flexible Financing Program",
|
|
113
|
+
logoPosition: "right",
|
|
114
|
+
pages,
|
|
115
|
+
showQuestionNumbers: "off",
|
|
116
|
+
questionErrorLocation: "bottom",
|
|
117
|
+
completedHtml: "<h3>Thank you for completing the form</h3>",
|
|
118
|
+
showTOC: firstPage.showTOC ?? true,
|
|
119
|
+
completeText: "Submit",
|
|
120
|
+
showPreviewBeforeComplete: "showAllQuestions",
|
|
121
|
+
showProgressBar: firstPage.showProgressBar === false ? "off" : "top",
|
|
122
|
+
widthMode: "responsive",
|
|
123
|
+
width: "100%",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Map a field definition to SurveyJS element format
|
|
128
|
+
*/
|
|
129
|
+
function mapFieldToSurveyJS(field) {
|
|
130
|
+
const element = {
|
|
131
|
+
type: field.type,
|
|
132
|
+
name: field.name,
|
|
133
|
+
...(field.displayName && { title: field.displayName }),
|
|
134
|
+
isRequired: field.isRequired ?? field.required ?? false,
|
|
135
|
+
...(field.maxLength && { maxLength: Number(field.maxLength) }),
|
|
136
|
+
};
|
|
137
|
+
if (field.type === "file") {
|
|
138
|
+
element.storeDataAsText = false;
|
|
139
|
+
}
|
|
140
|
+
// Copy other properties
|
|
141
|
+
const excludeKeys = ["displayName", "required", "category"];
|
|
142
|
+
Object.keys(field).forEach((key) => {
|
|
143
|
+
if (!(key in element) && !excludeKeys.includes(key) && field[key] !== undefined) {
|
|
144
|
+
element[key] = field[key];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// Store category for grouping
|
|
148
|
+
element.category = field.category ?? "";
|
|
149
|
+
return element;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve all fields from a page to full field definitions
|
|
153
|
+
*/
|
|
154
|
+
export function resolvePageFields(page, fields) {
|
|
155
|
+
return page.fields
|
|
156
|
+
.map(ref => resolveFieldReference(ref, fields))
|
|
157
|
+
.filter((f) => f !== null)
|
|
158
|
+
.map(f => applyDynamicTitles(f));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get category name from category ID (supports string or number IDs)
|
|
162
|
+
*/
|
|
163
|
+
export function getCategoryName(categoryId, categories) {
|
|
164
|
+
if (categoryId === undefined)
|
|
165
|
+
return "Other";
|
|
166
|
+
const cat = categories.find(c => c.id === categoryId);
|
|
167
|
+
return cat?.name || "Other";
|
|
168
|
+
}
|
|
169
|
+
export function groupFieldsByCategory(fields, categories) {
|
|
170
|
+
const groups = [];
|
|
171
|
+
let currentGroup = null;
|
|
172
|
+
for (const field of fields) {
|
|
173
|
+
const category = field.category ?? "";
|
|
174
|
+
if (!currentGroup || currentGroup.category !== category) {
|
|
175
|
+
currentGroup = {
|
|
176
|
+
category,
|
|
177
|
+
categoryName: getCategoryName(category, categories),
|
|
178
|
+
fields: []
|
|
179
|
+
};
|
|
180
|
+
groups.push(currentGroup);
|
|
181
|
+
}
|
|
182
|
+
currentGroup.fields.push(field);
|
|
183
|
+
}
|
|
184
|
+
return groups;
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateSurveyJson, resolvePageFields, getCategoryName, groupFieldsByCategory, applyDynamicTitles } from './survey-generator.js';
|
|
3
|
+
// ---- Shared fixtures ----
|
|
4
|
+
const categories = [
|
|
5
|
+
{ id: 'personal', name: 'Personal Info' },
|
|
6
|
+
{ id: 'financial', name: 'Financial Details' }
|
|
7
|
+
];
|
|
8
|
+
const baseConfig = {
|
|
9
|
+
displayName: 'Test Loan Application',
|
|
10
|
+
categories,
|
|
11
|
+
fields: {
|
|
12
|
+
fullName: {
|
|
13
|
+
type: 'text',
|
|
14
|
+
displayName: 'Full Name',
|
|
15
|
+
category: 'personal',
|
|
16
|
+
required: true
|
|
17
|
+
},
|
|
18
|
+
email: {
|
|
19
|
+
type: 'text',
|
|
20
|
+
inputType: 'email',
|
|
21
|
+
displayName: 'Email Address',
|
|
22
|
+
category: 'personal',
|
|
23
|
+
required: true
|
|
24
|
+
},
|
|
25
|
+
loanAmount: {
|
|
26
|
+
type: 'number',
|
|
27
|
+
displayName: 'Loan Amount',
|
|
28
|
+
category: 'financial',
|
|
29
|
+
min: 1000,
|
|
30
|
+
max: 500000,
|
|
31
|
+
required: true
|
|
32
|
+
},
|
|
33
|
+
termsAccepted: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
displayName: 'Accept Terms',
|
|
36
|
+
category: 'personal',
|
|
37
|
+
required: false
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
pages: [
|
|
41
|
+
{
|
|
42
|
+
id: 'page1',
|
|
43
|
+
title: 'Application',
|
|
44
|
+
showTOC: true,
|
|
45
|
+
showProgressBar: true,
|
|
46
|
+
showCategoryHeadings: true,
|
|
47
|
+
fields: ['fullName', 'email', 'loanAmount', 'termsAccepted']
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
// ---- generateSurveyJson ----
|
|
52
|
+
describe('generateSurveyJson', () => {
|
|
53
|
+
it('should return empty object if config has no pages', () => {
|
|
54
|
+
const result = generateSurveyJson({ ...baseConfig, pages: [] });
|
|
55
|
+
expect(result).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
it('should return empty object if config.pages is undefined', () => {
|
|
58
|
+
const { pages, ...noPagesConfig } = baseConfig;
|
|
59
|
+
const result = generateSurveyJson(noPagesConfig);
|
|
60
|
+
expect(result).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
it('should return empty object if only page is "success-message"', () => {
|
|
63
|
+
const result = generateSurveyJson({
|
|
64
|
+
...baseConfig,
|
|
65
|
+
pages: [{ id: 'success-message', title: 'Done', fields: [] }]
|
|
66
|
+
});
|
|
67
|
+
expect(result).toEqual({});
|
|
68
|
+
});
|
|
69
|
+
it('should generate valid SurveyJS JSON from a config', () => {
|
|
70
|
+
const result = generateSurveyJson(baseConfig);
|
|
71
|
+
expect(result).toHaveProperty('title', 'Test Loan Application');
|
|
72
|
+
expect(result).toHaveProperty('pages');
|
|
73
|
+
expect(Array.isArray(result.pages)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it('should use displayName as title or fall back to default', () => {
|
|
76
|
+
const noNameConfig = { ...baseConfig, displayName: undefined };
|
|
77
|
+
const result = generateSurveyJson(noNameConfig);
|
|
78
|
+
expect(result.title).toBe('Flexible Financing Program');
|
|
79
|
+
});
|
|
80
|
+
it('should group fields by category into separate pages', () => {
|
|
81
|
+
const result = generateSurveyJson(baseConfig);
|
|
82
|
+
expect(result.pages.length).toBe(2); // personal + financial
|
|
83
|
+
expect(result.pages[0].title).toBe('Personal Info');
|
|
84
|
+
expect(result.pages[1].title).toBe('Financial Details');
|
|
85
|
+
});
|
|
86
|
+
it('should hide category headings when showCategoryHeadings is false', () => {
|
|
87
|
+
const config = {
|
|
88
|
+
...baseConfig,
|
|
89
|
+
pages: [{
|
|
90
|
+
id: 'page1',
|
|
91
|
+
title: 'Test',
|
|
92
|
+
showCategoryHeadings: false,
|
|
93
|
+
fields: ['fullName']
|
|
94
|
+
}]
|
|
95
|
+
};
|
|
96
|
+
const result = generateSurveyJson(config);
|
|
97
|
+
expect(result.pages[0].title).toBe('');
|
|
98
|
+
});
|
|
99
|
+
it('should set showTOC from page config', () => {
|
|
100
|
+
const result = generateSurveyJson(baseConfig);
|
|
101
|
+
expect(result.showTOC).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('should set showProgressBar to "off" when disabled', () => {
|
|
104
|
+
const config = {
|
|
105
|
+
...baseConfig,
|
|
106
|
+
pages: [{ id: 'page1', title: 'Test', showProgressBar: false, fields: ['fullName'] }]
|
|
107
|
+
};
|
|
108
|
+
const result = generateSurveyJson(config);
|
|
109
|
+
expect(result.showProgressBar).toBe('off');
|
|
110
|
+
});
|
|
111
|
+
it('should set storeDataAsText=false for file fields', () => {
|
|
112
|
+
const config = {
|
|
113
|
+
...baseConfig,
|
|
114
|
+
fields: {
|
|
115
|
+
...baseConfig.fields,
|
|
116
|
+
upload: { type: 'file', displayName: 'Upload', category: 'personal' }
|
|
117
|
+
},
|
|
118
|
+
pages: [{ id: 'page1', title: 'Test', fields: ['upload'] }]
|
|
119
|
+
};
|
|
120
|
+
const result = generateSurveyJson(config);
|
|
121
|
+
const fileElement = result.pages[0].elements[0].elements.find((e) => e.name === 'upload');
|
|
122
|
+
expect(fileElement.storeDataAsText).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('should set isRequired based on field config', () => {
|
|
125
|
+
const result = generateSurveyJson(baseConfig);
|
|
126
|
+
const personalElements = result.pages[0].elements[0].elements;
|
|
127
|
+
const fullNameEl = personalElements.find((e) => e.name === 'fullName');
|
|
128
|
+
const termsEl = personalElements.find((e) => e.name === 'termsAccepted');
|
|
129
|
+
expect(fullNameEl.isRequired).toBe(true);
|
|
130
|
+
expect(termsEl.isRequired).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
it('should apply maxLength when specified', () => {
|
|
133
|
+
const config = {
|
|
134
|
+
...baseConfig,
|
|
135
|
+
fields: {
|
|
136
|
+
name: { type: 'text', displayName: 'Name', category: 'personal', maxLength: 50 }
|
|
137
|
+
},
|
|
138
|
+
pages: [{ id: 'page1', title: 'Test', fields: ['name'] }]
|
|
139
|
+
};
|
|
140
|
+
const result = generateSurveyJson(config);
|
|
141
|
+
const el = result.pages[0].elements[0].elements[0];
|
|
142
|
+
expect(el.maxLength).toBe(50);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ---- resolvePageFields ----
|
|
146
|
+
describe('resolvePageFields', () => {
|
|
147
|
+
it('should resolve simple string field references', () => {
|
|
148
|
+
const page = { id: 'p1', title: 'Test', fields: ['fullName', 'email'] };
|
|
149
|
+
const resolved = resolvePageFields(page, baseConfig.fields);
|
|
150
|
+
expect(resolved).toHaveLength(2);
|
|
151
|
+
expect(resolved[0].name).toBe('fullName');
|
|
152
|
+
expect(resolved[1].name).toBe('email');
|
|
153
|
+
});
|
|
154
|
+
it('should resolve ref-with-overrides', () => {
|
|
155
|
+
const page = {
|
|
156
|
+
id: 'p1',
|
|
157
|
+
title: 'Test',
|
|
158
|
+
fields: [
|
|
159
|
+
{ ref: 'fullName', displayName: 'Your Name', required: false }
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
const resolved = resolvePageFields(page, baseConfig.fields);
|
|
163
|
+
expect(resolved).toHaveLength(1);
|
|
164
|
+
expect(resolved[0].displayName).toBe('Your Name');
|
|
165
|
+
expect(resolved[0].required).toBe(false);
|
|
166
|
+
expect(resolved[0].isRequired).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
it('should resolve inline definitions', () => {
|
|
169
|
+
const page = {
|
|
170
|
+
id: 'p1',
|
|
171
|
+
title: 'Test',
|
|
172
|
+
fields: [
|
|
173
|
+
{ definition: { name: 'customField', type: 'text', displayName: 'Custom' } }
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
const resolved = resolvePageFields(page, baseConfig.fields);
|
|
177
|
+
expect(resolved).toHaveLength(1);
|
|
178
|
+
expect(resolved[0].name).toBe('customField');
|
|
179
|
+
expect(resolved[0].type).toBe('text');
|
|
180
|
+
});
|
|
181
|
+
it('should filter out unresolvable references', () => {
|
|
182
|
+
const page = {
|
|
183
|
+
id: 'p1',
|
|
184
|
+
title: 'Test',
|
|
185
|
+
fields: ['fullName', 'nonExistent', 'email']
|
|
186
|
+
};
|
|
187
|
+
const resolved = resolvePageFields(page, baseConfig.fields);
|
|
188
|
+
expect(resolved).toHaveLength(2);
|
|
189
|
+
expect(resolved.map(f => f.name)).toEqual(['fullName', 'email']);
|
|
190
|
+
});
|
|
191
|
+
it('should normalize required/isRequired on resolved fields', () => {
|
|
192
|
+
const page = { id: 'p1', title: 'Test', fields: ['fullName'] };
|
|
193
|
+
const resolved = resolvePageFields(page, baseConfig.fields);
|
|
194
|
+
// fullName has required: true, normalizeField should set both
|
|
195
|
+
expect(resolved[0].required).toBe(true);
|
|
196
|
+
expect(resolved[0].isRequired).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
it('should apply dynamic titles for bank_statement fields', () => {
|
|
199
|
+
const fields = {
|
|
200
|
+
bank_statement_t1: { type: 'file', displayName: 'Bank Statement', category: 'financial' }
|
|
201
|
+
};
|
|
202
|
+
const page = { id: 'p1', title: 'Test', fields: ['bank_statement_t1'] };
|
|
203
|
+
const resolved = resolvePageFields(page, fields);
|
|
204
|
+
expect(resolved[0].displayName).toContain('Bank Statement (');
|
|
205
|
+
});
|
|
206
|
+
it('should apply dynamic titles for financials field', () => {
|
|
207
|
+
const fields = {
|
|
208
|
+
financials: { type: 'file', displayName: 'Financial Statement', category: 'financial' }
|
|
209
|
+
};
|
|
210
|
+
const page = { id: 'p1', title: 'Test', fields: ['financials'] };
|
|
211
|
+
const resolved = resolvePageFields(page, fields);
|
|
212
|
+
expect(resolved[0].displayName).toContain('Audited Financial Statement (');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
// ---- getCategoryName ----
|
|
216
|
+
describe('getCategoryName', () => {
|
|
217
|
+
it('should return category name when ID matches', () => {
|
|
218
|
+
expect(getCategoryName('personal', categories)).toBe('Personal Info');
|
|
219
|
+
expect(getCategoryName('financial', categories)).toBe('Financial Details');
|
|
220
|
+
});
|
|
221
|
+
it('should return "Other" for undefined categoryId', () => {
|
|
222
|
+
expect(getCategoryName(undefined, categories)).toBe('Other');
|
|
223
|
+
});
|
|
224
|
+
it('should return "Other" for unrecognized categoryId', () => {
|
|
225
|
+
expect(getCategoryName('nonexistent', categories)).toBe('Other');
|
|
226
|
+
});
|
|
227
|
+
it('should return "Other" for empty categories array', () => {
|
|
228
|
+
expect(getCategoryName('personal', [])).toBe('Other');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
// ---- groupFieldsByCategory ----
|
|
232
|
+
describe('groupFieldsByCategory', () => {
|
|
233
|
+
const makeField = (name, category) => ({
|
|
234
|
+
name,
|
|
235
|
+
type: 'text',
|
|
236
|
+
category,
|
|
237
|
+
required: true,
|
|
238
|
+
isRequired: true
|
|
239
|
+
});
|
|
240
|
+
it('should group fields by category in order', () => {
|
|
241
|
+
const fields = [
|
|
242
|
+
makeField('a', 'personal'),
|
|
243
|
+
makeField('b', 'personal'),
|
|
244
|
+
makeField('c', 'financial'),
|
|
245
|
+
makeField('d', 'financial')
|
|
246
|
+
];
|
|
247
|
+
const groups = groupFieldsByCategory(fields, categories);
|
|
248
|
+
expect(groups).toHaveLength(2);
|
|
249
|
+
expect(groups[0].category).toBe('personal');
|
|
250
|
+
expect(groups[0].categoryName).toBe('Personal Info');
|
|
251
|
+
expect(groups[0].fields).toHaveLength(2);
|
|
252
|
+
expect(groups[1].category).toBe('financial');
|
|
253
|
+
expect(groups[1].fields).toHaveLength(2);
|
|
254
|
+
});
|
|
255
|
+
it('should create separate groups for interleaved categories', () => {
|
|
256
|
+
const fields = [
|
|
257
|
+
makeField('a', 'personal'),
|
|
258
|
+
makeField('b', 'financial'),
|
|
259
|
+
makeField('c', 'personal')
|
|
260
|
+
];
|
|
261
|
+
const groups = groupFieldsByCategory(fields, categories);
|
|
262
|
+
// Categories alternate, so 3 groups: personal, financial, personal
|
|
263
|
+
expect(groups).toHaveLength(3);
|
|
264
|
+
expect(groups[0].category).toBe('personal');
|
|
265
|
+
expect(groups[1].category).toBe('financial');
|
|
266
|
+
expect(groups[2].category).toBe('personal');
|
|
267
|
+
});
|
|
268
|
+
it('should return empty array for empty fields', () => {
|
|
269
|
+
const groups = groupFieldsByCategory([], categories);
|
|
270
|
+
expect(groups).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
it('should use "Other" for fields with no matching category', () => {
|
|
273
|
+
const fields = [makeField('a', 'unknown')];
|
|
274
|
+
const groups = groupFieldsByCategory(fields, categories);
|
|
275
|
+
expect(groups[0].categoryName).toBe('Other');
|
|
276
|
+
});
|
|
277
|
+
it('should handle fields with undefined category', () => {
|
|
278
|
+
const field = makeField('a', '');
|
|
279
|
+
field.category = undefined;
|
|
280
|
+
const groups = groupFieldsByCategory([field], categories);
|
|
281
|
+
expect(groups[0].category).toBe('');
|
|
282
|
+
expect(groups[0].categoryName).toBe('Other');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
// ---- applyDynamicTitles ----
|
|
286
|
+
describe('applyDynamicTitles', () => {
|
|
287
|
+
const makeField = (name, displayName) => ({
|
|
288
|
+
name,
|
|
289
|
+
type: 'file',
|
|
290
|
+
displayName,
|
|
291
|
+
required: true,
|
|
292
|
+
isRequired: true
|
|
293
|
+
});
|
|
294
|
+
it('should transform bank_statement_t1 with month label', () => {
|
|
295
|
+
const result = applyDynamicTitles(makeField('bank_statement_t1', 'Bank Statement'));
|
|
296
|
+
expect(result.displayName).toMatch(/^Bank Statement \(\w+\)$/);
|
|
297
|
+
});
|
|
298
|
+
it('should transform bank_statement_t3 with 3-month-ago label', () => {
|
|
299
|
+
const result = applyDynamicTitles(makeField('bank_statement_t3'));
|
|
300
|
+
expect(result.displayName).toMatch(/^Bank Statement \(\w+\)$/);
|
|
301
|
+
});
|
|
302
|
+
it('should transform financials field with year label', () => {
|
|
303
|
+
const result = applyDynamicTitles(makeField('financials', 'Financial Statement'));
|
|
304
|
+
const expectedYear = (new Date().getFullYear() - 1).toString();
|
|
305
|
+
expect(result.displayName).toBe(`Audited Financial Statement (${expectedYear})`);
|
|
306
|
+
});
|
|
307
|
+
it('should not modify regular fields', () => {
|
|
308
|
+
const field = makeField('fullName', 'Full Name');
|
|
309
|
+
const result = applyDynamicTitles(field);
|
|
310
|
+
expect(result.displayName).toBe('Full Name');
|
|
311
|
+
});
|
|
312
|
+
it('should not modify fields with similar but non-matching names', () => {
|
|
313
|
+
const field = makeField('bank_statement', 'Statement');
|
|
314
|
+
const result = applyDynamicTitles(field);
|
|
315
|
+
expect(result.displayName).toBe('Statement');
|
|
316
|
+
});
|
|
317
|
+
});
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for @finsys/core
|
|
3
|
+
*/
|
|
4
|
+
export declare function getPastMonthLabel(monthsAgo: number): string;
|
|
5
|
+
export declare function getPastYearLabel(yearsAgo: number): string;
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate a SurveyJS-style expression against a data object.
|
|
8
|
+
* e.g. "{totalFinancing} > 50000"
|
|
9
|
+
*
|
|
10
|
+
* NOTE: This is a simplified evaluator that replaces {key} with data[key] access.
|
|
11
|
+
* It does not support complex SurveyJS functions like age() or complicated nested paths unless handled by JS.
|
|
12
|
+
*/
|
|
13
|
+
export declare function evaluateExpression(expression: string, data: any): boolean;
|