import { computed, reactive, watch } from 'vue' import type { ComputedRef } from 'vue' import type { PillNavItem } from '../types' import type { ControlComponentBinding, ControlComponentBindingsById, ControlComponentBindingsConfig, ControlComponentPropsByIdMap, ControlComponentPropsMap, ControlModel, ControlModelBinding, ControlSchema, ControlWorkspaceOptions, } from '../composables/useControlSchema' import { controlValuesToComponentBindings, controlValuesToComponentBindingsById, controlValuesToComponentProps, controlsToViewItems, getControlDefaults, getDefaultControlView, mergeControlWorkspaceOptions, resolveControlModel, } from '../composables/useControlSchema' export interface PluginWorkspaceControlsOptions { model: () => ControlModel | ControlModelBinding | undefined controls: () => ControlSchema | undefined controlOptions: () => ControlWorkspaceOptions modelValue: () => Record | undefined values: () => Record | undefined componentBindings: () => ControlComponentBindingsConfig | undefined componentProps: () => ControlComponentPropsMap | undefined componentPropsById: () => ControlComponentPropsByIdMap | undefined emitValues: (values: Record) => void } export interface PluginWorkspaceControls { resolvedModel: ComputedRef resolvedControls: ComputedRef resolvedControlOptions: ComputedRef generatedPillNav: ComputedRef generatedDefaultView: ComputedRef internalControlValues: Record resolvedComponentBindings: ComputedRef resolvedComponentBindingsById: ComputedRef resolvedComponentProps: ComputedRef> resolvedComponentPropsById: ComputedRef>> handleControlValuesUpdate: (values: Record) => void } export function usePluginWorkspaceControls( options: PluginWorkspaceControlsOptions, ): PluginWorkspaceControls { const resolvedModel = computed(() => resolveControlModel(options.model()), ) const resolvedControls = computed(() => options.controls() ?? resolvedModel.value?.controls, ) const resolvedControlOptions = computed(() => mergeControlWorkspaceOptions(resolvedModel.value?.controlOptions ?? {}, options.controlOptions()), ) const externalControlValues = computed(() => options.modelValue() ?? options.values()) const internalControlValues = reactive>({}) let syncingControlValues = false const generatedControlDefaults = computed>(() => { if (!resolvedControls.value) return {} return { ...getControlDefaults(resolvedControls.value), ...(resolvedControlOptions.value.initialValues ?? {}), } }) function replaceControlValues(values: Record) { for (const key of Object.keys(internalControlValues)) { if (!(key in values)) { delete internalControlValues[key] } } Object.assign(internalControlValues, values) } function emitControlValues() { options.emitValues({ ...internalControlValues }) } function syncResolvedControlValues() { syncingControlValues = true replaceControlValues({ ...generatedControlDefaults.value, ...(externalControlValues.value === undefined ? internalControlValues : externalControlValues.value), }) syncingControlValues = false } function handleControlValuesUpdate(values: Record) { syncingControlValues = true replaceControlValues({ ...generatedControlDefaults.value, ...values, }) syncingControlValues = false emitControlValues() } const resolvedComponentBindings = computed(() => controlValuesToComponentBindings( internalControlValues, options.componentBindings() ?? resolvedModel.value?.componentBindings, ), ) const resolvedComponentBindingsById = computed(() => controlValuesToComponentBindingsById( internalControlValues, options.componentBindings() ?? resolvedModel.value?.componentBindings, ), ) const resolvedComponentProps = computed(() => { const mapping = options.componentProps() ?? resolvedModel.value?.componentProps return mapping === undefined ? {} : controlValuesToComponentProps(internalControlValues, mapping) }) const resolvedComponentPropsById = computed(() => { const mappings = options.componentPropsById() ?? resolvedModel.value?.componentPropsById if (mappings === undefined) return {} return Object.fromEntries( Object.entries(mappings).map(([id, mapping]) => [ id, controlValuesToComponentProps(internalControlValues, mapping), ]), ) }) const generatedPillNav = computed(() => { if (!resolvedControls.value) return [] return controlsToViewItems(resolvedControls.value, resolvedControlOptions.value) }) const generatedDefaultView = computed(() => { if (!resolvedControls.value) return '' return getDefaultControlView(resolvedControls.value, resolvedControlOptions.value) }) syncResolvedControlValues() watch( [generatedControlDefaults, externalControlValues], syncResolvedControlValues, { deep: true }, ) watch( internalControlValues, () => { if (syncingControlValues) return emitControlValues() }, { deep: true, flush: 'sync' }, ) return { resolvedModel, resolvedControls, resolvedControlOptions, generatedPillNav, generatedDefaultView, internalControlValues, resolvedComponentBindings, resolvedComponentBindingsById, resolvedComponentProps, resolvedComponentPropsById, handleControlValuesUpdate, } }