import { List, Map, OrderedSet, Set } from "immutable"; import { Unit, Guid, ParsedFormJSON, BuiltIns, Sum, BasicFun, Template, unit, EditFormState, EditFormTemplate, ApiErrors, CreateFormTemplate, EntityFormTemplate, CommonFormState, CreateFormState, EditFormContext, CreateFormContext, Synchronized, simpleUpdater, defaultValue, fromAPIRawValue, toAPIRawValue, EditFormForeignMutationsExpected, ParsedType, InjectedPrimitives, Injectables, ApiConverters, ApiResponseChecker, Debounced, ParsedFormConfig, PredicateValue, FieldPredicateExpressions, FormFieldPredicateEvaluation, ValueOrErrors, PassthroughFormState, PassthroughFormContext, PassthroughFormTemplate, defaultState, FieldName, ParsedRecordFormConfig, ParsedTableFormConfig, TableApiSources, PredicateComputedOrInlined, } from "../../../../main"; import { EnumReference } from "../collection/domains/reference/state"; import { SearchableInfiniteStreamState } from "../primitives/domains/searchable-infinite-stream/state"; import { RecordForm } from "../singleton/template"; import { ParsedRenderer } from "./domains/renderer/state"; export type ParsedRecordForm = { initialFormState: any; formConfig: any; formName: string; formDef: ParsedRecordFormConfig; visibilityPredicateExpressions: FieldPredicateExpressions; disabledPredicatedExpressions: FieldPredicateExpressions; fieldLabels: Map; form: EntityFormTemplate; }; export type ParsedTableForm = { initialFormState: any; formConfig: any; formName: string; formDef: ParsedTableFormConfig; visibleColumns: PredicateComputedOrInlined; columnHeaders: Map; form: EntityFormTemplate; }; export const ParseRecordForm = ( formName: string, formDef: ParsedRecordFormConfig, nestedContainerFormView: any, formViews: Record>, forms: ParsedForms, fieldsViewsConfig: any, infiniteStreamSources: any, enumOptionsSources: EnumOptionsSources, defaultValue: BasicFun, any>, defaultState: BasicFun, any>, injectedPrimitives?: InjectedPrimitives, tableApiSources?: TableApiSources, ): Omit, "form"> => { const formConfig: any = {}; let visibilityPredicateExpressions: FieldPredicateExpressions = Map(); let disabledPredicatedExpressions: FieldPredicateExpressions = Map(); let fieldLabels: Map = Map(); const initialFormState: any = { commonFormState: CommonFormState.Default(), formFieldStates: {}, }; const fieldNames = Object.keys(fieldsViewsConfig); fieldNames.forEach((fieldName) => { const parsedFormConfig = ParsedRenderer.Operations.RendererToForm( fieldName, { formViews, forms, nestedContainerFormView, defaultValue, defaultState, enumOptionsSources, infiniteStreamSources, injectedPrimitives, tableApiSources, }, formDef.fields.get(fieldName)!, ); if (parsedFormConfig.kind == "errors") { console.error(parsedFormConfig.errors.toJS()); throw Error(`Error parsing form ${fieldsViewsConfig[fieldName]}`); } formConfig[fieldName] = parsedFormConfig.value.form.renderer; visibilityPredicateExpressions = visibilityPredicateExpressions.set( fieldName, parsedFormConfig.value.visibilityPredicateExpression, ); disabledPredicatedExpressions = disabledPredicatedExpressions.set( fieldName, parsedFormConfig.value.disabledPredicatedExpression, ); fieldLabels = fieldLabels.set(fieldName, parsedFormConfig.value.label); initialFormState["formFieldStates"][fieldName] = parsedFormConfig.value.form.initialState; }); return { initialFormState, formName, formDef, formConfig, visibilityPredicateExpressions, disabledPredicatedExpressions, fieldLabels, }; }; export const ParseTableForm = ( formName: string, formDef: ParsedTableFormConfig, nestedContainerFormView: any, formViews: Record>, forms: ParsedForms, cellViewsConfig: any, infiniteStreamSources: any, enumOptionsSources: EnumOptionsSources, defaultValue: BasicFun, any>, defaultState: BasicFun, any>, injectedPrimitives?: InjectedPrimitives, tableApiSources?: TableApiSources, ): Omit, "form"> => { const formConfig: any = {}; let columnHeaders: Map = Map(); // const getChunk = infiniteStreamSources(formDef.api); const initialFormState: any = { commonFormState: CommonFormState.Default(), customFormState: { // streamParams: Debounced.Default(Map()), // stream: ValueInfiniteStreamState().Default(10, Map()), isInitialized: false, }, }; const columnNames = Object.keys(cellViewsConfig); columnNames.forEach((columnName) => { const parsedFormConfig = ParsedRenderer.Operations.RendererToForm( columnName, { formViews, forms, nestedContainerFormView, defaultValue, defaultState, enumOptionsSources, infiniteStreamSources, injectedPrimitives, tableApiSources, }, formDef.columns.get(columnName)!, ); if (parsedFormConfig.kind == "errors") { console.error(parsedFormConfig.errors.toJS()); throw Error( `Error parsing column renderer ${cellViewsConfig[columnName]}`, ); } formConfig[columnName] = parsedFormConfig.value.form.renderer; columnHeaders = columnHeaders.set(columnName, parsedFormConfig.value.label); }); return { initialFormState, formConfig, formDef, formName, visibleColumns: formDef.visibleColumns, columnHeaders, }; }; export const ParseForms = ( builtIns: BuiltIns, injectedPrimitives: InjectedPrimitives | undefined, nestedContainerFormView: any, fieldViews: any, infiniteStreamSources: any, enumOptionsSources: EnumOptionsSources, tableApiSources?: TableApiSources, ) => (formsConfig: ParsedFormJSON): ValueOrErrors, string> => { let errors: FormParsingErrors = List(); let seen = Set(); let formProcessingOrder = OrderedSet(); let parsedForms: ParsedForms = Map(); const traverse = (formDef: ParsedFormConfig) => { if (formProcessingOrder.has(formDef.name)) { return; } if (seen.has(formDef.name)) { errors.push( `aborting: cycle detected when parsing forms: ${JSON.stringify( formProcessingOrder.reverse().toArray(), )} -> ${formDef.name}`, ); return; } seen = seen.add(formDef.name); const formFields = formDef.kind == "recordForm" ? formDef.fields : formDef.columns; formFields.forEach((field, fieldName) => { if ( field.type.kind == "lookup" || field.type.kind == "record" || field.type.kind == "table" ) { traverse(formsConfig.forms.get(field.renderer)!); } try { if (field.kind == "list") { if (typeof field.elementRenderer == "string") throw Error( "Deprecated element renderer as string, use a render object instead - check parser.", ); if (formsConfig.forms.has(field.elementRenderer.renderer)) traverse(formsConfig.forms.get(field.elementRenderer.renderer)!); } if (field.kind == "map") { const keyRenderer = field.keyRenderer; const valueRenderer = field.valueRenderer; if (keyRenderer && formsConfig.forms.has(keyRenderer.renderer)) { traverse(formsConfig.forms.get(keyRenderer.renderer)!); } if ( valueRenderer && formsConfig.forms.has(valueRenderer.renderer) ) { traverse(formsConfig.forms.get(valueRenderer.renderer)!); } } if (field.kind == "sum") { if ( field.leftRenderer && formsConfig.forms.has(field.leftRenderer.renderer) ) { traverse(formsConfig.forms.get(field.leftRenderer.renderer)!); } if ( field.rightRenderer && formsConfig.forms.has(field.rightRenderer.renderer) ) { traverse(formsConfig.forms.get(field.rightRenderer.renderer)!); } } if (field.kind == "tuple") { field.itemRenderers.forEach((itemRenderer) => { if (formsConfig.forms.has(itemRenderer.renderer)) { traverse(formsConfig.forms.get(itemRenderer.renderer)!); } }); } } catch (error: any) { console.error(`error parsing field :${fieldName}:: `, error); errors.push(error.message ?? error); } }); formProcessingOrder = formProcessingOrder.add(formDef.name); }; const allForms = formsConfig.forms.valueSeq().toArray(); allForms.forEach((form) => { seen = seen.clear(); traverse(form); }); formProcessingOrder.forEach((formName) => { const formConfig = formsConfig.forms.get(formName)!; try { if (formConfig.kind == "recordForm") { const formFieldRenderers = formConfig.fields .map((field) => field.renderer) .toObject(); const parsedForm = ParseRecordForm( formName, formConfig, nestedContainerFormView, fieldViews, parsedForms, formFieldRenderers, infiniteStreamSources, enumOptionsSources, defaultValue(formsConfig.types, builtIns, injectedPrimitives), defaultState(formsConfig.types, builtIns, injectedPrimitives), injectedPrimitives, tableApiSources, ); const formBuilder = RecordForm().Default(); const form = formBuilder .template( { ...parsedForm.formConfig, }, parsedForm.fieldLabels, ) .mapContext((_) => { return { type: parsedForm.formDef.type, visible: (_ as any).visible ?? true, disabled: (_ as any).disabled ?? false, label: (_ as any).label, value: (_ as any).value, commonFormState: (_ as any).commonFormState, formFieldStates: (_ as any).formFieldStates, rootValue: (_ as any).rootValue, extraContext: (_ as any).extraContext, visibilities: (_ as any).visibilities, disabledFields: (_ as any).disabledFields, globalConfiguration: (_ as any).globalConfiguration, visibilityPredicateExpressions: parsedForm.visibilityPredicateExpressions, disabledPredicateExpressions: parsedForm.disabledPredicatedExpressions, layout: formConfig.tabs, fromApiParserByType: (_ as any).fromApiParserByType, }; }); parsedForms = parsedForms.set(formName, { ...parsedForm, form, }); } } catch (error: any) { console.error(error); errors.push(error.message ?? error); } }); if (errors.size > 0) { return ValueOrErrors.Default.throw(errors); } return ValueOrErrors.Default.return(parsedForms); }; export type EditLauncherContext = Omit< EditFormContext & EditFormState & { extraContext: ExtraContext; containerFormView: any; submitButtonWrapper: any; }, "api" | "parser" | "actualForm" >; export type CreateLauncherContext = Omit< CreateFormContext & CreateFormState & { extraContext: ExtraContext; containerFormView: any; submitButtonWrapper: any; }, "api" | "actualForm" >; export type PassthroughLauncherContext = Omit< PassthroughFormContext & PassthroughFormState & { extraContext: ExtraContext; containerFormView: any; containerWrapper: any; }, "api" | "actualForm" >; export type ParsedLaunchers = { create: Map< string, () => { form: Template< CreateLauncherContext & CreateFormState, CreateFormState, Unit >; initialState: CreateFormState; } >; edit: Map< string, () => { form: Template< EditLauncherContext & EditFormState, EditFormState, EditFormForeignMutationsExpected >; initialState: EditFormState; } >; passthrough: Map< string, () => { form: Template< PassthroughLauncherContext & PassthroughFormState, PassthroughFormState, Unit >; initialState: PassthroughFormState; fromApiParser: (value: any) => ValueOrErrors; toApiParser: ( value: PredicateValue, formState: any, type: ParsedType, ) => ValueOrErrors; parseGlobalConfiguration: ( raw: any, ) => ValueOrErrors; parsedFormType: ParsedType; parsedForms: Map>; } >; }; export type ParsedForms = Map< string, ParsedRecordForm | ParsedTableForm >; export type FormParsingErrors = List; export type FormParsingResult = Sum; export type EnumName = string; export type EnumOptionsSources = BasicFun< EnumName, BasicFun>> >; export type StreamName = string; export type InfiniteStreamSources = BasicFun< StreamName, SearchableInfiniteStreamState["customFormState"]["getChunk"] >; export type ConfigName = string; export type GlobalConfigurationSources = BasicFun>; export type EntityName = string; export type EntityApis = { create: BasicFun>>; default: BasicFun>>; update: BasicFun Promise>; get: BasicFun>>; }; export const parseFormsToLaunchers = ( builtIns: BuiltIns, injectedPrimitives: InjectedPrimitives | undefined, apiConverters: ApiConverters, containerFormView: any, nestedContainerFormView: any, fieldViews: any, infiniteStreamSources: InfiniteStreamSources, enumOptionsSources: EnumOptionsSources, entityApis: EntityApis, tableApiSources?: TableApiSources, ) => (formsConfig: ParsedFormJSON): FormParsingResult => { let parsedLaunchers: ParsedLaunchers = { create: Map(), edit: Map(), passthrough: Map(), }; const parsedFormsResult = ParseForms( builtIns, injectedPrimitives, nestedContainerFormView, fieldViews, infiniteStreamSources, enumOptionsSources, tableApiSources, )(formsConfig); if (parsedFormsResult.kind == "errors") { console.error(parsedFormsResult.errors); return Sum.Default.right(parsedFormsResult.errors); } const parsedForms = parsedFormsResult.value; formsConfig.launchers.edit.forEach((launcher, launcherName) => { const parsedForm = parsedForms.get(launcher.form)! as ParsedRecordForm; const form = parsedForm.form; const initialState = parsedForm.initialFormState; const formType = parsedForm.formDef.type; const globalConfigEntity = formsConfig.apis.entities.get( launcher.configApi, )!; const globalConfigurationType = formsConfig.types.get( globalConfigEntity.type, )!; const visibilityPredicateExpressions = parsedForm.visibilityPredicateExpressions; const disabledPredicatedExpressions = parsedForm.disabledPredicatedExpressions; const api = { getGlobalConfiguration: () => entityApis.get(launcher.configApi)(""), get: (id: string) => entityApis.get(launcher.api)(id), update: (id: any, parsed: any) => { return parsed.kind == "errors" ? Promise.reject(parsed.errors) : entityApis.update(launcher.api)(id, parsed.value); }, }; parsedLaunchers.edit = parsedLaunchers.edit.set(launcherName, < T, FormState, ExtraContext, Context extends EditLauncherContext, >() => ({ form: EditFormTemplate() .mapContext( (parentContext: Context) => ({ value: parentContext.entity.sync.kind == "loaded" ? parentContext.entity.sync.value : undefined, entity: parentContext.entity, globalConfiguration: parentContext.globalConfiguration, entityId: parentContext.entityId, commonFormState: parentContext.commonFormState, customFormState: parentContext.customFormState, formFieldStates: parentContext.formFieldStates, extraContext: parentContext.extraContext, visibilityPredicateExpressions, disabledPredicatedExpressions, types: formsConfig.types, formType: formType, api: api, parseGlobalConfiguration: (raw: any) => fromAPIRawValue( globalConfigurationType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(raw), fromApiParser: (value: any) => fromAPIRawValue( formType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value), toApiParser: (value: PredicateValue, formState: any) => toAPIRawValue( formType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value, formState), actualForm: form .withView(containerFormView) .mapContext((_: any) => ({ value: _.value, toApiParser: parentContext.toApiParser, fromApiParser: parentContext.fromApiParser, parseGlobalConfiguration: parentContext.parseGlobalConfiguration, formFieldStates: parentContext.formFieldStates, rootValue: _.value, extraContext: parentContext.extraContext, commonFormState: parentContext.commonFormState, predicateEvaluations: parentContext.customFormState.predicateEvaluations, visibilities: _.visibilities, disabledFields: _.disabledFields, globalConfiguration: parentContext.globalConfiguration.sync, })), }) as any, ) .withViewFromProps((props) => props.context.submitButtonWrapper) .mapForeignMutationsFromProps( (props) => props.foreignMutations as any, ), initialState: EditFormState().Default( initialState.formFieldStates, initialState.commonFormState, { initApiChecker: ApiResponseChecker.Default(true), updateApiChecker: ApiResponseChecker.Default(true), configApiChecker: ApiResponseChecker.Default(true), apiRunner: Debounced.Default(Synchronized.Default(unit)), predicateEvaluations: Debounced.Default( ValueOrErrors.Default.return({ visiblityPredicateEvaluations: FormFieldPredicateEvaluation.Default.form(false, Map()), disabledPredicateEvaluations: FormFieldPredicateEvaluation.Default.form(false, Map()), }), ), }, ), })); }); formsConfig.launchers.create.forEach((launcher, launcherName) => { const parsedForm = parsedForms.get(launcher.form)! as ParsedRecordForm; const form = parsedForm.form; const initialState = parsedForm.initialFormState; const formType = parsedForm.formDef.type; const globalConfigEntity = formsConfig.apis.entities.get( launcher.configApi, )!; const globalConfigurationType = formsConfig.types.get( globalConfigEntity.type, )!; const visibilityPredicateExpressions = parsedForm.visibilityPredicateExpressions; const disabledPredicatedExpressions = parsedForm.disabledPredicatedExpressions; const api = { getGlobalConfiguration: () => entityApis.get(launcher.configApi)(""), default: (_: Unit) => entityApis.default(launcher.api)(unit), create: (parsed: any) => parsed.kind == "errors" ? Promise.reject(parsed.errors) : entityApis.create(launcher.api)(parsed.value), }; parsedLaunchers.create = parsedLaunchers.create.set(launcherName, < T, FormState, ExtraContext, Context extends CreateLauncherContext, >() => ({ form: CreateFormTemplate() .mapContext((parentContext: Context) => { return { value: parentContext.entity.sync.kind == "loaded" ? parentContext.entity.sync.value : undefined, entity: parentContext.entity, globalConfiguration: parentContext.globalConfiguration, entityId: parentContext.entityId, commonFormState: parentContext.commonFormState, customFormState: parentContext.customFormState, formFieldStates: parentContext.formFieldStates, extraContext: parentContext.extraContext, visibilityPredicateExpressions, disabledPredicatedExpressions, types: formsConfig.types, formType: formType, api: api, parseGlobalConfiguration: (raw: any) => fromAPIRawValue( globalConfigurationType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(raw), fromApiParser: (value: any) => fromAPIRawValue( formType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value), toApiParser: (value: PredicateValue, formState: any) => toAPIRawValue( formType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value, formState), actualForm: form .withView(containerFormView) .mapContext((_: any) => { return { value: _.value, toApiParser: parentContext.toApiParser, fromApiParser: parentContext.fromApiParser, parseGlobalConfiguration: parentContext.parseGlobalConfiguration, formFieldStates: parentContext.formFieldStates, rootValue: _.value, extraContext: parentContext.extraContext, commonFormState: parentContext.commonFormState, predicateEvaluations: parentContext.customFormState.predicateEvaluations, visibilities: _.visibilities, disabledFields: _.disabledFields, globalConfiguration: parentContext.globalConfiguration.sync, }; }), } as any; }) .withViewFromProps((props) => props.context.submitButtonWrapper) .mapForeignMutationsFromProps( (props) => props.foreignMutations as any, ), initialState: CreateFormState().Default( initialState.formFieldStates, initialState.commonFormState, { initApiChecker: ApiResponseChecker.Default(true), createApiChecker: ApiResponseChecker.Default(true), configApiChecker: ApiResponseChecker.Default(true), apiRunner: Debounced.Default(Synchronized.Default(unit)), predicateEvaluations: Debounced.Default( ValueOrErrors.Default.return({ visiblityPredicateEvaluations: FormFieldPredicateEvaluation.Default.form(false, Map()), disabledPredicateEvaluations: FormFieldPredicateEvaluation.Default.form(false, Map()), }), ), }, ), })); }); formsConfig.launchers.passthrough.forEach((launcher, launcherName) => { const parsedForm = parsedForms.get(launcher.form)! as ParsedRecordForm; const form = parsedForm.form; const globalConfigurationType = formsConfig.types.get( launcher.configType, )!; const initialState = parsedForm.initialFormState; const formType = parsedForm.formDef.type; const visibilityPredicateExpressions = parsedForm.visibilityPredicateExpressions; const disabledPredicatedExpressions = parsedForm.disabledPredicatedExpressions; parsedLaunchers.passthrough = parsedLaunchers.passthrough.set( launcherName, < T, FormState, ExtraContext, Context extends PassthroughLauncherContext< T, FormState, ExtraContext >, >() => ({ form: PassthroughFormTemplate() .mapContext( (parentContext: Context) => ({ entity: parentContext.entity, globalConfiguration: parentContext.globalConfiguration, commonFormState: parentContext.commonFormState, customFormState: parentContext.customFormState, formFieldStates: parentContext.formFieldStates, extraContext: parentContext.extraContext, visibilityPredicateExpressions, disabledPredicatedExpressions, types: formsConfig.types, formType: formType, onEntityChange: parentContext.onEntityChange, actualForm: form .withView(containerFormView) .mapContext((_: any) => ({ type: _.type, value: _.value, entity: _.entity, formFieldStates: parentContext.formFieldStates, rootValue: _.value, extraContext: parentContext.extraContext, commonFormState: parentContext.commonFormState, predicateEvaluations: parentContext.customFormState.predicateEvaluations, visibilities: _.visibilities, disabledFields: _.disabledFields, globalConfiguration: parentContext.globalConfiguration, fromApiParserByType: (type: ParsedType) => (value: any): ValueOrErrors => fromAPIRawValue( type, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value), })), }) as any, ) .withViewFromProps((props) => props.context.containerWrapper) .mapForeignMutationsFromProps( (props) => props.foreignMutations as any, ), initialState: PassthroughFormState().Default( initialState.formFieldStates, initialState.commonFormState, ), parsedFormType: formType, parsedForms: formsConfig.types, fromApiParser: (value: any): ValueOrErrors => fromAPIRawValue( formType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value), toApiParser: ( value: PredicateValue, formState: any, type: ParsedType, ) => toAPIRawValue( type, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(value, formState), parseGlobalConfiguration: (raw: any) => fromAPIRawValue( globalConfigurationType, formsConfig.types, builtIns, apiConverters, injectedPrimitives, )(raw), }), ); }); return Sum.Default.left(parsedLaunchers); }; export type FormsParserContext< T extends { [key in keyof T]: { type: any; state: any } }, > = { containerFormView: any; nestedContainerFormView: any; fieldViews: any; fieldTypeConverters: ApiConverters; infiniteStreamSources: InfiniteStreamSources; enumOptionsSources: EnumOptionsSources; entityApis: EntityApis; getFormsConfig: BasicFun>; injectedPrimitives?: Injectables; tableApiSources?: TableApiSources; }; export type FormsParserState = { formsConfig: Synchronized; }; export const FormsParserState = { Default: (): FormsParserState => ({ formsConfig: Synchronized.Default(unit), }), Updaters: { ...simpleUpdater()("formsConfig"), }, };