From f840f1c3b44d56e81e96d8df1188c0e2a9f19e4a Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Mon, 2 Mar 2026 20:27:57 +0300 Subject: [PATCH 1/3] feat: implement custom validators for form authors - Add CustomValidatorLoader, Registry, and Executor services - Extend FormInitData interface to include validators in manifest - Scan validators folder in FormplayerModal and include in manifest - Load validators during form initialization - Execute validators on data changes in JSON Forms lifecycle - Convert validator errors to AJV-compatible format - Merge custom errors with standard validation via additionalErrors - Support cross-field validation via full form data access - Implement graceful failure handling Implements all 8 user stories: - US01: Define custom validators as JS modules - US02: Reference validators in UI schema options - US03: Execute in JSON Forms lifecycle - US04: Provide full form context to validators - US05: Return standardized validation errors - US06: Display custom validation messages - US07: Support reusable validators with different configs - US08: Graceful validator failure handling --- formulus-formplayer/src/App.tsx | 87 +++++- .../src/services/CustomValidatorExecutor.ts | 251 ++++++++++++++++++ .../src/services/CustomValidatorLoader.ts | 128 +++++++++ .../src/services/CustomValidatorRegistry.ts | 88 ++++++ .../src/types/CustomValidatorContract.ts | 95 +++++++ .../src/types/FormulusInterfaceDefinition.ts | 1 + formulus/src/components/FormplayerModal.tsx | 58 +++- .../webview/FormulusInterfaceDefinition.ts | 1 + 8 files changed, 691 insertions(+), 18 deletions(-) create mode 100644 formulus-formplayer/src/services/CustomValidatorExecutor.ts create mode 100644 formulus-formplayer/src/services/CustomValidatorLoader.ts create mode 100644 formulus-formplayer/src/services/CustomValidatorRegistry.ts create mode 100644 formulus-formplayer/src/types/CustomValidatorContract.ts diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index 8ab18b5c0..a5f6e0479 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -26,6 +26,7 @@ import { import { createTheme, getThemeOptions, CustomThemeColors } from './theme/theme'; import { tokens } from './theme/tokens-adapter'; import Ajv from 'ajv'; +import type { ErrorObject } from 'ajv'; import addErrors from 'ajv-errors'; import addFormats from 'ajv-formats'; import * as MUI from '@mui/material'; @@ -77,6 +78,13 @@ import { loadExtensions } from './services/ExtensionsLoader'; import { getBuiltinExtensions } from './builtinExtensions'; import { FormEvaluationProvider } from './FormEvaluationContext'; import { loadCustomQuestionTypes } from './services/CustomQuestionTypeLoader'; +import { loadCustomValidators } from './services/CustomValidatorLoader'; +import { customValidatorRegistry } from './services/CustomValidatorRegistry'; +import { + executeAllCustomValidators, + extractCustomValidators, + executeCustomValidators, +} from './services/CustomValidatorExecutor'; // Import development dependencies (Vite will tree-shake these in production) import { webViewMock } from './mocks/webview-mock'; @@ -298,6 +306,10 @@ function App() { JsonFormsRendererRegistryEntry[] >([]); const [customTypeFormats, setCustomTypeFormats] = useState([]); + // Custom validator errors (merged with AJV errors) + const [customValidatorErrors, setCustomValidatorErrors] = useState< + ErrorObject[] + >([]); // Reference to the FormulusClient instance and loading state const formulusClient = useRef(FormulusClient.getInstance()); @@ -418,6 +430,29 @@ function App() { customQTResult.errors, ); } + + // Load custom validators if provided + if (customQTManifest.validators) { + try { + const validatorResult = + await loadCustomValidators(customQTManifest); + customValidatorRegistry.registerAll(validatorResult.validators); + console.log( + `[Formplayer] Loaded ${validatorResult.validators.size} custom validator(s): ${Array.from(validatorResult.validators.keys()).join(', ')}`, + ); + if (validatorResult.errors.length > 0) { + console.warn( + '[Formplayer] Custom validator loading errors:', + validatorResult.errors, + ); + } + } catch (error) { + console.error( + '[Formplayer] Failed to load custom validators:', + error, + ); + } + } } catch (error) { console.error( '[Formplayer] Failed to load custom question types:', @@ -827,18 +862,6 @@ function App() { } }, [pendingFormInit, initializeForm]); - const handleDataChange = useCallback( - ({ data }: { data: FormData }) => { - setData(data); - - // Save draft data whenever form data changes - if (formInitData) { - draftService.saveDraft(formInitData.formType, data, formInitData); - } - }, - [formInitData], - ); - // Create AJV instance with extension definitions support const ajv = useMemo(() => { const instance = new Ajv({ @@ -891,6 +914,45 @@ function App() { return instance; }, [extensionDefinitions, customTypeFormats]); + const handleDataChange = useCallback( + ({ data: newData }: { data: FormData }) => { + setData(newData); + + // Save draft data whenever form data changes + if (formInitData) { + draftService.saveDraft(formInitData.formType, newData, formInitData); + } + + // Execute custom validators when data changes + if (uischema && schema) { + try { + const customErrors = executeAllCustomValidators( + uischema, + schema, + newData, + ajv, + ); + + // Flatten errors map to array + const allCustomErrors: ErrorObject[] = []; + for (const fieldErrors of customErrors.values()) { + allCustomErrors.push(...fieldErrors); + } + + setCustomValidatorErrors(allCustomErrors); + } catch (error) { + console.error( + '[Formplayer] Error executing custom validators:', + error, + ); + // Graceful failure: clear errors on execution failure + setCustomValidatorErrors([]); + } + } + }, + [formInitData, uischema, schema, ajv], + ); + // Create dynamic theme based on dark mode preference and custom app colors. // When a custom app provides themeColors, they override the default palette // so that form controls (buttons, inputs, etc.) match the app's branding. @@ -1076,6 +1138,7 @@ function App() { onChange={handleDataChange} validationMode="ValidateAndShow" ajv={ajv} + additionalErrors={customValidatorErrors} /> {/* Success Snackbar */} diff --git a/formulus-formplayer/src/services/CustomValidatorExecutor.ts b/formulus-formplayer/src/services/CustomValidatorExecutor.ts new file mode 100644 index 000000000..1e6321e7c --- /dev/null +++ b/formulus-formplayer/src/services/CustomValidatorExecutor.ts @@ -0,0 +1,251 @@ +/** + * CustomValidatorExecutor.ts + * + * Executes custom validators in the JSON Forms validation lifecycle. + * Extracts validators from UI schema options, executes them with form context, + * and converts errors to JSON Forms/AJV-compatible format. + */ + +import type { ErrorObject } from 'ajv'; +import type { JsonSchema7, UISchemaElement } from '@jsonforms/core'; +import type { + CustomValidatorFunction, + ValidationError as CustomValidationError, +} from '../types/CustomValidatorContract'; +import { customValidatorRegistry } from './CustomValidatorRegistry'; +import type Ajv from 'ajv'; + +/** + * Configuration for a custom validator reference in UI schema. + */ +export interface CustomValidatorReference { + /** Validator name (must match registry) */ + name: string; + /** Validator configuration object */ + config?: Record; +} + +/** + * Extract custom validators from UI schema options. + * + * @param uischema - UI schema element (Control, Layout, etc.) + * @returns Array of validator references, or empty array + */ +export function extractCustomValidators( + uischema: UISchemaElement | undefined, +): CustomValidatorReference[] { + if (!uischema || typeof uischema !== 'object') { + return []; + } + + // Check if this is a Control with options.customValidators + const options = (uischema as any).options; + if (!options || typeof options !== 'object') { + return []; + } + + const customValidators = options.customValidators; + if (!Array.isArray(customValidators)) { + return []; + } + + return customValidators + .filter((ref: any) => ref && typeof ref === 'object' && ref.name) + .map((ref: any) => ({ + name: String(ref.name), + config: ref.config || {}, + })); +} + +/** + * Convert a data path (e.g., "#/properties/age") to an instance path (e.g., "/age"). + * + * @param path - JSON Schema path + * @returns Instance path for AJV errors + */ +function pathToInstancePath(path: string): string { + // Remove "#/properties/" prefix and convert to instance path + if (path.startsWith('#/properties/')) { + return '/' + path.replace('#/properties/', '').replace(/\//g, '/'); + } + // If already an instance path, return as-is + if (path.startsWith('/')) { + return path; + } + // Fallback: remove leading "#" and convert + return path.replace(/^#/, '').replace(/\/properties\//g, '/'); +} + +/** + * Convert custom validation errors to AJV ErrorObject format. + * + * @param errors - Custom validation errors + * @param fieldPath - Field path (e.g., "#/properties/age") + * @returns AJV-compatible error objects + */ +function convertToAjvErrors( + errors: CustomValidationError[], + fieldPath: string, +): ErrorObject[] { + const instancePath = pathToInstancePath(fieldPath); + + return errors.map(error => { + const ajvError: ErrorObject = { + instancePath: error.path ? pathToInstancePath(error.path) : instancePath, + schemaPath: fieldPath, + keyword: error.keyword || 'customValidator', + params: error.params || {}, + message: error.message, + }; + return ajvError; + }); +} + +/** + * Execute custom validators for a specific field. + * + * @param validatorRefs - Array of validator references from UI schema + * @param data - Full form data + * @param value - Current field value + * @param path - Field path (e.g., "#/properties/age") + * @param ajv - Optional AJV instance + * @returns Array of AJV-compatible error objects + */ +export function executeCustomValidators( + validatorRefs: CustomValidatorReference[], + data: Record, + value: unknown, + path: string, + ajv?: Ajv, +): ErrorObject[] { + const allErrors: ErrorObject[] = []; + + for (const ref of validatorRefs) { + const validator = customValidatorRegistry.get(ref.name); + + if (!validator) { + console.warn( + `[CustomValidatorExecutor] Validator "${ref.name}" not found in registry. ` + + `Available validators: ${customValidatorRegistry.getNames().join(', ')}`, + ); + continue; + } + + try { + // Execute validator with full context + const result = validator({ + data, + value, + path, + config: ref.config || {}, + ajv, + }); + + // Convert result to array of errors + const errors: CustomValidationError[] = Array.isArray(result) + ? result + : result !== undefined && result !== null + ? [result as CustomValidationError] + : []; + + // Convert to AJV format + const ajvErrors = convertToAjvErrors(errors, path); + allErrors.push(...ajvErrors); + + if (ajvErrors.length > 0) { + console.debug( + `[CustomValidatorExecutor] Validator "${ref.name}" returned ${ajvErrors.length} error(s) for path "${path}"`, + ); + } + } catch (err) { + // Graceful failure: log error but don't crash + const errorMessage = err instanceof Error ? err.message : String(err); + console.error( + `[CustomValidatorExecutor] Validator "${ref.name}" failed for path "${path}":`, + errorMessage, + ); + // Optionally return a generic error to indicate validator failure + // For now, we just log and continue (graceful degradation) + } + } + + return allErrors; +} + +/** + * Execute custom validators for all fields in the form. + * Scans the UI schema to find all fields with custom validators and executes them. + * + * @param uischema - Root UI schema + * @param schema - JSON schema + * @param data - Full form data + * @param ajv - Optional AJV instance + * @returns Map of field path → array of AJV error objects + */ +export function executeAllCustomValidators( + uischema: UISchemaElement | undefined, + schema: JsonSchema7 | undefined, + data: Record, + ajv?: Ajv, +): Map { + const errors = new Map(); + + if (!uischema || !schema) { + return errors; + } + + // Recursively traverse UI schema to find all Controls with custom validators + function traverseUISchema( + element: UISchemaElement | UISchemaElement[] | undefined, + currentPath: string = '', + ): void { + if (!element) { + return; + } + + if (Array.isArray(element)) { + element.forEach(item => traverseUISchema(item, currentPath)); + return; + } + + const elem = element as any; + + // If this is a Control, check for custom validators + if (elem.type === 'Control' && elem.scope) { + const validatorRefs = extractCustomValidators(elem); + if (validatorRefs.length > 0) { + // Extract field path from scope (e.g., "#/properties/age" -> "age") + const fieldPath = elem.scope; + const fieldName = fieldPath.replace('#/properties/', ''); + const fieldValue = data[fieldName]; + + // Execute validators for this field + const fieldErrors = executeCustomValidators( + validatorRefs, + data, + fieldValue, + fieldPath, + ajv, + ); + + if (fieldErrors.length > 0) { + errors.set(fieldPath, fieldErrors); + } + } + } + + // Recursively process children + if (elem.elements) { + traverseUISchema(elem.elements, currentPath); + } + if (elem.elements && Array.isArray(elem.elements)) { + elem.elements.forEach((child: UISchemaElement) => + traverseUISchema(child, currentPath), + ); + } + } + + traverseUISchema(uischema); + + return errors; +} diff --git a/formulus-formplayer/src/services/CustomValidatorLoader.ts b/formulus-formplayer/src/services/CustomValidatorLoader.ts new file mode 100644 index 000000000..76776e99b --- /dev/null +++ b/formulus-formplayer/src/services/CustomValidatorLoader.ts @@ -0,0 +1,128 @@ +/** + * CustomValidatorLoader.ts + * + * Loads custom validator modules from source strings. + * The native Formulus RN side reads each validator's JS source and + * passes it in the manifest. This loader evaluates each source in + * a CommonJS-compatible sandbox. + * + * This loader: + * 1. Iterates over the manifest + * 2. Evaluates each module's source with CommonJS shims (module, exports) + * 3. Validates the default export is a function (validator function) + * 4. Passes all loaded validators to the registry + * 5. Returns loaded validators map + * + * Custom validators are referenced in UI schema options.customValidators. + */ + +import type { + CustomValidatorFunction, + CustomValidatorManifest, + FormplayerManifest, +} from '../types/CustomValidatorContract'; + +export interface CustomValidatorLoadResult { + /** Map of validator name → validator function */ + validators: Map; + /** Any errors that occurred during loading */ + errors: Array<{ name: string; error: string }>; +} + +/** + * Load custom validators from a manifest containing source strings. + * + * @param manifest - The manifest describing available custom validators (with source code) + * @returns Loaded validators map and any errors + */ +export async function loadCustomValidators( + manifest: FormplayerManifest | CustomValidatorManifest, +): Promise { + const result: CustomValidatorLoadResult = { + validators: new Map(), + errors: [], + }; + + // Extract validators from manifest (support both formats) + const validatorsManifest = + 'validators' in manifest + ? manifest.validators + : (manifest as FormplayerManifest).validators; + + if (!validatorsManifest || Object.keys(validatorsManifest).length === 0) { + console.log('[CustomValidatorLoader] No custom validators in manifest'); + return result; + } + + for (const [validatorName, meta] of Object.entries(validatorsManifest)) { + try { + console.log( + `[CustomValidatorLoader] Loading "${validatorName}" (${meta.source.length} bytes)`, + ); + + // Create a CommonJS-compatible sandbox with module/exports shims + // The validators use: module.exports = { default: validateFunction } + const moduleShim: { exports: Record } = { + exports: {}, + }; + const exportsShim = moduleShim.exports; + + // Evaluate the source in a function scope with CommonJS shims + // Validators don't need React or MaterialUI, but we can provide them if needed + const factory = new Function('module', 'exports', meta.source); + factory(moduleShim, exportsShim); + + // Extract the validator: try module.exports.default, then module.exports itself + const validator = moduleShim.exports.default ?? moduleShim.exports; + + // Validate that the export is a function + if (typeof validator !== 'function') { + throw new Error( + `Module does not export a valid validator function. ` + + `Expected a function, got ${typeof validator}. ` + + `Make sure your module exports a default function.`, + ); + } + + // Validate function signature by checking it accepts one parameter + // We can't check the exact structure, but we can verify it's callable + if (validator.length === 0) { + console.warn( + `[CustomValidatorLoader] Validator "${validatorName}" doesn't accept parameters. ` + + `It should accept { data, value, path, config, ajv }`, + ); + } + + result.validators.set( + validatorName, + validator as CustomValidatorFunction, + ); + + console.log( + `[CustomValidatorLoader] Successfully loaded "${validatorName}"`, + ); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.error( + `[CustomValidatorLoader] Failed to load "${validatorName}":`, + errorMessage, + ); + result.errors.push({ name: validatorName, error: errorMessage }); + } + } + + if (result.validators.size > 0) { + console.log( + `[CustomValidatorLoader] Loaded ${result.validators.size} custom validator(s)`, + ); + } + + if (result.errors.length > 0) { + console.warn( + `[CustomValidatorLoader] ${result.errors.length} validator(s) failed to load:`, + result.errors.map(e => e.name).join(', '), + ); + } + + return result; +} diff --git a/formulus-formplayer/src/services/CustomValidatorRegistry.ts b/formulus-formplayer/src/services/CustomValidatorRegistry.ts new file mode 100644 index 000000000..57344c692 --- /dev/null +++ b/formulus-formplayer/src/services/CustomValidatorRegistry.ts @@ -0,0 +1,88 @@ +/** + * CustomValidatorRegistry.ts + * + * Registry for loaded custom validators. + * Provides lookup by name and manages the validator lifecycle. + * + * Usage: + * const registry = new CustomValidatorRegistry(); + * registry.register('isAdult', validatorFunction); + * const validator = registry.get('isAdult'); + */ + +import type { CustomValidatorFunction } from '../types/CustomValidatorContract'; + +/** + * Registry for custom validators. + * Provides thread-safe lookup and registration of validators. + */ +export class CustomValidatorRegistry { + private validators: Map = new Map(); + + /** + * Register a validator function with a name. + * + * @param name - Validator name (must match UI schema reference) + * @param validator - Validator function + */ + register(name: string, validator: CustomValidatorFunction): void { + if (this.validators.has(name)) { + console.warn( + `[CustomValidatorRegistry] Validator "${name}" is already registered. Overwriting.`, + ); + } + this.validators.set(name, validator); + console.log(`[CustomValidatorRegistry] Registered validator "${name}"`); + } + + /** + * Register multiple validators at once. + * + * @param validators - Map of name → validator function + */ + registerAll(validators: Map): void { + for (const [name, validator] of validators) { + this.register(name, validator); + } + } + + /** + * Get a validator by name. + * + * @param name - Validator name + * @returns Validator function, or undefined if not found + */ + get(name: string): CustomValidatorFunction | undefined { + return this.validators.get(name); + } + + /** + * Check if a validator is registered. + * + * @param name - Validator name + * @returns True if validator exists + */ + has(name: string): boolean { + return this.validators.has(name); + } + + /** + * Get all registered validator names. + * + * @returns Array of validator names + */ + getNames(): string[] { + return Array.from(this.validators.keys()); + } + + /** + * Clear all registered validators. + */ + clear(): void { + this.validators.clear(); + console.log('[CustomValidatorRegistry] Cleared all validators'); + } +} + +// Singleton instance for global access +export const customValidatorRegistry = new CustomValidatorRegistry(); diff --git a/formulus-formplayer/src/types/CustomValidatorContract.ts b/formulus-formplayer/src/types/CustomValidatorContract.ts new file mode 100644 index 000000000..37344e445 --- /dev/null +++ b/formulus-formplayer/src/types/CustomValidatorContract.ts @@ -0,0 +1,95 @@ +/** + * CustomValidatorContract.ts + * + * Defines the public interface that custom validators must follow. + * Form authors create validator functions that accept these parameters and return validation errors. + * + * Usage in UI Schema: + * { + * "type": "Control", + * "scope": "#/properties/age", + * "options": { + * "customValidators": [ + * { + * "name": "isAdult", + * "config": { "minAge": 18 } + * } + * ] + * } + * } + * + * Usage in custom_app: + * custom_app/validators/isAdult/index.js + * module.exports = { + * default: function validate({ value, config, path, data }) { + * const minAge = config.minAge || 18; + * if (typeof value !== 'number' || value < minAge) { + * return [{ path, message: `Must be at least ${minAge} years old` }]; + * } + * return []; + * } + * }; + */ + +import type Ajv from 'ajv'; + +/** + * A validation error returned by a custom validator. + */ +export interface ValidationError { + /** Data path of the field (e.g., "#/properties/age" or "/age") */ + path: string; + /** Human-readable error message */ + message: string; + /** Optional keyword for error categorization (defaults to "customValidator") */ + keyword?: string; + /** Optional additional parameters */ + params?: Record; +} + +/** + * Parameters passed to a custom validator function. + */ +export interface CustomValidatorParams { + /** Full form data object (all field values) */ + data: Record; + /** Current field value being validated */ + value: unknown; + /** Data path of the field (e.g., "#/properties/age") */ + path: string; + /** Validator configuration object from UI schema */ + config: Record; + /** Optional AJV instance for schema validation utilities */ + ajv?: Ajv; +} + +/** + * Custom validator function signature. + * Returns an array of validation errors, or an empty array/void if validation passes. + */ +export type CustomValidatorFunction = ( + params: CustomValidatorParams, +) => ValidationError[] | void; + +/** + * Manifest passed from the native side describing available custom validators. + * Each entry maps a validator name to the source code of the module that implements it. + * The RN side reads the JS file and passes the source string here for sandboxed evaluation. + */ +export interface CustomValidatorManifest { + validators: Record< + string, + { + /** The JS source code of the module (read by RN via RNFS.readFile) */ + source: string; + } + >; +} + +/** + * Extended manifest that includes both question types and validators. + */ +export interface FormplayerManifest { + custom_types?: Record; + validators?: Record; +} diff --git a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts index e048cd8dc..f54b6191b 100644 --- a/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts +++ b/formulus-formplayer/src/types/FormulusInterfaceDefinition.ts @@ -59,6 +59,7 @@ export interface FormInitData { extensions?: ExtensionMetadata; customQuestionTypes?: { custom_types: Record; + validators?: Record; }; } diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index e9e0b9e92..61f9e36d1 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -316,8 +316,8 @@ const FormplayerModal = forwardRef( return; } - // Scan custom question types and read their source code - // Check app/question_types (bundle root) and app/forms/question_types (legacy) + // Scan custom question types and validators, read their source code + // Check app/question_types and app/validators (bundle root) and app/forms/question_types, app/forms/validators (legacy) let customQuestionTypes = undefined; try { const qtDirs = [ @@ -326,8 +326,16 @@ const FormplayerModal = forwardRef( RNFS.DocumentDirectoryPath + '/forms/question_types', ]; + const validatorDirs = [ + `${customAppPath}/validators`, + `${customAppPath}/forms/validators`, + RNFS.DocumentDirectoryPath + '/forms/validators', + ]; + const custom_types: Record = {}; + const validators: Record = {}; + // Scan custom question types for (const qtDir of qtDirs) { const qtDirExists = await RNFS.exists(qtDir); if (!qtDirExists) { @@ -365,15 +373,53 @@ const FormplayerModal = forwardRef( } } - if (Object.keys(custom_types).length > 0) { - customQuestionTypes = { custom_types }; + // Scan custom validators + for (const validatorDir of validatorDirs) { + const validatorDirExists = await RNFS.exists(validatorDir); + if (!validatorDirExists) { + continue; + } + + const folders = await RNFS.readDir(validatorDir); + + for (const folder of folders) { + if (folder.isDirectory() && !validators[folder.name]) { + // Validators use index.js (standard convention) + const indexPath = `${folder.path}/index.js`; + const hasIndex = await RNFS.exists(indexPath); + + if (hasIndex) { + // Read the source code so the WebView can evaluate it directly + const source = await RNFS.readFile(indexPath, 'utf8'); + validators[folder.name] = { source }; + console.log( + `[FormplayerModal] Custom validator: "${folder.name}" (${source.length} bytes from ${indexPath})`, + ); + } else { + console.warn( + `[FormplayerModal] Skipping validator "${folder.name}": no index.js found`, + ); + } + } + } + } + + // Build manifest with both question types and validators + if ( + Object.keys(custom_types).length > 0 || + Object.keys(validators).length > 0 + ) { + customQuestionTypes = { + custom_types: Object.keys(custom_types).length > 0 ? custom_types : undefined, + validators: Object.keys(validators).length > 0 ? validators : undefined, + }; } else { console.warn( - '[FormplayerModal] No custom question types found in any path', + '[FormplayerModal] No custom question types or validators found in any path', ); } } catch (error) { - console.warn('Failed to scan custom question types:', error); + console.warn('Failed to scan custom question types and validators:', error); } const formInitData = { diff --git a/formulus/src/webview/FormulusInterfaceDefinition.ts b/formulus/src/webview/FormulusInterfaceDefinition.ts index e048cd8dc..f54b6191b 100644 --- a/formulus/src/webview/FormulusInterfaceDefinition.ts +++ b/formulus/src/webview/FormulusInterfaceDefinition.ts @@ -59,6 +59,7 @@ export interface FormInitData { extensions?: ExtensionMetadata; customQuestionTypes?: { custom_types: Record; + validators?: Record; }; } From 11ae678e8d3cb74b1d8d690fa24ca09d43d7f67c Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Tue, 3 Mar 2026 00:06:39 +0300 Subject: [PATCH 2/3] fix: remove unused imports to resolve linting errors - Remove unused extractCustomValidators and executeCustomValidators imports from App.tsx - Remove unused CustomValidatorFunction import from CustomValidatorExecutor.ts --- formulus-formplayer/src/App.tsx | 6 +----- formulus-formplayer/src/services/CustomValidatorExecutor.ts | 5 +---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index a5f6e0479..052abd2fa 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -80,11 +80,7 @@ import { FormEvaluationProvider } from './FormEvaluationContext'; import { loadCustomQuestionTypes } from './services/CustomQuestionTypeLoader'; import { loadCustomValidators } from './services/CustomValidatorLoader'; import { customValidatorRegistry } from './services/CustomValidatorRegistry'; -import { - executeAllCustomValidators, - extractCustomValidators, - executeCustomValidators, -} from './services/CustomValidatorExecutor'; +import { executeAllCustomValidators } from './services/CustomValidatorExecutor'; // Import development dependencies (Vite will tree-shake these in production) import { webViewMock } from './mocks/webview-mock'; diff --git a/formulus-formplayer/src/services/CustomValidatorExecutor.ts b/formulus-formplayer/src/services/CustomValidatorExecutor.ts index 1e6321e7c..8d74d76cb 100644 --- a/formulus-formplayer/src/services/CustomValidatorExecutor.ts +++ b/formulus-formplayer/src/services/CustomValidatorExecutor.ts @@ -8,10 +8,7 @@ import type { ErrorObject } from 'ajv'; import type { JsonSchema7, UISchemaElement } from '@jsonforms/core'; -import type { - CustomValidatorFunction, - ValidationError as CustomValidationError, -} from '../types/CustomValidatorContract'; +import type { ValidationError as CustomValidationError } from '../types/CustomValidatorContract'; import { customValidatorRegistry } from './CustomValidatorRegistry'; import type Ajv from 'ajv'; From 401e0477e7b1e0a1f119a2b8d09b06e1528fca4a Mon Sep 17 00:00:00 2001 From: Mishael-2584 Date: Tue, 3 Mar 2026 00:22:33 +0300 Subject: [PATCH 3/3] fix: apply prettier formatting to formulus custom validators files --- formulus/src/components/FormplayerModal.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 61f9e36d1..3f0cff742 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -410,8 +410,10 @@ const FormplayerModal = forwardRef( Object.keys(validators).length > 0 ) { customQuestionTypes = { - custom_types: Object.keys(custom_types).length > 0 ? custom_types : undefined, - validators: Object.keys(validators).length > 0 ? validators : undefined, + custom_types: + Object.keys(custom_types).length > 0 ? custom_types : undefined, + validators: + Object.keys(validators).length > 0 ? validators : undefined, }; } else { console.warn( @@ -419,7 +421,10 @@ const FormplayerModal = forwardRef( ); } } catch (error) { - console.warn('Failed to scan custom question types and validators:', error); + console.warn( + 'Failed to scan custom question types and validators:', + error, + ); } const formInitData = {