import type { ControlComponentBindingsConfig, ControlComponentPropSource, ControlComponentPropsByIdMap, ControlComponentPropsMap, ControlModelBinding, ControlSchema, DoseCalculatorControlPropsOptions, DoseDesignControlModelOptions, WellPlateControlPropsOptions, WellPlateDoseControlPropsOptions, } from './controlSchemaTypes' import type { WellConcentration } from './useDoseCalculator' import { defineControlModel, } from './controlSchemaModel' import { recordValue, } from './controlSchemaUtils' /** Return a default WellPlate prop mapping for generated control workspaces. */ export function defineWellPlateControlProps = Record>( options: WellPlateControlPropsOptions = {}, ): ControlComponentPropsMap { const selectedWells = options.selectedWells ?? sourceKey('selectedWells') const onUpdateModelValue = options.onUpdateModelValue === false ? undefined : options.onUpdateModelValue ?? updateControlValueSource(selectedWells) return compactComponentPropsMap({ modelValue: selectedWells, 'onUpdate:modelValue': onUpdateModelValue, format: options.format ?? sourceKey('plateFormat'), wells: options.wells ?? sourceKey('wells'), disabled: options.disabled ?? sourceKey('disabled'), readonly: options.readonly ?? sourceKey('readonly'), showLegend: options.showLegend, showSampleTypeIndicator: options.showSampleTypeIndicator, }) } /** Return a default DoseCalculator prop mapping for generated control workspaces. */ export function defineDoseCalculatorControlProps = Record>( options: DoseCalculatorControlPropsOptions = {}, ): ControlComponentPropsMap { const targetWells = options.targetWells ?? sourceKey('selectedWells') const wells = options.wells ?? sourceKey('wells') const onApplyToWells = options.onApplyToWells === false ? undefined : options.onApplyToWells ?? defaultDoseApplyHandler(wells, targetWells) return compactComponentPropsMap({ mode: options.mode ?? sourceKey('doseMode'), targetWells, disabled: options.disabled ?? sourceKey('disabled'), molecularWeight: options.molecularWeight, onApplyToWells, }) } /** Return named WellPlate + DoseCalculator prop mappings for one dose-design control model. */ export function defineWellPlateDoseControlProps = Record>( options: WellPlateDoseControlPropsOptions = {}, ): ControlComponentPropsByIdMap { return { [options.plateId ?? 'plate']: defineWellPlateControlProps(options.plate), [options.doseId ?? 'dose']: defineDoseCalculatorControlProps(options.dose), } } /** Return named WellPlate + DoseCalculator component bindings for one dose-design control model. */ export function defineWellPlateDoseComponentBindings = Record>( options: WellPlateDoseControlPropsOptions = {}, ): ControlComponentBindingsConfig { return [ { id: options.plateId ?? 'plate', component: 'WellPlate', props: defineWellPlateControlProps(options.plate), }, { id: options.doseId ?? 'dose', component: 'DoseCalculator', props: defineDoseCalculatorControlProps(options.dose), }, ] } /** * Return a complete ControlWorkspaceView model for WellPlate + DoseCalculator dose design. * * @example Recommended * ```ts * const workspaceModel = defineDoseDesignControlModel({ selectedWells: ['A1', 'A2'] }) * ``` */ export function defineDoseDesignControlModel( options: DoseDesignControlModelOptions = {}, ): ControlModelBinding { const viewId = options.viewId ?? 'design' const sectionId = options.sectionId ?? 'dose' const doseControls: ControlSchema = { selectedWells: { type: 'tags', label: 'Selected wells', default: [...(options.selectedWells ?? ['A1', 'A2'])], }, plateFormat: { label: 'Plate format', default: options.plateFormat ?? 96, options: options.plateFormats ?? [96, 384], }, doseMode: { label: 'Dose mode', default: options.doseMode ?? 'serial', options: options.doseModes ?? ['serial', 'dilution', 'conversion'], }, disabled: { label: 'Disable tools', default: options.disabled ?? false, }, } if (options.includeMolecularWeight) { doseControls.molecularWeight = { type: 'number', label: 'Molecular weight', default: options.molecularWeight ?? 300, min: 0, } } const componentProps: WellPlateDoseControlPropsOptions = { ...options.componentProps, dose: { ...(options.componentProps?.dose ?? {}), ...(options.includeMolecularWeight && options.componentProps?.dose?.molecularWeight === undefined ? { molecularWeight: 'molecularWeight' } : {}), }, } return defineControlModel({ componentBindings: defineWellPlateDoseComponentBindings(componentProps), componentPropsById: defineWellPlateDoseControlProps(componentProps), views: { [viewId]: { label: options.viewLabel ?? 'Design', sections: { [sectionId]: { label: options.sectionLabel ?? 'Dose design', description: options.sectionDescription ?? 'Well selection and dose calculator controls', controls: doseControls, }, }, }, }, }) } function compactComponentPropsMap>( mapping: Record | undefined>, ): ControlComponentPropsMap { return Object.fromEntries( Object.entries(mapping).filter((entry): entry is [string, ControlComponentPropSource] => entry[1] !== undefined ), ) } function sourceKey>(key: string): keyof TValues & string { return key as keyof TValues & string } function updateControlValueSource>( source: ControlComponentPropSource, ): ((values: TValues) => (value: string[]) => void) | undefined { if (typeof source !== 'string') return undefined return values => (value: string[]) => { const writableValues = values as Record writableValues[source] = value } } function defaultDoseApplyHandler>( wells: ControlComponentPropSource, targetWells: ControlComponentPropSource, ): ((values: TValues) => (results: WellConcentration[]) => void) | undefined { if (wells === targetWells) return undefined return updateWellsFromDoseResults(wells) } function updateWellsFromDoseResults>( source: ControlComponentPropSource, ): ((values: TValues) => (results: WellConcentration[]) => void) | undefined { if (typeof source !== 'string') return undefined return values => (results: WellConcentration[]) => { const writableValues = values as Record const currentWells = recordValue(writableValues[source]) const nextWells: Record> = {} for (const [wellId, well] of Object.entries(currentWells)) { nextWells[wellId] = recordValue(well) } for (const result of results) { const existing = recordValue(nextWells[result.wellId]) const metadata = recordValue(existing.metadata) nextWells[result.wellId] = { ...existing, state: 'filled', value: result.concentration.value, metadata: { ...metadata, concentration: result.concentration, ...(result.volume === undefined ? {} : { volume: result.volume }), }, } } writableValues[source] = nextWells } }