/* * 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 {fragment, p, strong} from '@xh/hoist/cmp/layout'; import { composeGroupPath, getGroupLeaf, getGroupParent, isGroupSameOrDescendant, normalizeGroupPath, VIEW_GROUP_DELIMITER } from '@xh/hoist/cmp/viewmanager'; import {HoistModel, managed, XH} from '@xh/hoist/core'; import {required, StoreRecord} from '@xh/hoist/data'; import {bindable, makeObservable} from '@xh/hoist/mobx'; import {pluralize} from '@xh/hoist/utils/js'; import {ManageDialogModel} from '../ManageDialogModel'; import {confirmGroupMergeIfExistsAsync} from '../Utils'; /** * Backing model for the RenameGroupDialog - renames the selected group in place, cascading to all * views within it. */ export class RenameGroupDialogModel extends HoistModel { parent: ManageDialogModel; @managed formModel: FormModel; /** True to display the dialog. */ @bindable isRenameDialogOpen = false; get groupRecord(): StoreRecord { return this.parent.selectedGroupRecord; } /** Path of the group under edit - null for the shared tab's synthetic owner rows. */ get group(): string { return this.groupRecord?.data.group ?? null; } get isGlobal(): boolean { return this.parent.gridType === 'global'; } constructor(parent: ManageDialogModel) { super(); makeObservable(this); this.parent = parent; this.formModel = this.createFormModel(); this.addReaction({ track: () => [this.groupRecord], run: () => { const {formModel, group} = this; formModel.init({ name: getGroupLeaf(group) }); }, fireImmediately: true }); } reset() { this.formModel.reset(); } /** @returns true if changes were applied (or none were pending), false if blocked. */ async saveAsync(): Promise { const {parent, group, isGlobal, formModel} = this; if (!formModel.isDirty) return true; if (!(await formModel.validateAsync())) return false; const {name} = formModel.getData(), to = normalizeGroupPath(composeGroupPath(getGroupParent(group), name)), renamePending = to !== group; // Run all confirms up front, before applying any changes. if (renamePending && !(await this.confirmGlobalRenameAsync())) return false; if ( renamePending && !(await confirmGroupMergeIfExistsAsync(parent.viewManagerModel, group, to, isGlobal)) ) { return false; } if (renamePending) await parent.renameGroupAsync(group, to, isGlobal); return true; } //------------------------ // Implementation //------------------------ /** Confirm renaming a global group - it re-groups views within every user's menu. */ private async confirmGlobalRenameAsync(): Promise { const {isGlobal, group, parent} = this; if (!isGlobal) return true; const {viewManagerModel} = parent, {globalDisplayName, typeDisplayName} = viewManagerModel, globalViews = viewManagerModel.globalViews.filter( v => v.isGlobal && isGroupSameOrDescendant(v.group, group) ); if (!globalViews.length) return true; const countStr = pluralize( `${globalDisplayName} ${typeDisplayName}`, globalViews.length, true ); return XH.confirm({ message: fragment( p( `This will rename the group across ${countStr} for all other ${XH.appName} users.` ), p(strong('Are you sure you want to proceed?')) ), confirmProps: { text: 'Yes, rename', outlined: true, autoFocus: false, intent: 'primary' } }); } private createFormModel(): FormModel { return new FormModel({ fields: [ { name: 'name', displayName: 'Group Name', rules: [ required, ({value}) => value?.includes(VIEW_GROUP_DELIMITER) ? `Group names can't contain "${VIEW_GROUP_DELIMITER}".` : null ] } ] }); } }