import { Global } from '@/global/Global'; import { createApp } from 'vue'; import main_ui_router from '@/components/main_ui_router'; import TTContextMenu from '@/components/context_menu/TTContextMenu'; import PrimeVue from 'primevue/config'; import TTEventBus from '@/services/TTEventBus'; /* * -- Structure of the ContextMenuManager -- * You can search the code for `#VueContextMenu#` which will help identify the key areas during development and refactor. * Dynamic: For the Dynamic creation of the menu, you can search for #VueContextMenu#Dynamic-View and #VueContextMenu#Dynamic-EditView and look at mountContextMenu * * There are 3 elements to the ContextMenuManager * 1) The TT applications view code, 2) the ContextMenuManager, and 3) the Vue context menu. * The Vue component is controlled by events, the events work as follows: * 1) The Vue component is setup to listen to events from the EventBus (mitt) * 2) The control events are fired from the ContextMenuManager. * 3) The view controller calls functions in the ContextMenuManager to trigger these events. * - ContextMenuManagers are created in BaseViewController.buildContextMenu * - ContextMenu Models are built in BaseViewController.buildContextMenuModels() and all other ViewControllers that override those functions. * - The Vue component is created currently by the LegacyView component, and only when a viewId is passed to the LegacyView, currently done by the Vue Router. * The Vue router in turn is triggered NOT by the hash changes, as this currently leads to some race conditions/out of sync with the TT app. * Instead, the Vue router is running in createMemoryHistory mode, and routing manually triggered by VueRouter.push, which is currently done from BaseViewController.loadView * * -- Creating a new Context Menu -- * So step 1 in adding a new context menu, is make sure the Vue Router is triggered, to create a new menu. * The new contextMenu responds to a viewId, so this needs to be unique. (Approach might have to be re-evaluated for places with subviews. * Maybe in the router we pass a contextmenu id which can be the same as the viewId in most cases? Discuss with Mike. * Either way, having the router control that Vue contextmenu generation will control duplicates/clashes. * -- Points of note * - Make sure any new listeners in the Vue component are also destroyed once no longer used (e.g. when component is unMounted). * * Initialized from BaseViewController rather than the Vue component, so that each manager created is controlled from this point. * Every contextmenu Vue component will then listen to a given viewId which is triggered by BaseVC.loadview. * If it was the other way around, there could be more chance for a viewId manager to be overriden in the array. * Still, the organisation between when a Vue contextmenu is created, and a manager from the TT app side, could be improved. * Also, worst case scenario, a manager is created in BaseView, and no vue component to listen to it. * If the vue component was to control when a manager was created, then we would get problems when functions like this.context_menu.parseContextRibbonAction are called from setDefault menu functions within views. So BaseView MUST be in charge of creating the manager. * - An area to improve would be to have our own EventBus, not re-use the one from PrimeVue. */ class ContextMenu { constructor( options ) { // Set validation to reject if minimum data not supplied. this.id = options.id || null; this.type = options.type || null; this.vue_menu_instance = options.vue_menu_instance || null; this.view_controller_instance = options.view_controller_instance || null; this.menu_model = null; } } class ContextMenuManager { constructor() { this._menus = {}; // Will contain ContextMenu's this.event_bus = new TTEventBus({ view_id: 'context_menu' }); } createAndMountMenu( menu_id, parent_mount_container, parent_context ) { if( !menu_id || !parent_mount_container || !parent_context ) { Debug.Error( 'Error: Invalid parameters passed to function.', 'ContextMenuManager.js', 'ContextMenuManager', 'createAndMountMenu', 1 ); return false; } if( this.getMenu( menu_id ) !== undefined ) { Debug.Error( 'Error: Context Menu Manager ('+ menu_id +') already exists and mounted.', 'ContextMenuManager.js', 'ContextMenuManager', 'createAndMountMenu', 1 ); return false; } // TODO: // - Tie in a onDestroy function where the vue context and reference in the array are deleted if the menu is removed from the dom. // #VueContextMenu#Dynamic-View - Create dynamic container for the vue context menu parent_mount_container.prepend(''); // Create and mount unique context menu for this view. let vue_context = this.mountContextMenu( menu_id ); let menu = new ContextMenu({ id: menu_id, vue_menu_instance: vue_context, view_controller_instance: parent_context , }); // Add context menu to the array of active context menu's so we can track them. this._menus[ menu_id ] = menu; return menu.id; } generateMenuId( parent_type, parent_id ) { if( !parent_type || !parent_id ) { Debug.Error( 'Error: Invalid parameters passed to function.', 'ContextMenuManager.js', 'ContextMenuManager', 'generateMenuId', 1 ); return false; } /* -- Examples -- * View: BranchView * EditView: BranchView -> Edit * EditView with tabs: Employee -> Edit * SubView: Example? * SubViewLists: Employee -> Edit -> Qualifications. * */ return `CM-${parent_id}-${parent_type}`; // Maybe need a unique menu on the end? or not? } getMenu( id ) { return this._menus[ id ]; } mountContextMenu( menu_id ) { if( menu_id === undefined ) { Debug.Error( 'Error: Invalid parameters passed to function.', 'ContextMenuManager.js', 'ContextMenuManager', 'mountContextMenu', 1 ); return false; } if( this.getMenu( menu_id ) !== undefined ) { Debug.Error( 'Error: Context Menu Manager ('+ menu_id +') already exists and mounted.', 'ContextMenuManager.js', 'ContextMenuManager', 'mountContextMenu', 1 ); return false; } // Used by #VueContextMenu#Dynamic-View and #VueContextMenu#Dynamic-EditView let mount_component = TTContextMenu; let mount_reference = '#' + menu_id; // TODO: Check if component has not already been mounted or existing in the menu list. let vue_menu_instance = createApp( mount_component, { menu_id: menu_id } ); // Can pass an object in here too for proper JS only code, and allow data in without eventBus. vue_menu_instance.use( PrimeVue, { ripple: true, inputStyle: 'filled' }); // From: AppConfig.vue this.$primevue.config.inputStyle value is filled/outlined as we dont use AppConfig in TT. vue_menu_instance.use( main_ui_router ); // #VueContextMenu# FIXES: Failed to resolve component: router-link when TTOverlayMenuButton is opened. Because each context menu is a separate Vue instance, and they did not globally 'use' the Router, only in main ui. vue_menu_instance.mount( mount_reference ); // e.g. '#tt-edit-view-test' return vue_menu_instance; } unmountContextMenu ( id ) { if( this._menus[ id ] ) { this._menus[ id ].vue_menu_instance.unmount(); delete this._menus[ id ]; Debug.Text( 'Context menu successfully unmounted ('+ id +').', 'ContextMenuManager.js', 'ContextMenuManager', 'unmountContextMenu', 11 ); return true; } else { Debug.Warn( 'Unable to unmount context menu. Menu not found ('+ id +'). Maybe already removed?', 'ContextMenuManager.js', 'ContextMenuManager', 'unmountContextMenu', 11 ); return false; } } buildContextMenuModelFromBackbone( menu_id, final_context_menu_model, view_controller_context ) { if( menu_id === null ) return false; var parsed_bb_menu = this.convertBackBoneMenuModelToPrimeVue( final_context_menu_model, view_controller_context ); this.updateMenuModel( menu_id, parsed_bb_menu ); if ( this.getMenu( menu_id ) ) { //Fixes context menu flashing. For example when a user has lower permission levels context icons will appear and then dissapear half a second later. //NOTE: These branching paths may be removed in the future when setDefaultMenu() and setEditMenu() are consolidated and some changes may be needed to reduce duplicate calls to the set*Menu() functions. if ( !this.getMenu( menu_id ).view_controller_instance.is_edit && !this.getMenu( menu_id ).view_controller_instance.is_add && !this.getMenu( menu_id ).view_controller_instance.is_viewing ) { this.getMenu( menu_id ).view_controller_instance.setDefaultMenu(); } else { //Fixes issue where refreshing an edit view would flash context menus. this.getMenu( menu_id ).view_controller_instance.setEditMenu(); } } else { Debug.Error( 'Error: Cannot find Vue context menu.', 'ContextMenuManager.js', 'ContextMenuManager', 'getMenuModelByMenuId', 1 ); } } convertBackBoneMenuModelToPrimeVue( backbone_menu_format, view_controller_context ) { /* * This would have arrays for groups & icons * icons is very similar to the PrimeVue MenuModel. * * Note: This function only builds the menu when its asked to do so, like on view controller init. * However, if the menu icons are manipulated via permission controls or user selecting items in a data table, this function does not run. * Thats mostly fine, as the menu structure itself does not really change based on user interaction, * But some things might. E.g. (Paystubs ->Edit Employee) when the Save menu initially has all the options during this build run, but later ends up with only one Save option. That needs to be handled dynamically in Vue. * */ var icons = backbone_menu_format.icons; var action_groups = {}; var parsed_icons = []; // TODO: Is the below still needed? And can we do this in a neater/clearer way. if ( icons && Object.keys( icons ).length && Object.keys( icons ).length < 1) { // Invalid data. console.error('ContextMenuManager: Invalid data format. No icons or not an array.'); return false; } // Sort icons by their sort_order attribute. icons = Object.values(backbone_menu_format.icons).sort(this.sortCompareIcons); // parse the icons item.icons further for ( var key in icons ) { // Dev warning: iteration order is not guaranteed during for...in loops. And only modify the currently iterated key. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#deleted_added_or_modified_properties if(icons.hasOwnProperty( key )) { var item = icons[key]; // Validation checks and default values. // When modifying these, make sure to update sub_menu_item checks later on too. if( !item.id ) { Debug.Error( 'ERROR: Invalid data. Missing ID.', 'ContextMenuManager.js', 'ContextMenuManager', 'convertBackBoneMenuModelToPrimeVue', 1 ); } if( item.icon ) delete item.icon; // Remove the .icon attribute, because it interferes with PrimeVue