From 9cd47826e35626450aadde5773243c2f33c0f8de Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Fri, 14 Nov 2025 06:35:05 +0000 Subject: [PATCH 1/5] initial work on form config migration using the visitor pattern --- .../visitor/migrate-config-v4-v5.visitor.ts | 389 ++++++++++++++++++ .../unit/migrate-config-v4-v5.visitor.test.ts | 35 ++ 2 files changed, 424 insertions(+) create mode 100644 packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts create mode 100644 packages/sails-ng-common/test/unit/migrate-config-v4-v5.visitor.test.ts diff --git a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts new file mode 100644 index 000000000..b336f7ffc --- /dev/null +++ b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts @@ -0,0 +1,389 @@ +import {cloneDeep as _cloneDeep} from "lodash"; +import {FormConfigFrame, FormConfigOutline} from "../form-config.outline"; +import {FormModel, ILogger} from "@researchdatabox/redbox-core-types"; +import {CurrentPathFormConfigVisitor} from "./base.model"; +import {FormConfig} from "../form-config.model"; +import { + ComponentClassDefMapType, + FieldComponentDefinitionMap, + FieldLayoutDefinitionMap, + FieldModelDefinitionMap, + FormComponentClassDefMapType, + FormComponentDefinitionMap, + LayoutClassDefMapType, + ModelClassDefMapType +} from "../dictionary.model"; +import { + ContentFieldComponentConfig, + ContentFieldComponentDefinition, + ContentFormComponentDefinition +} from "../component/content.model"; +import { + SimpleInputFieldComponentDefinitionOutline, + SimpleInputFieldModelDefinitionOutline, SimpleInputFormComponentDefinitionOutline +} from "../component/simple-input.outline"; +import { + ContentFieldComponentDefinitionOutline, + ContentFormComponentDefinitionOutline +} from "../component/content.outline"; +import { + RepeatableElementFieldLayoutDefinitionOutline, + RepeatableFieldComponentDefinitionOutline, + RepeatableFieldModelDefinitionOutline, RepeatableFormComponentDefinitionOutline +} from "../component/repeatable.outline"; +import { + ValidationSummaryFieldComponentDefinitionOutline, + ValidationSummaryFormComponentDefinitionOutline +} from "../component/validation-summary.outline"; +import { + GroupFieldComponentDefinitionOutline, + GroupFieldModelDefinitionOutline, + GroupFormComponentDefinitionOutline +} from "../component/group.outline"; +import { + TabFieldComponentDefinitionOutline, + TabFieldLayoutDefinitionOutline, + TabFormComponentDefinitionOutline +} from "../component/tab.outline"; +import { + TabContentFieldComponentDefinitionOutline, + TabContentFieldLayoutDefinitionOutline, TabContentFormComponentDefinitionOutline +} from "../component/tab-content.outline"; +import { + SaveButtonFieldComponentDefinitionOutline, + SaveButtonFormComponentDefinitionOutline +} from "../component/save-button.outline"; +import { + TextAreaFieldComponentDefinitionOutline, + TextAreaFieldModelDefinitionOutline, TextAreaFormComponentDefinitionOutline +} from "../component/text-area.outline"; +import {DefaultFieldLayoutDefinitionOutline} from "../component/default-layout.outline"; +import { + CheckboxInputFieldComponentDefinitionOutline, + CheckboxInputFieldModelDefinitionOutline, CheckboxInputFormComponentDefinitionOutline +} from "../component/checkbox-input.outline"; +import { + DropdownInputFieldComponentDefinitionOutline, + DropdownInputFieldModelDefinitionOutline, DropdownInputFormComponentDefinitionOutline +} from "../component/dropdown-input.outline"; +import { + RadioInputFieldComponentDefinitionOutline, + RadioInputFieldModelDefinitionOutline, RadioInputFormComponentDefinitionOutline +} from "../component/radio-input.outline"; +import { + DateInputFieldComponentDefinitionOutline, + DateInputFieldModelDefinitionOutline, DateInputFormComponentDefinitionOutline +} from "../component/date-input.outline"; + +export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisitor { + private original?: FormModel; + private result?: FormConfigOutline; + + private fieldComponentMap?: ComponentClassDefMapType; + private fieldModelMap?: ModelClassDefMapType; + private fieldLayoutMap?: LayoutClassDefMapType; + private formComponentMap?: FormComponentClassDefMapType; + + constructor(logger: ILogger) { + super(logger); + this.fieldComponentMap = FieldComponentDefinitionMap; + this.fieldModelMap = FieldModelDefinitionMap; + this.fieldLayoutMap = FieldLayoutDefinitionMap; + this.formComponentMap = FormComponentDefinitionMap; + } + + start(original: FormModel): FormConfigFrame { + this.original = _cloneDeep(original) ?? {}; + this.result = new FormConfig(); + this.resetCurrentPath(); + this.result.accept(this); + return this.result; + } + + /* Form Config */ + + visitFormConfig(item: FormConfigOutline): void { + const currentData = this.getDataPath(this.original, this.currentPath); + + // Set properties that are the same in v4 and v5. + this.sharedProps.setPropOverride('name', item, currentData); + this.sharedProps.setPropOverride('type', item, currentData); + this.sharedProps.setPropOverride('viewCssClasses', item, currentData); + this.sharedProps.setPropOverride('editCssClasses', item, currentData); + + // TODO: form.customAngularApp? + // TODO: form.fields? + // TODO: form.workflowStep? + // TODO: form.requiredFieldIndicator? + // TODO: form.messages? + // TODO: form.attachmentFields? + + // Convert properties from v4 to v5. + if (Object.hasOwn(currentData, 'skipValidationOnSave')) { + switch (currentData.skipValidationOnSave) { + case true: + item.enabledValidationGroups = ["none"]; + break; + default: + case false: + item.enabledValidationGroups = ["all"]; + break; + } + } + + // Convert components. + const fields: Record[] = currentData.fields ?? []; + fields.forEach((field, index) => { + const {class: componentClassString, message} = this.mapV4ClasstoV5Class(field); + const formComponentClass = this.formComponentMap?.get(componentClassString); + + + let formComponent; + if (componentClassString && formComponentClass) { + formComponent = new formComponentClass(); + } else { + formComponent = new ContentFormComponentDefinition(); + formComponent.component = new ContentFieldComponentDefinition(); + formComponent.component.config = new ContentFieldComponentConfig(); + + let msg = message; + if (componentClassString) { + msg += ` Could not find class for form component class name '${componentClassString}' at path '${this.currentPath}'.`; + } + formComponent.component.config.content = msg; + this.logger.warn(formComponent.component.config.content); + } + + // Store the instances on the item + item.componentDefinitions.push(formComponent); + + // Continue the construction + this.acceptCurrentPath(formComponent, [index.toString()]); + }); + } + + + /* SimpleInput */ + + visitSimpleInputFieldComponentDefinition(item: SimpleInputFieldComponentDefinitionOutline): void { + } + + visitSimpleInputFieldModelDefinition(item: SimpleInputFieldModelDefinitionOutline): void { + } + + visitSimpleInputFormComponentDefinition(item: SimpleInputFormComponentDefinitionOutline): void { + const currentData = this.getDataPath(this.original, this.currentPath); + + } + + + /* Content */ + + visitContentFieldComponentDefinition(item: ContentFieldComponentDefinitionOutline): void { + } + + visitContentFormComponentDefinition(item: ContentFormComponentDefinitionOutline): void { + const currentData = this.getDataPath(this.original, this.currentPath); + + } + + /* Repeatable */ + + visitRepeatableFieldComponentDefinition(item: RepeatableFieldComponentDefinitionOutline): void { + } + + visitRepeatableFieldModelDefinition(item: RepeatableFieldModelDefinitionOutline): void { + } + + visitRepeatableElementFieldLayoutDefinition(item: RepeatableElementFieldLayoutDefinitionOutline): void { + } + + visitRepeatableFormComponentDefinition(item: RepeatableFormComponentDefinitionOutline): void { + } + + /* Validation Summary */ + + visitValidationSummaryFieldComponentDefinition(item: ValidationSummaryFieldComponentDefinitionOutline): void { + } + + visitValidationSummaryFormComponentDefinition(item: ValidationSummaryFormComponentDefinitionOutline): void { + } + + /* Group */ + + visitGroupFieldComponentDefinition(item: GroupFieldComponentDefinitionOutline): void { + (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { + // Visit children + this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); + }); + } + + visitGroupFieldModelDefinition(item: GroupFieldModelDefinitionOutline): void { + } + + visitGroupFormComponentDefinition(item: GroupFormComponentDefinitionOutline): void { + } + + /* Tab */ + + visitTabFieldComponentDefinition(item: TabFieldComponentDefinitionOutline): void { + (item.config?.tabs ?? []).forEach((componentDefinition, index) => { + // Visit children + this.acceptCurrentPath(componentDefinition, ["config", "tabs", index.toString()]); + }); + } + + visitTabFieldLayoutDefinition(item: TabFieldLayoutDefinitionOutline): void { + } + + visitTabFormComponentDefinition(item: TabFormComponentDefinitionOutline): void { + } + + /* Tab Content */ + + visitTabContentFieldComponentDefinition(item: TabContentFieldComponentDefinitionOutline): void { + (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { + // Visit children + this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); + }); + } + + visitTabContentFieldLayoutDefinition(item: TabContentFieldLayoutDefinitionOutline): void { + } + + visitTabContentFormComponentDefinition(item: TabContentFormComponentDefinitionOutline): void { + } + + /* Save Button */ + + visitSaveButtonFieldComponentDefinition(item: SaveButtonFieldComponentDefinitionOutline): void { + } + + visitSaveButtonFormComponentDefinition(item: SaveButtonFormComponentDefinitionOutline): void { + } + + /* Text Area */ + + visitTextAreaFieldComponentDefinition(item: TextAreaFieldComponentDefinitionOutline): void { + } + + visitTextAreaFieldModelDefinition(item: TextAreaFieldModelDefinitionOutline): void { + } + + visitTextAreaFormComponentDefinition(item: TextAreaFormComponentDefinitionOutline): void { + } + + /* Default Layout */ + + visitDefaultFieldLayoutDefinition(item: DefaultFieldLayoutDefinitionOutline): void { + } + + /* Checkbox Input */ + + visitCheckboxInputFieldComponentDefinition(item: CheckboxInputFieldComponentDefinitionOutline): void { + } + + visitCheckboxInputFieldModelDefinition(item: CheckboxInputFieldModelDefinitionOutline): void { + } + + visitCheckboxInputFormComponentDefinition(item: CheckboxInputFormComponentDefinitionOutline): void { + } + + /* Dropdown Input */ + + visitDropdownInputFieldComponentDefinition(item: DropdownInputFieldComponentDefinitionOutline): void { + } + + visitDropdownInputFieldModelDefinition(item: DropdownInputFieldModelDefinitionOutline): void { + } + + visitDropdownInputFormComponentDefinition(item: DropdownInputFormComponentDefinitionOutline): void { + } + + /* Radio Input */ + + visitRadioInputFieldComponentDefinition(item: RadioInputFieldComponentDefinitionOutline): void { + } + + visitRadioInputFieldModelDefinition(item: RadioInputFieldModelDefinitionOutline): void { + } + + visitRadioInputFormComponentDefinition(item: RadioInputFormComponentDefinitionOutline): void { + } + + /* Date Input */ + + visitDateInputFieldComponentDefinition(item: DateInputFieldComponentDefinitionOutline): void { + } + + visitDateInputFieldModelDefinition(item: DateInputFieldModelDefinitionOutline): void { + } + + visitDateInputFormComponentDefinition(item: DateInputFormComponentDefinitionOutline): void { + } + + /* Shared */ + + /** + * Map from v4 class and compClass to v5 class. + */ + protected mapV4ClasstoV5Class(field: Record): { class: string, message: string } { + const v4ClassName = field.class?.toString() ?? ""; + const v4CompClassName = field.compClass?.toString() ?? ""; + const fieldDefinition = (field?.definition ?? {}) as Record; + const classMap: Record> = { + "Container": { + "TextBlockComponent": "ContentComponent", + "GenericGroupComponent": "GroupComponent", + "": "GroupComponent", + }, + "RepeatableContributor": { + "RepeatableContributorComponent": "RepeatableComponent", + }, + "SelectionField": {}, + "RepeatableContainer": {}, + "RepeatableVocab": { + "RepeatableVocabComponent": "RepeatableComponent", + }, + "NotInFormField": {}, + "WorkspaceSelectorField": {}, + "TextArea": { + "": "TextAreaComponent" + }, + "TabOrAccordionContainer": { + "TabOrAccordionContainerComponent": "TabContentComponent", + }, + "ButtonBarContainer": { + "ButtonBarContainerComponent": "GroupComponent", + }, + "TextField": { + "": "SimpleInputComponent", + }, + "DropdownFieldComponent": { + "": "DropdownInputComponent", + }, + "SelectionFieldComponent": { + "": "SelectionInputComponent", + } + }; + + const v4ClassOpts = classMap[v4ClassName] ?? {}; + let v5ClassName = v4ClassOpts[v4CompClassName] ?? ""; + + // Some components need special processing. + if (v5ClassName === "SelectionInputComponent" && fieldDefinition?.controlType === 'checkbox') { + v5ClassName = "CheckboxInputComponent"; + } + + // Provide a message for unparsable fields. + let message = ""; + if (!v5ClassName) { + const v4Name = fieldDefinition?.name ?? "(unknown)"; + message = `Unparsable: class '${v4ClassName}' compClass '${v4CompClassName}' name '${v4Name}'`; + } + return { + class: v5ClassName || "", + message: message, + } + } +} diff --git a/packages/sails-ng-common/test/unit/migrate-config-v4-v5.visitor.test.ts b/packages/sails-ng-common/test/unit/migrate-config-v4-v5.visitor.test.ts new file mode 100644 index 000000000..f5f749538 --- /dev/null +++ b/packages/sails-ng-common/test/unit/migrate-config-v4-v5.visitor.test.ts @@ -0,0 +1,35 @@ +import {logger} from "./helpers"; +import {MigrationV4ToV5FormConfigVisitor} from "../../src/config/visitor/migrate-config-v4-v5.visitor"; +import fs from "fs"; +import path from "path"; + +let expect: Chai.ExpectStatic; +import("chai").then(mod => expect = mod.expect); + +describe("Migrate v4 to v5 Visitor", async () => { + const relPath = path.relative(path.join(__dirname, 'packages/sails-ng-common'), __dirname); + const inputFiles = path.resolve(relPath, 'support/ng19-forms-migration/inputFiles'); + const outputFiles = path.resolve(relPath, 'support/ng19-forms-migration/outputFiles'); + it(`should migrate as expected`, async function () { + const v4InputFiles = fs.readdirSync(inputFiles).filter(file => file.endsWith('.js')); + for (const v4InputFile of v4InputFiles) { + const fullPath = path.join(inputFiles, v4InputFile); + const formConfig = require(fullPath); + if (!formConfig.name) { + formConfig.name = 'v4FormConfig'; + } + const v5InputFile = v4InputFile.replace('.js', '.ts'); + + const visitor = new MigrationV4ToV5FormConfigVisitor(logger); + const actual = visitor.start(formConfig); + + const tsContent = ` +import {FormConfigFrame} from "@researchdatabox/sails-ng-common"; +const formConfig: FormConfigFrame = ${JSON.stringify(actual, null, 2)}; +module.exports = formConfig; +`; + // fs.writeFileSync(`${outputFiles}/parsed-${v5InputFile}`, tsContent, "utf8"); + // expect(actual).to.not.be.empty; + } + }); +}); From 1ab2fa70bf9c8d3f23871060258bb4c8676ed6ac Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Mon, 17 Nov 2025 07:17:18 +0000 Subject: [PATCH 2/5] implemented populating component, model, layout blocks --- .../src/config/visitor/base.model.ts | 6 +- .../visitor/migrate-config-v4-v5.visitor.ts | 539 ++++++++++++++---- 2 files changed, 431 insertions(+), 114 deletions(-) diff --git a/packages/sails-ng-common/src/config/visitor/base.model.ts b/packages/sails-ng-common/src/config/visitor/base.model.ts index 017418a8a..7dbe04f03 100644 --- a/packages/sails-ng-common/src/config/visitor/base.model.ts +++ b/packages/sails-ng-common/src/config/visitor/base.model.ts @@ -1,5 +1,5 @@ import {get as _get} from "lodash"; -import {FormConfigFrame, FormConfigOutline} from "../form-config.outline"; +import {FormConfigOutline} from "../form-config.outline"; import {CanVisit, FormConfigVisitorOutline} from "./base.outline"; import { SimpleInputFieldComponentDefinitionOutline, @@ -277,7 +277,7 @@ export abstract class FormConfigVisitor implements FormConfigVisitorOutline { } // TODO: fix typing - protected getDataPath(data?: FormConfigFrame, path?: string[]) { + protected getDataPath(data?: any, path?: string[]) { const result = path && path.length > 0 ? _get(data, path.map((i: string) => i.toString())) : data; // for debugging: @@ -436,4 +436,4 @@ export class PopulateProperties { } -} \ No newline at end of file +} diff --git a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts index b336f7ffc..5b23d1ae3 100644 --- a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts +++ b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts @@ -1,79 +1,141 @@ -import {cloneDeep as _cloneDeep} from "lodash"; -import {FormConfigFrame, FormConfigOutline} from "../form-config.outline"; -import {FormModel, ILogger} from "@researchdatabox/redbox-core-types"; -import {CurrentPathFormConfigVisitor} from "./base.model"; +import {cloneDeep as _cloneDeep, mergeWith as _mergeWith} from "lodash"; import {FormConfig} from "../form-config.model"; import { ComponentClassDefMapType, FieldComponentDefinitionMap, FieldLayoutDefinitionMap, - FieldModelDefinitionMap, - FormComponentClassDefMapType, - FormComponentDefinitionMap, - LayoutClassDefMapType, - ModelClassDefMapType + FieldModelDefinitionMap, FormComponentClassDefMapType, + FormComponentDefinitionMap, LayoutClassDefMapType, ModelClassDefMapType, } from "../dictionary.model"; +import {CurrentPathFormConfigVisitor} from "./base.model"; +import {FormConfigFrame, FormConfigOutline} from "../form-config.outline"; import { - ContentFieldComponentConfig, - ContentFieldComponentDefinition, - ContentFormComponentDefinition -} from "../component/content.model"; + GroupFieldComponentDefinitionFrame, + GroupFieldComponentDefinitionOutline, GroupFieldComponentName, GroupFieldModelDefinitionFrame, + GroupFieldModelDefinitionOutline, GroupFieldModelName, + GroupFormComponentDefinitionOutline +} from "../component/group.outline"; +import { + RepeatableComponentName, RepeatableElementFieldLayoutDefinitionFrame, + RepeatableElementFieldLayoutDefinitionOutline, RepeatableElementLayoutName, RepeatableFieldComponentDefinitionFrame, + RepeatableFieldComponentDefinitionOutline, + RepeatableFieldModelDefinitionFrame, RepeatableFieldModelDefinitionOutline, + RepeatableFormComponentDefinitionOutline, RepeatableModelName +} from "../component/repeatable.outline"; +import { + RepeatableElementFieldLayoutConfig, + RepeatableFieldComponentConfig, + RepeatableFieldModelConfig +} from "../component/repeatable.model"; import { - SimpleInputFieldComponentDefinitionOutline, - SimpleInputFieldModelDefinitionOutline, SimpleInputFormComponentDefinitionOutline + GroupFieldComponentConfig, + GroupFieldModelConfig +} from "../component/group.model"; +import { + SimpleInputComponentName, + SimpleInputFieldComponentDefinitionFrame, + SimpleInputFieldComponentDefinitionOutline, SimpleInputFieldModelDefinitionFrame, + SimpleInputFieldModelDefinitionOutline, SimpleInputFormComponentDefinitionOutline, SimpleInputModelName } from "../component/simple-input.outline"; +import {SimpleInputFieldComponentConfig, SimpleInputFieldModelConfig} from "../component/simple-input.model"; +import { + DefaultFieldLayoutDefinitionFrame, + DefaultFieldLayoutDefinitionOutline, DefaultLayoutName +} from "../component/default-layout.outline"; +import {DefaultFieldLayoutConfig, DefaultFieldLayoutDefinition} from "../component/default-layout.model"; +import {FormConstraintAuthorizationConfig, FormConstraintConfig, FormExpressionsConfig} from "../form-component.model"; +import {FormComponentDefinitionFrame, FormComponentDefinitionOutline} from "../form-component.outline"; import { + ContentComponentName, ContentFieldComponentDefinitionFrame, ContentFieldComponentDefinitionOutline, ContentFormComponentDefinitionOutline } from "../component/content.outline"; import { - RepeatableElementFieldLayoutDefinitionOutline, - RepeatableFieldComponentDefinitionOutline, - RepeatableFieldModelDefinitionOutline, RepeatableFormComponentDefinitionOutline -} from "../component/repeatable.outline"; -import { - ValidationSummaryFieldComponentDefinitionOutline, - ValidationSummaryFormComponentDefinitionOutline -} from "../component/validation-summary.outline"; -import { - GroupFieldComponentDefinitionOutline, - GroupFieldModelDefinitionOutline, - GroupFormComponentDefinitionOutline -} from "../component/group.outline"; -import { - TabFieldComponentDefinitionOutline, + TabComponentName, TabFieldComponentDefinitionFrame, + TabFieldComponentDefinitionOutline, TabFieldLayoutDefinitionFrame, TabFieldLayoutDefinitionOutline, - TabFormComponentDefinitionOutline + TabFormComponentDefinitionOutline, TabLayoutName } from "../component/tab.outline"; +import {TabFieldComponentConfig, TabFieldLayoutConfig} from "../component/tab.model"; import { - TabContentFieldComponentDefinitionOutline, - TabContentFieldLayoutDefinitionOutline, TabContentFormComponentDefinitionOutline + TabContentComponentName, + TabContentFieldComponentDefinitionFrame, + TabContentFieldComponentDefinitionOutline, TabContentFieldLayoutDefinitionFrame, + TabContentFieldLayoutDefinitionOutline, + TabContentFormComponentDefinitionFrame, TabContentFormComponentDefinitionOutline, TabContentLayoutName } from "../component/tab-content.outline"; import { - SaveButtonFieldComponentDefinitionOutline, - SaveButtonFormComponentDefinitionOutline -} from "../component/save-button.outline"; + TabContentFieldComponentConfig, + TabContentFieldLayoutConfig, + TabContentFormComponentDefinition +} from "../component/tab-content.model"; import { - TextAreaFieldComponentDefinitionOutline, - TextAreaFieldModelDefinitionOutline, TextAreaFormComponentDefinitionOutline + TextAreaComponentName, + TextAreaFieldComponentDefinitionFrame, + TextAreaFieldComponentDefinitionOutline, TextAreaFieldModelDefinitionFrame, + TextAreaFieldModelDefinitionOutline, TextAreaFormComponentDefinitionOutline, TextAreaModelName } from "../component/text-area.outline"; -import {DefaultFieldLayoutDefinitionOutline} from "../component/default-layout.outline"; +import {TextAreaFieldComponentConfig, TextAreaFieldModelConfig} from "../component/text-area.model"; import { - CheckboxInputFieldComponentDefinitionOutline, - CheckboxInputFieldModelDefinitionOutline, CheckboxInputFormComponentDefinitionOutline -} from "../component/checkbox-input.outline"; + ContentFieldComponentConfig, + ContentFieldComponentDefinition, + ContentFormComponentDefinition +} from "../component/content.model"; import { - DropdownInputFieldComponentDefinitionOutline, - DropdownInputFieldModelDefinitionOutline, DropdownInputFormComponentDefinitionOutline + DropdownInputComponentName, + DropdownInputFieldComponentDefinitionFrame, + DropdownInputFieldComponentDefinitionOutline, DropdownInputFieldModelDefinitionFrame, + DropdownInputFieldModelDefinitionOutline, DropdownInputFormComponentDefinitionOutline, DropdownInputModelName } from "../component/dropdown-input.outline"; +import {DropdownInputFieldComponentConfig, DropdownInputFieldModelConfig} from "../component/dropdown-input.model"; import { - RadioInputFieldComponentDefinitionOutline, - RadioInputFieldModelDefinitionOutline, RadioInputFormComponentDefinitionOutline + CheckboxInputComponentName, + CheckboxInputFieldComponentDefinitionFrame, + CheckboxInputFieldComponentDefinitionOutline, CheckboxInputFieldModelDefinitionFrame, + CheckboxInputFieldModelDefinitionOutline, CheckboxInputFormComponentDefinitionOutline, CheckboxInputModelName +} from "../component/checkbox-input.outline"; +import {CheckboxInputFieldComponentConfig, CheckboxInputFieldModelConfig} from "../component/checkbox-input.model"; +import { + RadioInputComponentName, RadioInputFieldComponentDefinitionFrame, + RadioInputFieldComponentDefinitionOutline, RadioInputFieldModelDefinitionFrame, + RadioInputFieldModelDefinitionOutline, RadioInputFormComponentDefinitionOutline, RadioInputModelName } from "../component/radio-input.outline"; +import {RadioInputFieldComponentConfig, RadioInputFieldModelConfig} from "../component/radio-input.model"; import { - DateInputFieldComponentDefinitionOutline, - DateInputFieldModelDefinitionOutline, DateInputFormComponentDefinitionOutline + DateInputComponentName, + DateInputFieldComponentDefinitionFrame, + DateInputFieldComponentDefinitionOutline, DateInputFieldModelDefinitionFrame, + DateInputFieldModelDefinitionOutline, DateInputFormComponentDefinitionOutline, DateInputModelName } from "../component/date-input.outline"; +import {DateInputFieldComponentConfig, DateInputFieldModelConfig} from "../component/date-input.model"; +import { + SaveButtonComponentName, + SaveButtonFieldComponentDefinitionFrame, + SaveButtonFieldComponentDefinitionOutline, + SaveButtonFormComponentDefinitionOutline +} from "../component/save-button.outline"; +import {SaveButtonFieldComponentConfig} from "../component/save-button.model"; +import { + ValidationSummaryComponentName, + ValidationSummaryFieldComponentDefinitionFrame, + ValidationSummaryFieldComponentDefinitionOutline, + ValidationSummaryFormComponentDefinitionOutline +} from "../component/validation-summary.outline"; +import {ValidationSummaryFieldComponentConfig} from "../component/validation-summary.model"; +import { + isTypeFieldDefinitionName, + isTypeFormComponentDefinition, isTypeFormComponentDefinitionName, + isTypeFormConfig, +} from "../helpers"; +import {AvailableFormComponentDefinitionFrames, ReusableFormDefinitions} from "../dictionary.outline"; +import {FormModesConfig} from "../shared.outline"; +import {ReusableComponentName, ReusableFormComponentDefinitionFrame} from "../component/reusable.outline"; +import {FormModel, ILogger} from "@researchdatabox/redbox-core-types"; +import {ConstructOverrides} from "./construct.overrides"; +import { FieldModelConfigFrame } from "../field-model.outline"; +import {FieldComponentConfigFrame} from "../field-component.outline"; +import {FieldLayoutConfigFrame} from "../field-layout.outline"; + export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisitor { private original?: FormModel; @@ -118,47 +180,30 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // TODO: form.messages? // TODO: form.attachmentFields? - // Convert properties from v4 to v5. - if (Object.hasOwn(currentData, 'skipValidationOnSave')) { - switch (currentData.skipValidationOnSave) { - case true: - item.enabledValidationGroups = ["none"]; - break; - default: - case false: - item.enabledValidationGroups = ["all"]; - break; - } - } + // TODO: enabledValidationGroups + // // Convert properties from v4 to v5. + // if (Object.hasOwn(currentData, 'skipValidationOnSave')) { + // switch (currentData.skipValidationOnSave) { + // case true: + // item.enabledValidationGroups = ["none"]; + // break; + // default: + // case false: + // item.enabledValidationGroups = ["all"]; + // break; + // } + // } // Convert components. const fields: Record[] = currentData.fields ?? []; fields.forEach((field, index) => { - const {class: componentClassString, message} = this.mapV4ClasstoV5Class(field); - const formComponentClass = this.formComponentMap?.get(componentClassString); - - - let formComponent; - if (componentClassString && formComponentClass) { - formComponent = new formComponentClass(); - } else { - formComponent = new ContentFormComponentDefinition(); - formComponent.component = new ContentFieldComponentDefinition(); - formComponent.component.config = new ContentFieldComponentConfig(); - - let msg = message; - if (componentClassString) { - msg += ` Could not find class for form component class name '${componentClassString}' at path '${this.currentPath}'.`; - } - formComponent.component.config.content = msg; - this.logger.warn(formComponent.component.config.content); - } + const formComponent = this.sharedConstructFormComponentFromField(field); // Store the instances on the item item.componentDefinitions.push(formComponent); // Continue the construction - this.acceptCurrentPath(formComponent, [index.toString()]); + this.acceptCurrentPath(formComponent, ["fields", index.toString()]); }); } @@ -166,52 +211,80 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit /* SimpleInput */ visitSimpleInputFieldComponentDefinition(item: SimpleInputFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new SimpleInputFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); } visitSimpleInputFieldModelDefinition(item: SimpleInputFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new SimpleInputFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); + // maxLength: field?.definition?.maxLength ?? 0, + // 'model.config.validators', [ {name: 'maxLength', message: '', config: {maxLength: fieldConfig.maxLength}} ]); } visitSimpleInputFormComponentDefinition(item: SimpleInputFormComponentDefinitionOutline): void { - const currentData = this.getDataPath(this.original, this.currentPath); - + this.sharedPopulateFormComponent(item); } /* Content */ visitContentFieldComponentDefinition(item: ContentFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + if (!item.config) { + item.config = new ContentFieldComponentConfig(); + } + this.sharedPopulateFieldComponentConfig(item.config, field); } visitContentFormComponentDefinition(item: ContentFormComponentDefinitionOutline): void { - const currentData = this.getDataPath(this.original, this.currentPath); - + this.sharedPopulateFormComponent(item); } /* Repeatable */ visitRepeatableFieldComponentDefinition(item: RepeatableFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new RepeatableFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); } visitRepeatableFieldModelDefinition(item: RepeatableFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new RepeatableFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitRepeatableElementFieldLayoutDefinition(item: RepeatableElementFieldLayoutDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new RepeatableElementFieldLayoutConfig(); + this.sharedPopulateFieldLayoutConfig(item.config, field); } visitRepeatableFormComponentDefinition(item: RepeatableFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Validation Summary */ visitValidationSummaryFieldComponentDefinition(item: ValidationSummaryFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new ValidationSummaryFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); } visitValidationSummaryFormComponentDefinition(item: ValidationSummaryFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Group */ visitGroupFieldComponentDefinition(item: GroupFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new GroupFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { // Visit children this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); @@ -219,14 +292,21 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit } visitGroupFieldModelDefinition(item: GroupFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new GroupFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitGroupFormComponentDefinition(item: GroupFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Tab */ visitTabFieldComponentDefinition(item: TabFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TabFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); (item.config?.tabs ?? []).forEach((componentDefinition, index) => { // Visit children this.acceptCurrentPath(componentDefinition, ["config", "tabs", index.toString()]); @@ -234,14 +314,21 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit } visitTabFieldLayoutDefinition(item: TabFieldLayoutDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TabFieldLayoutConfig(); + this.sharedPopulateFieldLayoutConfig(item.config, field); } visitTabFormComponentDefinition(item: TabFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Tab Content */ visitTabContentFieldComponentDefinition(item: TabContentFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TabContentFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { // Visit children this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); @@ -249,141 +336,371 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit } visitTabContentFieldLayoutDefinition(item: TabContentFieldLayoutDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TabContentFieldLayoutConfig(); + this.sharedPopulateFieldLayoutConfig(item.config, field); } visitTabContentFormComponentDefinition(item: TabContentFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Save Button */ visitSaveButtonFieldComponentDefinition(item: SaveButtonFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new SaveButtonFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); } visitSaveButtonFormComponentDefinition(item: SaveButtonFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Text Area */ visitTextAreaFieldComponentDefinition(item: TextAreaFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TextAreaFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); + // cols: field?.definition?.cols ?? field?.definition?.columns ?? 0, + // rows: field?.definition?.rows ?? 0 } visitTextAreaFieldModelDefinition(item: TextAreaFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new TextAreaFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitTextAreaFormComponentDefinition(item: TextAreaFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Default Layout */ visitDefaultFieldLayoutDefinition(item: DefaultFieldLayoutDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new DefaultFieldLayoutConfig(); + this.sharedPopulateFieldLayoutConfig(item.config, field); + // config: { + // label: common.label, + // helpText: common.help, + // } } /* Checkbox Input */ visitCheckboxInputFieldComponentDefinition(item: CheckboxInputFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new CheckboxInputFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); + // component.config.options = field?.definition?.options ?? {} } visitCheckboxInputFieldModelDefinition(item: CheckboxInputFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new CheckboxInputFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitCheckboxInputFormComponentDefinition(item: CheckboxInputFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Dropdown Input */ visitDropdownInputFieldComponentDefinition(item: DropdownInputFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new DropdownInputFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); + // component.config.options = field?.definition?.options ?? {} } visitDropdownInputFieldModelDefinition(item: DropdownInputFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new DropdownInputFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitDropdownInputFormComponentDefinition(item: DropdownInputFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Radio Input */ visitRadioInputFieldComponentDefinition(item: RadioInputFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new RadioInputFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); + // component.config.options = field?.definition?.options ?? {} } visitRadioInputFieldModelDefinition(item: RadioInputFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new RadioInputFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitRadioInputFormComponentDefinition(item: RadioInputFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Date Input */ visitDateInputFieldComponentDefinition(item: DateInputFieldComponentDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new DateInputFieldComponentConfig(); + this.sharedPopulateFieldComponentConfig(item.config, field); } visitDateInputFieldModelDefinition(item: DateInputFieldModelDefinitionOutline): void { + const field = this.getDataPath(this.original, this.currentPath); + item.config = new DateInputFieldModelConfig(); + this.sharedPopulateFieldModelConfig(item.config, field); } visitDateInputFormComponentDefinition(item: DateInputFormComponentDefinitionOutline): void { + this.sharedPopulateFormComponent(item); } /* Shared */ /** * Map from v4 class and compClass to v5 class. + * Return an instance of the form component without properties set. */ - protected mapV4ClasstoV5Class(field: Record): { class: string, message: string } { - const v4ClassName = field.class?.toString() ?? ""; - const v4CompClassName = field.compClass?.toString() ?? ""; + protected sharedConstructFormComponentFromField(field: Record) { + const {componentClassName: componentClassString, message} = this.mapV4ToV5(field); + + const formComponentClass = this.formComponentMap?.get(componentClassString); + + let formComponent; + if (componentClassString && formComponentClass) { + formComponent = new formComponentClass(); + } else { + formComponent = new ContentFormComponentDefinition(); + formComponent.component = new ContentFieldComponentDefinition(); + formComponent.component.config = new ContentFieldComponentConfig(); + + let msg = message; + if (componentClassString) { + msg += ` Could not find class for form component class name '${componentClassString}' at path '${this.currentPath}'.`; + } + formComponent.component.config.content = msg; + this.logger.warn(msg); + } + + return formComponent; + } + + protected mapV4ToV5(field: Record): MappedClasses & { message: string } { + const v4ClassName = field?.class?.toString() ?? ""; + const v4CompClassName = field?.compClass?.toString() ?? ""; const fieldDefinition = (field?.definition ?? {}) as Record; - const classMap: Record> = { + + // Overall mapping from v4 class, v4 compClass to v5 class. + const classMap: Record> = { "Container": { - "TextBlockComponent": "ContentComponent", - "GenericGroupComponent": "GroupComponent", - "": "GroupComponent", + "TextBlockComponent": { + componentClassName: "ContentComponent", + }, + "GenericGroupComponent": { + componentClassName: "GroupComponent", + modelClassName: "GroupModel", + }, + "": { + componentClassName: "GroupComponent", + modelClassName: "GroupModel", + }, }, "RepeatableContributor": { - "RepeatableContributorComponent": "RepeatableComponent", + "RepeatableContributorComponent": { + componentClassName: "RepeatableComponent", + modelClassName: "RepeatableModel", + }, }, - "SelectionField": {}, - "RepeatableContainer": {}, "RepeatableVocab": { - "RepeatableVocabComponent": "RepeatableComponent", + "RepeatableVocabComponent": { + componentClassName: "RepeatableComponent", + modelClassName: "RepeatableModel", + }, }, - "NotInFormField": {}, - "WorkspaceSelectorField": {}, "TextArea": { - "": "TextAreaComponent" + "": { + componentClassName: "TextAreaComponent", + }, }, "TabOrAccordionContainer": { - "TabOrAccordionContainerComponent": "TabContentComponent", + "TabOrAccordionContainerComponent": { + componentClassName: "TabContentComponent", + }, }, "ButtonBarContainer": { - "ButtonBarContainerComponent": "GroupComponent", + "ButtonBarContainerComponent": { + componentClassName: "GroupComponent", + modelClassName: "GroupModel", + }, }, "TextField": { - "": "SimpleInputComponent", + "": { + componentClassName: "SimpleInputComponent", + modelClassName: "SimpleInputModel", + }, }, "DropdownFieldComponent": { - "": "DropdownInputComponent", + "": { + componentClassName: "DropdownInputComponent", + modelClassName: "DropdownInputModel", + }, }, "SelectionFieldComponent": { - "": "SelectionInputComponent", + "": { + componentClassName: "SelectionInputComponent", + modelClassName: "SelectionInputModel", + }, + }, + "SaveButtonComponent": { + "": { + componentClassName: "SaveButtonComponent", + }, } }; - const v4ClassOpts = classMap[v4ClassName] ?? {}; - let v5ClassName = v4ClassOpts[v4CompClassName] ?? ""; + const v5ClassNames = v4ClassOpts[v4CompClassName] ?? {}; + let v5ComponentClassName = v5ClassNames.componentClassName || ""; + let v5ModelClassName = v5ClassNames.modelClassName || ""; + let v5LayoutClassName = v5ClassNames.layoutClassName || ""; + // Some components need special processing. - if (v5ClassName === "SelectionInputComponent" && fieldDefinition?.controlType === 'checkbox') { - v5ClassName = "CheckboxInputComponent"; + if (v5ComponentClassName === "SelectionInputComponent" && fieldDefinition?.controlType === 'checkbox') { + v5ComponentClassName = "CheckboxInputComponent"; + v5ModelClassName = "CheckboxInputModel"; } - // Provide a message for unparsable fields. + // Provide a message for not yet implemented fields. let message = ""; - if (!v5ClassName) { + if (!v5ComponentClassName) { const v4Name = fieldDefinition?.name ?? "(unknown)"; - message = `Unparsable: class '${v4ClassName}' compClass '${v4CompClassName}' name '${v4Name}'`; + message = `Not yet implemented v4 class '${v4ClassName}' v4 compClass '${v4CompClassName}' name '${v4Name}'`; } return { - class: v5ClassName || "", + componentClassName: v5ComponentClassName || "", + modelClassName: v5ModelClassName || "", + layoutClassName: v5LayoutClassName || "", message: message, } } + + protected sharedPopulateFormComponent(item: FormComponentDefinitionOutline): void { + // Get the current raw data for constructing the class instance. + const currentData = this.getDataPath(this.original, this.currentPath); + let {componentClassName, modelClassName, layoutClassName, message} = this.mapV4ToV5(currentData); + + // Set the simple properties + item.name = currentData?.definition?.name; + item.module = undefined; + + // Set the constraints + item.constraints = new FormConstraintConfig(); + item.constraints.allowModes = []; + if (currentData?.editOnly === true) { + item.constraints.allowModes.push("edit"); + } + if (currentData?.viewOnly === true) { + item.constraints.allowModes.push("view"); + } + + item.constraints.authorization = new FormConstraintAuthorizationConfig(); + item.constraints.authorization.allowRoles = []; + if (currentData?.roles?.length > 0) { + item.constraints.authorization.allowRoles.push(...currentData?.roles); + } + + // Set the expressions + item.expressions = new FormExpressionsConfig(); + // TODO: expressions + + // Get the classes + let componentClass = this.fieldComponentMap?.get(componentClassName); + const modelClass = modelClassName ? this.fieldModelMap?.get(modelClassName) : null; + let layoutClass = layoutClassName ? this.fieldLayoutMap?.get(layoutClassName) : null; + + // Use content component if the component is not yet implemented. + if (!componentClass) { + componentClass = ContentFieldComponentDefinition; + layoutClass = DefaultFieldLayoutDefinition; + message += ` Could not find class for form component class name '${componentClassName}' at path '${this.currentPath}' with currentData ${JSON.stringify(currentData)}.`; + this.logger.warn(message); + } + + // Create new instances + const component = new componentClass(); + const model = modelClass ? new modelClass() : null; + const layout = layoutClass ? new layoutClass() : null; + + // Add message to content component if it is not yet implemented. + if (message) { + const contentConfig = new ContentFieldComponentConfig(); + contentConfig.content = message; + component.config = contentConfig + } + + // Set the instances + item.component = component; + item.model = model || undefined; + item.layout = layout || undefined; + + // Continue visiting + this.acceptCurrentPath(item.component, []); + if (item.model) { + this.acceptCurrentPath(item.model, []); + } + if (item.layout) { + this.acceptCurrentPath(item.layout, []); + } + } + + protected sharedPopulateFieldComponentConfig(item: FieldComponentConfigFrame, field?: any) { + // Set the common field component config properties + // this.sharedProps.setPropOverride('readonly', item, config); + // this.sharedProps.setPropOverride('visible', item, config); + // this.sharedProps.setPropOverride('editMode', item, config); + this.sharedProps.setPropOverride('label', item, field?.definition?.label); + // this.sharedProps.setPropOverride('defaultComponentCssClasses', item, config); + // this.sharedProps.setPropOverride('hostCssClasses', item, config); + // this.sharedProps.setPropOverride('wrapperCssClasses', item, config); + // this.sharedProps.setPropOverride('disabled', item, config); + // this.sharedProps.setPropOverride('autofocus', item, config); + // this.sharedProps.setPropOverride('tooltip', item, config); + } + + protected sharedPopulateFieldModelConfig(item: FieldModelConfigFrame, field?: any) { + // Set the common field model config properties + // this.sharedProps.setPropOverride('disableFormBinding', item, config); + // this.sharedProps.setPropOverride('value', item, config); + this.sharedProps.setPropOverride('defaultValue', item, field?.definition?.defaultValue); + // this.sharedProps.setPropOverride('validators', item, config); + // this.sharedProps.setPropOverride('wrapperCssClasses', item, config); + // this.sharedProps.setPropOverride('editCssClasses', item, config); + } + + protected sharedPopulateFieldLayoutConfig(item: FieldLayoutConfigFrame, field?: any) { + // Set the common field model config properties + this.sharedPopulateFieldComponentConfig(item, field); + // this.sharedProps.setPropOverride('labelRequiredStr', item, config); + this.sharedProps.setPropOverride('helpText', item, field?.definition?.help); + // this.sharedProps.setPropOverride('cssClassesMap', item, config); + // this.sharedProps.setPropOverride('helpTextVisibleOnInit', item, config); + // this.sharedProps.setPropOverride('helpTextVisible', item, config); + } + + +} + +interface MappedClasses { + componentClassName: string; + modelClassName?: string; + layoutClassName?: string; } From 7bb418b4742df46e967ed61202b7f2e78ed09b36 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Tue, 18 Nov 2025 01:18:30 +0000 Subject: [PATCH 3/5] follow nested v4 field definition fields --- .../visitor/migrate-config-v4-v5.visitor.ts | 159 ++++++++---------- 1 file changed, 69 insertions(+), 90 deletions(-) diff --git a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts index 5b23d1ae3..a8eb22887 100644 --- a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts +++ b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts @@ -132,7 +132,7 @@ import {FormModesConfig} from "../shared.outline"; import {ReusableComponentName, ReusableFormComponentDefinitionFrame} from "../component/reusable.outline"; import {FormModel, ILogger} from "@researchdatabox/redbox-core-types"; import {ConstructOverrides} from "./construct.overrides"; -import { FieldModelConfigFrame } from "../field-model.outline"; +import {FieldModelConfigFrame} from "../field-model.outline"; import {FieldComponentConfigFrame} from "../field-component.outline"; import {FieldLayoutConfigFrame} from "../field-layout.outline"; @@ -198,11 +198,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const fields: Record[] = currentData.fields ?? []; fields.forEach((field, index) => { const formComponent = this.sharedConstructFormComponentFromField(field); - - // Store the instances on the item item.componentDefinitions.push(formComponent); - - // Continue the construction this.acceptCurrentPath(formComponent, ["fields", index.toString()]); }); } @@ -249,6 +245,12 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new RepeatableFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); + + if (field?.definition?.fields?.length === 1) { + // populate elementTemplate + item.config.elementTemplate = this.sharedConstructFormComponentFromField(field); + this.acceptCurrentPath(item.config.elementTemplate, ["definition", "fields", "0"]); + } } visitRepeatableFieldModelDefinition(item: RepeatableFieldModelDefinitionOutline): void { @@ -283,11 +285,15 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit visitGroupFieldComponentDefinition(item: GroupFieldComponentDefinitionOutline): void { const field = this.getDataPath(this.original, this.currentPath); - item.config = new GroupFieldComponentConfig(); + const config = new GroupFieldComponentConfig(); + item.config = config; this.sharedPopulateFieldComponentConfig(item.config, field); - (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { - // Visit children - this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); + + const fields: Record[] = field?.definition?.fields ?? []; + fields.forEach((field, index) => { + const formComponent = this.sharedConstructFormComponentFromField(field); + config.componentDefinitions.push(formComponent); + this.acceptCurrentPath(formComponent, ["definition", "fields", index.toString()]); }); } @@ -305,11 +311,17 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit visitTabFieldComponentDefinition(item: TabFieldComponentDefinitionOutline): void { const field = this.getDataPath(this.original, this.currentPath); - item.config = new TabFieldComponentConfig(); + const config = new TabFieldComponentConfig(); + item.config = config; this.sharedPopulateFieldComponentConfig(item.config, field); - (item.config?.tabs ?? []).forEach((componentDefinition, index) => { - // Visit children - this.acceptCurrentPath(componentDefinition, ["config", "tabs", index.toString()]); + + const fields: Record[] = field?.definition?.fields ?? []; + fields.forEach((field, index) => { + // TODO: build tab component from field + const tab = new TabContentFormComponentDefinition(); + config.tabs.push(tab); + field.class = "TabContentContainer"; + this.acceptCurrentPath(tab, ["definition", "fields", index.toString()]); }); } @@ -327,11 +339,15 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit visitTabContentFieldComponentDefinition(item: TabContentFieldComponentDefinitionOutline): void { const field = this.getDataPath(this.original, this.currentPath); - item.config = new TabContentFieldComponentConfig(); + const config = new TabContentFieldComponentConfig(); + item.config = config; this.sharedPopulateFieldComponentConfig(item.config, field); - (item.config?.componentDefinitions ?? []).forEach((componentDefinition, index) => { - // Visit children - this.acceptCurrentPath(componentDefinition, ["config", "componentDefinitions", index.toString()]); + + const fields: Record[] = field?.definition?.fields ?? []; + fields.forEach((field, index) => { + const formComponent = this.sharedConstructFormComponentFromField(field); + config.componentDefinitions.push(formComponent); + this.acceptCurrentPath(formComponent, ["definition", "fields", index.toString()]); }); } @@ -500,74 +516,37 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const fieldDefinition = (field?.definition ?? {}) as Record; // Overall mapping from v4 class, v4 compClass to v5 class. - const classMap: Record> = { - "Container": { - "TextBlockComponent": { - componentClassName: "ContentComponent", - }, - "GenericGroupComponent": { - componentClassName: "GroupComponent", - modelClassName: "GroupModel", - }, - "": { - componentClassName: "GroupComponent", - modelClassName: "GroupModel", - }, - }, - "RepeatableContributor": { - "RepeatableContributorComponent": { - componentClassName: "RepeatableComponent", - modelClassName: "RepeatableModel", - }, - }, - "RepeatableVocab": { - "RepeatableVocabComponent": { - componentClassName: "RepeatableComponent", - modelClassName: "RepeatableModel", - }, - }, - "TextArea": { - "": { - componentClassName: "TextAreaComponent", - }, - }, - "TabOrAccordionContainer": { - "TabOrAccordionContainerComponent": { - componentClassName: "TabContentComponent", - }, - }, - "ButtonBarContainer": { - "ButtonBarContainerComponent": { - componentClassName: "GroupComponent", - modelClassName: "GroupModel", - }, - }, - "TextField": { - "": { - componentClassName: "SimpleInputComponent", - modelClassName: "SimpleInputModel", - }, - }, - "DropdownFieldComponent": { - "": { - componentClassName: "DropdownInputComponent", - modelClassName: "DropdownInputModel", - }, - }, - "SelectionFieldComponent": { - "": { - componentClassName: "SelectionInputComponent", - modelClassName: "SelectionInputModel", - }, - }, - "SaveButtonComponent": { - "": { - componentClassName: "SaveButtonComponent", - }, - } + const contentComponent = {componentClassName: "ContentComponent"}; + const groupComponent = {componentClassName: "GroupComponent", modelClassName: "GroupModel"}; + const repeatableComponent = {componentClassName: "RepeatableComponent", modelClassName: "RepeatableModel"}; + const tabComponent = {componentClassName: "TabComponent"}; + const tabContentComponent = {componentClassName: "TabContentComponent"}; + const textAreaComponent = {componentClassName: "TextAreaComponent", modelClassName: "TextAreaModel"}; + const simpleInputComponent = {componentClassName: "SimpleInputComponent", modelClassName: "SimpleInputModel"}; + const dropDownComponent = {componentClassName: "DropdownInputComponent", modelClassName: "DropdownInputModel"}; + const dateInputComponent = {componentClassName: "DateInputComponent", modelClassName: "DateInputModel"}; + const saveButtonComponent = {componentClassName: "SaveButtonComponent"}; + const classMap: Record = { + "Container__TextBlockComponent": contentComponent, + "Container__GenericGroupComponent": groupComponent, + "TextArea__": textAreaComponent, + "TextArea__TextAreaComponent": textAreaComponent, + "TabOrAccordionContainer__TabOrAccordionContainerComponent": tabComponent, + "ButtonBarContainer__ButtonBarContainerComponent": groupComponent, + "Container__": groupComponent, + "TextField__": simpleInputComponent, + "RepeatableContainer__RepeatableTextfieldComponent": repeatableComponent, + "RepeatableContainer__RepeatableGroupComponent": repeatableComponent, + "RepeatableContributor__RepeatableContributorComponent": repeatableComponent, + "RepeatableVocab__RepeatableVocabComponent": repeatableComponent, + "SelectionField__DropdownFieldComponent": dropDownComponent, + "DateTime__": dateInputComponent, + "SaveButton__": saveButtonComponent, + "TabContentContainer__": tabContentComponent, // TabContentContainer is a made-up v4 class for mapping to tab content component + // TODO: generic components that likely need to be more specific + "ContributorField__": groupComponent, }; - const v4ClassOpts = classMap[v4ClassName] ?? {}; - const v5ClassNames = v4ClassOpts[v4CompClassName] ?? {}; + const v5ClassNames = classMap[`${v4ClassName}__${v4CompClassName}`] ?? {}; let v5ComponentClassName = v5ClassNames.componentClassName || ""; let v5ModelClassName = v5ClassNames.modelClassName || ""; let v5LayoutClassName = v5ClassNames.layoutClassName || ""; @@ -582,8 +561,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // Provide a message for not yet implemented fields. let message = ""; if (!v5ComponentClassName) { - const v4Name = fieldDefinition?.name ?? "(unknown)"; - message = `Not yet implemented v4 class '${v4ClassName}' v4 compClass '${v4CompClassName}' name '${v4Name}'`; + const v4Name = fieldDefinition?.name; + message = `Not yet implemented v4: class ${JSON.stringify(v4ClassName)} compClass ${JSON.stringify(v4CompClassName)} name ${JSON.stringify(v4Name)}.`; } return { componentClassName: v5ComponentClassName || "", @@ -599,7 +578,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit let {componentClassName, modelClassName, layoutClassName, message} = this.mapV4ToV5(currentData); // Set the simple properties - item.name = currentData?.definition?.name; + item.name = currentData?.definition?.name || `${componentClassName}-${this.currentPath}`; item.module = undefined; // Set the constraints @@ -624,15 +603,15 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // Get the classes let componentClass = this.fieldComponentMap?.get(componentClassName); - const modelClass = modelClassName ? this.fieldModelMap?.get(modelClassName) : null; + let modelClass = modelClassName ? this.fieldModelMap?.get(modelClassName) : null; let layoutClass = layoutClassName ? this.fieldLayoutMap?.get(layoutClassName) : null; // Use content component if the component is not yet implemented. if (!componentClass) { componentClass = ContentFieldComponentDefinition; + modelClass = null layoutClass = DefaultFieldLayoutDefinition; - message += ` Could not find class for form component class name '${componentClassName}' at path '${this.currentPath}' with currentData ${JSON.stringify(currentData)}.`; - this.logger.warn(message); + message += ` Could not find class for form component class name ${JSON.stringify(componentClassName)} at path ${JSON.stringify(this.currentPath)} with currentData ${JSON.stringify(currentData)}.`; } // Create new instances From e92c766b9b7c7021b4593f1b125649513870dc04 Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Tue, 18 Nov 2025 02:12:28 +0000 Subject: [PATCH 4/5] migrate some component-specific properties --- .../src/config/component/tab.model.ts | 9 +-- .../src/config/visitor/base.model.ts | 3 + .../visitor/migrate-config-v4-v5.visitor.ts | 68 +++++++++++-------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/sails-ng-common/src/config/component/tab.model.ts b/packages/sails-ng-common/src/config/component/tab.model.ts index abddcfd33..99762e8f7 100644 --- a/packages/sails-ng-common/src/config/component/tab.model.ts +++ b/packages/sails-ng-common/src/config/component/tab.model.ts @@ -1,6 +1,6 @@ import { FieldComponentConfigKind, FieldComponentDefinitionKind, FieldLayoutConfigKind, FieldLayoutDefinitionKind, - FormComponentDefinitionKind + FormComponentDefinitionKind, KeyValueStringProperty } from "../shared.outline"; import { ButtonSectionAriaOrientationOptionsType, @@ -44,9 +44,10 @@ export class TabFieldComponentDefinition extends FieldComponentDefinition implem export class TabFieldLayoutConfig extends FieldLayoutConfig implements TabFieldLayoutConfigOutline { - buttonSectionCssClass?: string; - tabPaneCssClass?: string; - tabPaneActiveCssClass?: string; + hostCssClasses?: KeyValueStringProperty = 'd-flex align-items-start'; + buttonSectionCssClass?: string = 'nav flex-column nav-pills me-5'; + tabPaneCssClass?: string = 'tab-pane fade'; + tabPaneActiveCssClass?: string = 'active show'; buttonSectionAriaOrientation?: ButtonSectionAriaOrientationOptionsType = 'vertical'; constructor() { diff --git a/packages/sails-ng-common/src/config/visitor/base.model.ts b/packages/sails-ng-common/src/config/visitor/base.model.ts index 13edf19c8..924019d1d 100644 --- a/packages/sails-ng-common/src/config/visitor/base.model.ts +++ b/packages/sails-ng-common/src/config/visitor/base.model.ts @@ -429,6 +429,9 @@ export class PopulateProperties { throw new Error("Property name provided to setPropOverride was undefined or null."); } + // for debugging: + // console.log(`Get property ${JSON.stringify(name)} from sources ${JSON.stringify(sources)} for target ${JSON.stringify(target)}.`); + const propValue = [target, ...sources].findLast(val => val?.[name] !== undefined)?.[name]; if (propValue !== undefined) { target[name] = propValue; diff --git a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts index a8eb22887..71cf1eaaa 100644 --- a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts +++ b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts @@ -216,8 +216,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new SimpleInputFieldModelConfig(); this.sharedPopulateFieldModelConfig(item.config, field); - // maxLength: field?.definition?.maxLength ?? 0, - // 'model.config.validators', [ {name: 'maxLength', message: '', config: {maxLength: fieldConfig.maxLength}} ]); + + this.sharedProps.setPropOverride('type', item.config, field?.definition); } visitSimpleInputFormComponentDefinition(item: SimpleInputFormComponentDefinitionOutline): void { @@ -379,8 +379,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new TextAreaFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // cols: field?.definition?.cols ?? field?.definition?.columns ?? 0, - // rows: field?.definition?.rows ?? 0 + // TODO: cols: field?.definition?.cols ?? field?.definition?.columns ?? 0, + // TODO: rows: field?.definition?.rows ?? 0 } visitTextAreaFieldModelDefinition(item: TextAreaFieldModelDefinitionOutline): void { @@ -399,10 +399,6 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new DefaultFieldLayoutConfig(); this.sharedPopulateFieldLayoutConfig(item.config, field); - // config: { - // label: common.label, - // helpText: common.help, - // } } /* Checkbox Input */ @@ -411,7 +407,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new CheckboxInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // component.config.options = field?.definition?.options ?? {} + // TODO: component.config.options = field?.definition?.options ?? {} } visitCheckboxInputFieldModelDefinition(item: CheckboxInputFieldModelDefinitionOutline): void { @@ -430,7 +426,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new DropdownInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // component.config.options = field?.definition?.options ?? {} + // TODO: component.config.options = field?.definition?.options ?? {} } visitDropdownInputFieldModelDefinition(item: DropdownInputFieldModelDefinitionOutline): void { @@ -449,7 +445,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new RadioInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // component.config.options = field?.definition?.options ?? {} + // TODO: component.config.options = field?.definition?.options ?? {} } visitRadioInputFieldModelDefinition(item: RadioInputFieldModelDefinitionOutline): void { @@ -516,16 +512,25 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const fieldDefinition = (field?.definition ?? {}) as Record; // Overall mapping from v4 class, v4 compClass to v5 class. - const contentComponent = {componentClassName: "ContentComponent"}; - const groupComponent = {componentClassName: "GroupComponent", modelClassName: "GroupModel"}; - const repeatableComponent = {componentClassName: "RepeatableComponent", modelClassName: "RepeatableModel"}; - const tabComponent = {componentClassName: "TabComponent"}; - const tabContentComponent = {componentClassName: "TabContentComponent"}; - const textAreaComponent = {componentClassName: "TextAreaComponent", modelClassName: "TextAreaModel"}; - const simpleInputComponent = {componentClassName: "SimpleInputComponent", modelClassName: "SimpleInputModel"}; - const dropDownComponent = {componentClassName: "DropdownInputComponent", modelClassName: "DropdownInputModel"}; - const dateInputComponent = {componentClassName: "DateInputComponent", modelClassName: "DateInputModel"}; - const saveButtonComponent = {componentClassName: "SaveButtonComponent"}; + const contentComponent = {componentClassName: ContentComponentName}; + const groupComponent = {componentClassName: GroupFieldComponentName, modelClassName: GroupFieldModelName}; + const repeatableComponent = {componentClassName: RepeatableComponentName, modelClassName: RepeatableModelName}; + const tabComponent = {componentClassName: TabComponentName, layoutClassName: TabLayoutName}; + const tabContentComponent = { + componentClassName: TabContentComponentName, + layoutClassName: TabContentLayoutName + }; + const textAreaComponent = {componentClassName: TextAreaComponentName, modelClassName: TextAreaModelName}; + const simpleInputComponent = { + componentClassName: SimpleInputComponentName, + modelClassName: SimpleInputModelName + }; + const dropDownComponent = { + componentClassName: DropdownInputComponentName, + modelClassName: DropdownInputModelName + }; + const dateInputComponent = {componentClassName: DateInputComponentName, modelClassName: DateInputModelName}; + const saveButtonComponent = {componentClassName: SaveButtonComponentName}; const classMap: Record = { "Container__TextBlockComponent": contentComponent, "Container__GenericGroupComponent": groupComponent, @@ -549,7 +554,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const v5ClassNames = classMap[`${v4ClassName}__${v4CompClassName}`] ?? {}; let v5ComponentClassName = v5ClassNames.componentClassName || ""; let v5ModelClassName = v5ClassNames.modelClassName || ""; - let v5LayoutClassName = v5ClassNames.layoutClassName || ""; + let v5LayoutClassName = v5ClassNames.layoutClassName || DefaultLayoutName; // Some components need special processing. @@ -561,7 +566,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // Provide a message for not yet implemented fields. let message = ""; if (!v5ComponentClassName) { - const v4Name = fieldDefinition?.name; + const v4Name = fieldDefinition?.name || fieldDefinition?.id; message = `Not yet implemented v4: class ${JSON.stringify(v4ClassName)} compClass ${JSON.stringify(v4CompClassName)} name ${JSON.stringify(v4Name)}.`; } return { @@ -578,7 +583,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit let {componentClassName, modelClassName, layoutClassName, message} = this.mapV4ToV5(currentData); // Set the simple properties - item.name = currentData?.definition?.name || `${componentClassName}-${this.currentPath}`; + item.name = currentData?.definition?.name || currentData?.definition?.id || `${componentClassName}-${this.currentPath}`; item.module = undefined; // Set the constraints @@ -646,7 +651,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // this.sharedProps.setPropOverride('readonly', item, config); // this.sharedProps.setPropOverride('visible', item, config); // this.sharedProps.setPropOverride('editMode', item, config); - this.sharedProps.setPropOverride('label', item, field?.definition?.label); + this.sharedProps.setPropOverride('label', item, field?.definition); // this.sharedProps.setPropOverride('defaultComponentCssClasses', item, config); // this.sharedProps.setPropOverride('hostCssClasses', item, config); // this.sharedProps.setPropOverride('wrapperCssClasses', item, config); @@ -659,17 +664,26 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit // Set the common field model config properties // this.sharedProps.setPropOverride('disableFormBinding', item, config); // this.sharedProps.setPropOverride('value', item, config); - this.sharedProps.setPropOverride('defaultValue', item, field?.definition?.defaultValue); + this.sharedProps.setPropOverride('defaultValue', item, {defaultValue: field?.definition?.defaultValue ?? field?.definition?.value}); // this.sharedProps.setPropOverride('validators', item, config); // this.sharedProps.setPropOverride('wrapperCssClasses', item, config); // this.sharedProps.setPropOverride('editCssClasses', item, config); + if (!item.validators) { + item.validators = []; + } + if (field?.definition?.required === true) { + item.validators.push({class: 'required'}); + } + if (field?.definition?.maxLength !== undefined) { + item.validators.push({class: 'maxLength', config: {maxLength: field?.definition?.maxLength}}); + } } protected sharedPopulateFieldLayoutConfig(item: FieldLayoutConfigFrame, field?: any) { // Set the common field model config properties this.sharedPopulateFieldComponentConfig(item, field); // this.sharedProps.setPropOverride('labelRequiredStr', item, config); - this.sharedProps.setPropOverride('helpText', item, field?.definition?.help); + this.sharedProps.setPropOverride('helpText', item, {helpText: field?.definition?.help}); // this.sharedProps.setPropOverride('cssClassesMap', item, config); // this.sharedProps.setPropOverride('helpTextVisibleOnInit', item, config); // this.sharedProps.setPropOverride('helpTextVisible', item, config); From 9e8e2689b3431a1a91d51bb2883a9cb00fdb18dd Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Tue, 18 Nov 2025 02:58:54 +0000 Subject: [PATCH 5/5] migrate some component-specific properties --- .../src/config/visitor/base.model.ts | 7 +++++-- .../visitor/migrate-config-v4-v5.visitor.ts | 21 ++++++++++++------- .../src/config/visitor/validator.visitor.ts | 5 ++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/sails-ng-common/src/config/visitor/base.model.ts b/packages/sails-ng-common/src/config/visitor/base.model.ts index 924019d1d..f293324b8 100644 --- a/packages/sails-ng-common/src/config/visitor/base.model.ts +++ b/packages/sails-ng-common/src/config/visitor/base.model.ts @@ -429,10 +429,13 @@ export class PopulateProperties { throw new Error("Property name provided to setPropOverride was undefined or null."); } + const propValue = [target, ...sources].findLast(val => val?.[name] !== undefined)?.[name]; + // for debugging: - // console.log(`Get property ${JSON.stringify(name)} from sources ${JSON.stringify(sources)} for target ${JSON.stringify(target)}.`); + // if (name === 'options') { + // console.log(`Get property ${JSON.stringify(name)} from sources ${JSON.stringify(sources)} for target ${JSON.stringify(target)} propValue ${JSON.stringify(propValue)}.`); + // } - const propValue = [target, ...sources].findLast(val => val?.[name] !== undefined)?.[name]; if (propValue !== undefined) { target[name] = propValue; } diff --git a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts index 71cf1eaaa..586751317 100644 --- a/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts +++ b/packages/sails-ng-common/src/config/visitor/migrate-config-v4-v5.visitor.ts @@ -210,6 +210,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new SimpleInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); + this.sharedProps.setPropOverride('type', item.config, field?.definition); } visitSimpleInputFieldModelDefinition(item: SimpleInputFieldModelDefinitionOutline): void { @@ -217,7 +218,6 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit item.config = new SimpleInputFieldModelConfig(); this.sharedPopulateFieldModelConfig(item.config, field); - this.sharedProps.setPropOverride('type', item.config, field?.definition); } visitSimpleInputFormComponentDefinition(item: SimpleInputFormComponentDefinitionOutline): void { @@ -379,8 +379,12 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new TextAreaFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // TODO: cols: field?.definition?.cols ?? field?.definition?.columns ?? 0, - // TODO: rows: field?.definition?.rows ?? 0 + + const cols = field?.definition?.cols ?? field?.definition?.columns ?? undefined; + this.sharedProps.setPropOverride('cols', item.config, {cols: cols === undefined ? undefined : parseInt(cols)}); + + const rows = field?.definition?.rows ?? undefined; + this.sharedProps.setPropOverride('rows', item.config, {rows: rows === undefined ? undefined : parseInt(rows)}); } visitTextAreaFieldModelDefinition(item: TextAreaFieldModelDefinitionOutline): void { @@ -407,7 +411,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new CheckboxInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // TODO: component.config.options = field?.definition?.options ?? {} + + this.sharedProps.setPropOverride('options', item.config, field?.definition); } visitCheckboxInputFieldModelDefinition(item: CheckboxInputFieldModelDefinitionOutline): void { @@ -426,7 +431,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new DropdownInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // TODO: component.config.options = field?.definition?.options ?? {} + + this.sharedProps.setPropOverride('options', item.config, field?.definition); } visitDropdownInputFieldModelDefinition(item: DropdownInputFieldModelDefinitionOutline): void { @@ -445,7 +451,8 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit const field = this.getDataPath(this.original, this.currentPath); item.config = new RadioInputFieldComponentConfig(); this.sharedPopulateFieldComponentConfig(item.config, field); - // TODO: component.config.options = field?.definition?.options ?? {} + + this.sharedProps.setPropOverride('options', item.config, field?.definition); } visitRadioInputFieldModelDefinition(item: RadioInputFieldModelDefinitionOutline): void { @@ -583,7 +590,7 @@ export class MigrationV4ToV5FormConfigVisitor extends CurrentPathFormConfigVisit let {componentClassName, modelClassName, layoutClassName, message} = this.mapV4ToV5(currentData); // Set the simple properties - item.name = currentData?.definition?.name || currentData?.definition?.id || `${componentClassName}-${this.currentPath}`; + item.name = currentData?.definition?.name || currentData?.definition?.id || `${componentClassName}-${this.currentPath.join('-')}`; item.module = undefined; // Set the constraints diff --git a/packages/sails-ng-common/src/config/visitor/validator.visitor.ts b/packages/sails-ng-common/src/config/visitor/validator.visitor.ts index 600add7a6..56c2851b0 100644 --- a/packages/sails-ng-common/src/config/visitor/validator.visitor.ts +++ b/packages/sails-ng-common/src/config/visitor/validator.visitor.ts @@ -387,7 +387,10 @@ export class ValidatorFormConfigVisitor extends CurrentPathFormConfigVisitor { ); const formValidatorFns = createFormValidatorFns(this.validatorDefinitionsMap, filteredValidators); const recordFormControl = this.createFormControlFromRecordValue(value); - this.logger.verbose(`validateFormComponent createFormControlFromRecordValue: ${JSON.stringify(recordFormControl)}`) + + // For debugging: + // this.logger.verbose(`validateFormComponent createFormControlFromRecordValue: ${JSON.stringify(recordFormControl)}`) + const summaryErrors: FormValidatorSummaryErrors = { id: itemName, message: message || null,