/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import {FormModel} from '@xh/hoist/cmp/form'; import {AppOptionSpec, HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; import {action, computed, makeObservable, observable} from '@xh/hoist/mobx'; import {resolve} from '@xh/hoist/promise'; import {isOmitted} from '@xh/hoist/utils/impl'; import {assign, mapValues, pickBy} from 'lodash'; import {AppOption} from './AppOption'; /** * Manages built-in setting of options. * @internal */ export class OptionsDialogModel extends HoistModel { override xhImpl = true; @observable isOpen: boolean = false; @observable.ref options: AppOption[] = []; @managed loadTask: TaskObserver = TaskObserver.trackLast(); @managed formModel: FormModel = null; constructor() { super(); makeObservable(this); } init() { this.addReaction({ track: () => XH.routerState, run: () => this.hide() }); } //------------------- // Setting options //------------------- setOptions(options: AppOptionSpec[]) { this.options = options.filter(o => !isOmitted(o)).map(o => new AppOption(o)); const fields = this.options.map(o => assign({name: o.name}, o.fieldModel)); this.formModel = new FormModel({fields, xhImpl: true}); } get hasOptions(): boolean { return !!this.options.length; } @computed get reloadRequired(): boolean { const {formModel} = this; return ( formModel && this.options.some(o => formModel.fields[o.name].isDirty && o.reloadRequired) ); } @computed get refreshRequired(): boolean { const {formModel} = this; return ( formModel && this.options.some(o => formModel.fields[o.name].isDirty && o.refreshRequired) ); } //------------------- // Dialog //------------------- @action show() { this.isOpen = true; this.initAsync(); } @action hide() { this.isOpen = false; } @action toggleVisibility() { if (this.isOpen) { this.hide(); } else { this.show(); } } async initAsync() { const {options, formModel, loadTask} = this, promises = options.map(option => { return option.getValueAsync().then(v => formModel.fields[option.name].init(v)); }); await Promise.allSettled(promises).linkTo(loadTask); } async saveAsync(): Promise { const {formModel, reloadRequired, refreshRequired, loadTask} = this; await formModel.validateAsync(); if (!formModel.isValid) return; if (reloadRequired) { loadTask.setMessage('Reloading app to apply changes...'); } await resolve() .then(() => this.doSaveAsync()) .wait(1000) .then(() => { this.hide(); if (reloadRequired) { XH.reloadApp(); } else if (refreshRequired) { XH.refreshAppAsync(); } }) .linkTo(loadTask) .catchDefault(); } async doSaveAsync(): Promise { const {formModel} = this, dirtyFields = pickBy(formModel.fields, {isDirty: true}), promises = this.options .filter(o => dirtyFields[o.name]) .map(o => o.setValueAsync(dirtyFields[o.name].value)); await Promise.allSettled(promises); await XH.prefService.pushPendingAsync(); XH.track({ message: 'Changed options', category: 'App', data: mapValues(dirtyFields, f => ({value: f.value, oldValue: f.initialValue})) }); } }