wb/mgr/Manager.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Logan Byam
 */


/**
 * @module nmodule/webEditors/rc/wb/mgr/Manager
 */
define([
  'jquery',
  'Promise',
  'underscore',
  'bajaux/Widget',
  'bajaux/commands/CommandGroup',
  'bajaux/util/CommandButtonGroup',
  'nmodule/webEditors/rc/fe/fe',
  'nmodule/webEditors/rc/fe/feDialogs',
  'nmodule/webEditors/rc/fe/baja/BaseEditor',
  'nmodule/webEditors/rc/fe/baja/util/typeUtils',
  'nmodule/webEditors/rc/util/ListSelection',
  'nmodule/webEditors/rc/wb/commands/AddSlotCommand',
  'nmodule/webEditors/rc/wb/mgr/commands/AllDescendantsCommand',
  'nmodule/webEditors/rc/wb/mgr/commands/EditCommand',
  'nmodule/webEditors/rc/wb/mgr/commands/MgrCommand',
  'nmodule/webEditors/rc/wb/mgr/commands/NewCommand',
  'nmodule/webEditors/rc/wb/mgr/commands/NewFolderCommand',
  'nmodule/webEditors/rc/wb/menu/menuUtils',
  'nmodule/webEditors/rc/wb/menu/Separator',
  'nmodule/webEditors/rc/wb/mgr/MgrStateHandler',
  'nmodule/webEditors/rc/wb/mgr/mgrUtils',
  'nmodule/webEditors/rc/wb/mgr/model/MgrColumn',
  'nmodule/webEditors/rc/wb/mgr/model/MgrModel',
  'nmodule/webEditors/rc/wb/mixin/ContextMenuSupport',
  'nmodule/webEditors/rc/wb/mixin/mixinUtils',
  'nmodule/webEditors/rc/wb/mixin/TransferSupport',
  'nmodule/webEditors/rc/wb/table/Table',
  'nmodule/webEditors/rc/wb/table/menu/DefaultTableContextMenu',
  'nmodule/webEditors/rc/wb/table/model/TableModel',
  'nmodule/webEditors/rc/util/htmlUtils' ], function (
  $,
  Promise,
  _,
  Widget,
  CommandGroup,
  CommandButtonGroup,
  fe,
  feDialogs,
  BaseEditor,
  typeUtils,
  ListSelection,
  AddSlotCommand,
  AllDescendantsCommand,
  EditCommand,
  MgrCommand,
  NewCommand,
  NewFolderCommand,
  menuUtils,
  Separator,
  MgrStateHandler,
  mgrUtils,
  MgrColumn,
  MgrModel,
  ContextMenuSupport,
  mixinUtils,
  TransferSupport,
  Table,
  DefaultTableContextMenu,
  TableModel,
  htmlUtils) {

  'use strict';

  const { isComponent } = typeUtils;
  const { contextMenuOnLongPress } = htmlUtils;
  const { hasMixin } = mixinUtils;
  const { getMainTableSelectedSubjects, getLearnTableSelectedSubjects, isShownInActionBar } = mgrUtils;
  const { forSubject } = menuUtils;
  const IS_ADDED_FROM_MAKE_COMMANDS = Symbol('isAddedFromMakeCommands');
  const { ALL, MAIN_CONTEXT_MENU } = MgrCommand.flags;

  /**
   * Return the default HTML used for the manager view. If the learn mixin
   * is applied, this will be overridden by a template that describes the
   * multi table split pane markup.
   *
   * @returns {String}
   */
  function defaultHtml() {
    return '<div class="mainTable">' +
      '<div class="tableContainer"></div>' +
      '</div>' +
      '<div class="showHideMenu mgr-show-hide-menu mgr-show-hide-menu-main"></div>' +
      '<div class="commandContainer mgr-action-bar"></div>';
  }

  const widgetDefaults = () => ({
    properties: {
      rootCssClass: 'Manager'
    }
  });

  /**
   * API Status: **Development**
   *
   * View for managing groups of components, monitoring their current state
   * and adding/removing components to the group. The concrete manager type
   * must provide the `moduleName` and `keyName` parameters if it requires
   * state to be saved between hyperlinks and page reloads, as these values
   * will be used when generating the key used to index the cached state data.
   *
   * Due to the incubating status of the manager framework, it is *not
   * recommended* that you extend `Manager` directly. Instead, extend
   * `DeviceMgr` or `PointMgr`: these will provide more robust functionality
   * for most use cases.
   *
   * When implementing a `Manager` constructor, please take note of the `makeCommands()` function.
   * Any commands created by `makeCommands()` will be added the `Manager`'s `CommandGroup` _after_
   * the constructor runs, so commands added directly in the constructor will appear _first_ in the
   * list, not last. If the ordering of `Command`s matters, add them by overriding `makeCommands()`.
   *
   * @param {Object} params
   * @param {String} params.moduleName - The module name, used for accessing values from the lexicon
   * and also used to generate the key for saving state information for a manager type.
   * @param {String} params.keyName - The key name, used for accessing values from the lexicon and
   * also used to generate the key for saving state information for a manager type.
   *
   * @see module:nmodule/driver/rc/wb/mgr/DeviceMgr
   * @see module:nmodule/driver/rc/wb/mgr/PointMgr
   *
   * @class
   * @abstract
   * @alias module:nmodule/webEditors/rc/wb/mgr/Manager
   * @extends module:nmodule/webEditors/rc/fe/baja/BaseEditor
   */
  const Manager = function Manager(params = {}) {
    BaseEditor.call(this, { params, defaults: widgetDefaults() });
    const { moduleName, keyName } = params;
    this.$getStateHandler = _.once(() => {
      return Promise.resolve(moduleName && keyName && this.makeStateHandler(moduleName, keyName));
    });
    this.$isAllDescendantsSelected = false;
    TransferSupport(this);
    ContextMenuSupport(this);
  };

  Manager.prototype = Object.create(BaseEditor.prototype);
  Manager.prototype.constructor = Manager;

  /**
   * @private
   * @returns {module:nmodule/webEditors/rc/wb/mgr/commands/NewCommand}
   */
  Manager.prototype.$getNewCommand = function () {
    return this.getCommandGroup().findCommand(NewCommand);
  };

  /**
   * @private
   * @returns {module:nmodule/webEditors/rc/wb/mgr/commands/EditCommand}
   */
  Manager.prototype.$getEditCommand = function () {
    return this.getCommandGroup().findCommand(EditCommand);
  };

  /**
   * @param {JQuery} dom
   * @returns {Array|null} the selected subject of the table being clicked
   */
  Manager.prototype.getSubject = function (dom) {
    const tableDom = dom.closest('.TableWidget');

    if (!$.contains(this.jq()[0], tableDom[0])) { return null; }

    const table = Widget.in(tableDom);

    if (!table) { return null; }

    return table.getSubject(dom);
  };

  /**
   * Get the main Table widget.
   *
   * @returns {module:nmodule/webEditors/rc/wb/table/Table}
   * @since Niagara 4.6
   */
  Manager.prototype.getMainTable = function () {
    // noinspection JSValidateTypes
    return Widget.in(this.$getMainTableElement());
  };

  /**
   * Get the element that holds the main table.
   * @private
   * @returns {JQuery}
   */
  Manager.prototype.$getMainTableElement = function () {
    return this.jq().find('.mainTable');
  };

  /**
   * Get the element that holds the command buttons in the action bar.
   *
   * @private
   * @returns {JQuery}
   */
  Manager.prototype.$getActionBarElement = function () {
    return this.jq().find('.commandContainer.mgr-action-bar');
  };

  /**
   * Get the action bar CommandButtonGroup widget.
   *
   * @private
   * @returns {module:bajaux/util/CommandButtonGroup}
   */
  Manager.prototype.$getActionBarCommandButtonGroup = function () {
    // noinspection JSValidateTypes
    return Widget.in(this.$getActionBarElement());
  };

  /**
   * Set up elements for the main table and command group.
   *
   * @param {JQuery} dom
   * @param {Object} [params] the initialization parameters
   * @returns {Promise}
   */
  Manager.prototype.doInitialize = function (dom, params) {

    // Look for a (private) optional parameter allowing custom HTML
    // markup to be specified for the manager. If not defined, the
    // default HTML will be used, with a single main manager table,
    // show/hide menu and the command button group.

    const html = (params && params.html) || defaultHtml();

    dom.html(html).addClass('Manager');
    contextMenuOnLongPress(dom, '.tableContainer');
    return this.$getStateHandler()
      .then((handler) => (this.$stateHandler = handler));
  };

  /**
   * @returns {string} `tr` - by default, the elements that are right-clickable in managers are the
   * rows in the main and database tables.
   * @since Niagara 4.14
   */
  Manager.prototype.getContextMenuSelector = function () {
    return 'tr';
  };

  /**
   * When the main database table is right-clicked, the default profile context menu (Views,
   * Actions, etc.) will be shown, with additional Manager-specific Commands appended to it. This
   * function defines exactly what Commands will be appended.
   *
   * By default, these will be any commands with the `MAIN_CONTEXT_MENU` flag (i.e. the New and
   * Edit commands), but _not_ including any commands with flags that have not been configured
   * (i.e. those commands with flags still set to the default value of `ALL`). You must explicitly
   * set the command flags to a value that includes `MAIN_CONTEXT_MENU` to make it appear here.
   *
   * @returns {Promise.<Array.<module:bajaux/commands/Command|bajaux/commands/CommandGroup>>}
   * @since Niagara 4.14
   * @see module:nmodule/webEditors/rc/wb/mgr/commands/MgrCommand~flags
   */
  Manager.prototype.getMainTableCommands = function () {
    return Promise.resolve(this.getCommandGroup().filter({
      include: (cmd) => {
        const flags = cmd.getFlags();
        return flags !== ALL && (flags & MAIN_CONTEXT_MENU);
      }
    }).getChildren());
  };

  /**
   * @private
   * @param {JQuery.TriggeredEvent} e
   * @returns {Promise.<module:bajaux/commands/CommandGroup>}
   * @since Niagara 4.14
   */
  Manager.prototype.toContextMenuCommandGroup = function (e) {
    return Promise.all([
      this.$resolveProfileContextMenuCommands(e)
        .catch(() => null),
      this.getMainTableCommands()
    ])
      .then(([ profileCommands, mainTableCommands ]) => {
        let commands = (profileCommands && profileCommands.getChildren()) || [];

        if (mainTableCommands && mainTableCommands.length) {
          if (commands.length) {
            commands.push(new Separator());
          }
          commands = commands.concat(mainTableCommands);
        }

        return new CommandGroup({ commands });
      });
  };

  /**
   * @private
   * @param {JQuery.TriggeredEvent} e
   * @returns {Promise.<module:bajaux/commands/CommandGroup|null>}
   */
  Manager.prototype.$resolveProfileContextMenuCommands = function (e) {
    return Promise.resolve(this.getSubject($(e.target)))
      .then((subject) => subject && forSubject(this, subject));
  };

  /**
   * Private framework override point called when the main model has been created, but before
   * it has been loaded into the main table. The deserialized manager state is passed as a
   * parameter, allowing any model configuration to be restored from the manager state.
   *
   * This is intended for internal framework use only.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} model - the main table model
   * @param {Object} deserializedState
   * @returns {Promise.<*>}
   */
  Manager.prototype.$beforeMainTableLoaded = function (model, deserializedState) {
    return Promise.resolve();
  };

  /**
   * Private framework override point called after the main table model has been loaded
   * into the table widget. The deserialized manager state is passed as a parameter; this
   * is called before the main state restoration is performed.
   *
   * This is intended for internal framework use only.
   *
   * @private
   * @param {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel} model - the main table model
   * @param {Object} deserializedState
   * @returns {Promise.<*>}
   */
  Manager.prototype.$afterMainTableLoaded = function (model, deserializedState) {
    return Promise.resolve();
  };

  /**
   * Get the `MgrModel` backing this manager. This will return `undefined` until
   * `load()` is called. It can safely be called from inside `doLoad()`.
   *
   * @returns {module:nmodule/webEditors/rc/wb/mgr/model/MgrModel}
   * @since Niagara 4.6
   */
  Manager.prototype.getModel = function () {
    return this.$mgrModel;
  };

  /**
   * Construct an array of the commands used directly by this manager. These commands will be added
   * to the Manager's own command group. The Manager will make them available based on the flags
   * with which they are configured.
   *
   * - `MgrCommand.flags.MENU_BAR` : this command will appear in Workbench's menu bar when this
   *   manager is viewed in Workbench (this is default behavior for all `bajaux` views).
   * - `MgrCommand.flags.ACTION_BAR`: this command will appear in the Manager's action bar (the row
   *   of command buttons at the bottom of the Manager).
   * - `MgrCommand.flags.TOOL_BAR`: this command will appear in the toolbar (the command buttons in
   *   the profile itself at the top of the view, next to the view selector).
   * - `MgrCommand.flags.MAIN_CONTEXT_MENU`: this command will appear when right-clicking objects in
   *   main database table.
   * - `MgrCommand.flags.LEARN_CONTEXT_MENU`: this command will appear when right-clicking objects
   *   in the discovery table.
   *
   * @returns {Promise.<Array.<module:bajaux/commands/Command>>|Array.<module:bajaux/commands/Command>}
   * @since Niagara 4.14
   *
   * @see module:nmodule/webEditors/rc/wb/mgr/commands/MgrCommand.flags
   *
   * @example
   * makeCommands() {
   *   return Promise.resolve(super.makeCommands())
   *     .then((commands) => [
   *       ...commands,
   *
   *       // this Command will be accessible only in the action bar and toolbar.
   *       new Command({
   *         flags: MgrCommand.flags.ACTION_BAR | MgrCommand.flags.TOOL_BAR
   *       })
   *     ]);
   * };
   */
  Manager.prototype.makeCommands = function () {
    let commands = [];

    if (hasMixin(this, 'MGR_FOLDER')) {
      commands.push(new NewFolderCommand(this), new AllDescendantsCommand(this));
    }

    commands.push(new NewCommand(this), new EditCommand(this));

    if (hasMixin(this, 'MGR_LEARN')) {
      return Promise.resolve(this.makeDiscoveryCommands())
        .then((discoveryCommands) => [ ...commands, ...discoveryCommands ]);
    }

    return Promise.resolve(commands);
  };

  /**
   * Abstract method to create the `MgrModel` for the main database table. The method
   * should return a `Promise` that will resolve to a `MgrModel` instance for the
   * `ComponentSource` provided in the parameter. This is used to convert the value
   * being loaded into the widget (e.g. a network) into the model used for the table
   * widget.
   *
   * @abstract
   *
   * @param {baja.Component} value - the value being loaded into the `Widget`.
   * @returns {Promise.<module:nmodule/webEditors/rc/wb/mgr/model/MgrModel>} the model for the main table.
   */
  Manager.prototype.makeModel = function (value) {
    throw new Error('Manager does not provide a makeModel() function.');
  };

  Manager.prototype.load = function (comp) {
    const args = arguments;
    if (comp === this.value()) { return Promise.resolve(); }

    // Read and deserialize the state from storage, but we don't do the full restore
    // until the end of loading. We get the state at this point so we can find out
    // any state that might need to be restored on the model prior to loading. The
    // full manager state is restored *after* the model has been loaded into the
    // table widget.

    this.$deserializedState = (this.$stateHandler && this.$stateHandler.deserializeFromStorage());

    return Promise.resolve(this.makeModel(comp))
      .then((model) => {
        this.$mgrModel = model;
        return BaseEditor.prototype.load.apply(this, args);
      })
      .finally(() => {
        delete this.$deserializedState;
      });
  };

  /**
   * Initializes and loads the main table with the `MgrModel`. If overriding,
   * be sure to call the super method.
   *
   * @returns {Promise}
   */
  Manager.prototype.doLoad = function () {
    const jq = this.jq();
    const model = this.getModel();
    const tableContainer = jq.find('.mainTable');

    _.each(model.getColumns(), (col) => {
      if (col instanceof MgrColumn) {
        col.$init(this);
      }
    });
    // Create functions to be passed to the main table that will give us a hook
    // into the cell construction/destruction process, should there be need to
    // override any of the main table's default behavior, such as manipulating
    // the css for a particular view.

    return this.$beforeMainTableLoaded(model, this.$deserializedState)
      .then(() => {
        let $constructorParams;
        const { $resolveHandlers } = TableModel;
        if ($resolveHandlers) {
          $constructorParams = { selection: new ListSelection({ $resolveHandlers }) };
        }
        return fe.buildFor({
          dom: tableContainer,
          type: Table,
          value: model,
          $constructorParams,
          initializeParams: {
            buildCell: this.buildMainTableCell.bind(this),
            destroyCell: this.destroyMainTableCell.bind(this),
            finishBuildingRow: this.finishMainTableRow.bind(this)
          },
          hooks: {
            instantiated: this.$tableInstantiated
          },
          properties: { enableStriping: false }
        });
      })
      .then((table) => {
        new DefaultTableContextMenu(table).arm(jq, '.mgr-show-hide-menu-main');
        return this.$afterMainTableLoaded(model, this.$deserializedState);
      })
      .then(() => this.restoreState())
      .then(() => this.$registerOnMainTableEvents())
      .then(() => this.$makeAndModifyCommands())
      .then((commands) => {
        if (commands) {
          //In multi load cases, remove all commands and add.
          this.getCommandGroup().add(...commands);
          return fe.buildFor({
            dom: this.$getActionBarElement(),
            type: CommandButtonGroup,
            value: this.$getCommandGroupForActionBar()
          });
        }
      });
  };

  /**
   * @since Niagara 4.14
   * @returns {Boolean}
   */
  Manager.prototype.isAllDescendantsSelected = function () {
    return this.$isAllDescendantsSelected;
  };

  /**
   * @since Niagara 4.14
   * @param {Boolean} allDescendantsSelected
   */
  Manager.prototype.setAllDescendantsSelected = function (allDescendantsSelected) {
    this.$isAllDescendantsSelected = allDescendantsSelected;
  };

  /**
   * @private
   * @returns {Promise.<Array.<module:bajaux/commands/Command>>}
   */
  Manager.prototype.$makeAndModifyCommands = function () {
    return Promise.resolve(this.makeCommands())
      .then((commands) => {
        const commandGroup = this.getCommandGroup();
        //Remove commands that were added earlier from makeCommands workflow
        commandGroup.getChildren().forEach((cmd) => {
          if (cmd[IS_ADDED_FROM_MAKE_COMMANDS]) {
            commandGroup.remove(cmd);
          }
        });
        //Mark commands to specify that they were added from the 'makeCommands' workflow
        commands.forEach((cmd) => (cmd[IS_ADDED_FROM_MAKE_COMMANDS] = true));
        return commands;
      });
  };

  /**
   * @since Niagara 4.14
   * @private
   * @returns {module:bajaux/commands/CommandGroup}
   */
  Manager.prototype.$getCommandGroupForActionBar = function () {
    return this.getCommandGroup().filter({ include: isShownInActionBar });
  };

  /**
   * If the loaded `MgrModel` is backed by a mounted Component, then use that
   * Component to resolve ORDs.
   *
   * @returns {Promise.<baja.Component|undefined>}
   */
  Manager.prototype.getOrdBase = function () {
    const component = this.value();
    return Promise.resolve(isComponent(component) ? component : undefined);
  };

  /**
   * Overrides the base `destroy` method to give the manager a chance to save its state
   * before the content (such as the child table widgets) is destroyed.
   * @returns {Promise}
   */
  Manager.prototype.destroy = function () {
    this.saveState();
    return BaseEditor.prototype.destroy.apply(this, arguments);
  };

  /**
   * Destroy child editors, the main table, and its model.
   *
   * @returns {Promise}
   */
  Manager.prototype.doDestroy = function () {
    const model = this.getModel();

    this.$removeMainTableListeners();

    return this.getChildWidgets().destroyAll()
      .then(() => {
        return (model instanceof MgrModel) && model.destroy();
      });
  };

  /**
   * Invoke the handler created by `makeStateHandler()` to restore the Manager's
   * current state when the Manager is loaded.
   *
   * @returns {Promise}
   */
  Manager.prototype.restoreState = function () {
    const handler = this.$stateHandler;
    const state = this.$deserializedState;

    return Promise.resolve(handler && state && this.getModel() && handler.restore(this, state));
  };

  /**
   * Invoke the handler created by `makeStateHandler()` to save the Manager's
   * current state when the Manager is destroyed.
   */
  Manager.prototype.saveState = function () {
    const handler = this.$stateHandler;

    if (handler && this.getModel()) { handler.save(this); }
  };

  /**
   * Make a state handler instance for saving and restoring the Manager's
   * state.
   * @returns {Promise.<module:nmodule/webEditors/rc/wb/mgr/MgrStateHandler>}
   */
  Manager.prototype.makeStateHandler = function (keyName, moduleName) {
    return MgrStateHandler.make(keyName + '.' + moduleName);
  };

  /**
   * Override point, allowing a `Manager` to customize the building of
   * a cell within its main table. This will default to delegating the
   * cell building to the column.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Column} column
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise|*}
   */
  Manager.prototype.buildMainTableCell = function (column, row, dom) {
    return column.buildCell(row, dom);
  };

  /**
   * Override point, allowing a `Manager` to customize the destruction
   * of a cell that it created in `#buildMainTableCell`. The default
   * behavior is to delegate to the column.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Column} column
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise|*}
   */
  Manager.prototype.destroyMainTableCell = function (column, row, dom) {
    return column.destroyCell(row, dom);
  };

  /**
   * Override point, allowing a `Manager` to customize the dom for
   * a `Row` after the cells have been built, but before it is inserted
   * into the main table. This allows sub-classes to perform any CSS
   * customizations they may require at an individual row level.
   *
   * @param {module:nmodule/webEditors/rc/wb/table/model/Row} row
   * @param {jQuery} dom
   * @returns {Promise}
   */
  Manager.prototype.finishMainTableRow = function (row, dom) {
    return Promise.resolve(dom);
  };

  /**
   * Registers listeners on to the main table events
   * @since Niagara 4.14
   * @private
   */
  Manager.prototype.$registerOnMainTableEvents = function () {
    const mainTable = this.getMainTable();

    const mainSelection = mainTable.$getSelection();
    this.$mainSelectionChangedCallBack = () => this.$tableSelectionChanged().catch(feDialogs.error);

    this.$mainDblClickedCallBack = (event) => this.$mainTableDblClicked(event);

    mainSelection.on('changed', this.$mainSelectionChangedCallBack);
    this.jq().on('dblclick', '.mainTable tr', this.$mainDblClickedCallBack);
  };

  /**
   * Removes the listeners that were added to the main table
   * @since Niagara 4.14
   * @private
   */
  Manager.prototype.$removeMainTableListeners = function () {
    const mainTable = this.getMainTable();
    const that = this;
    //There are some test that create the manager, but do not load/create the main table
    if (mainTable) {
      const mainSelection = mainTable.$getSelection();
      mainSelection.removeListener('changed', that.$mainSelectionChangedCallBack);
      that.jq().off('dblclick', '.mainTable tr', that.$mainDblClickedCallBack);
    }
  };

  /**
   * Gathers the necessary information needed to be passed to the table selection changed
   *  event handler, mainly the current selected subjects in each table.
   * @since Niagara 4.14
   * @private
   * @returns {Promise}
   */
  Manager.prototype.$tableSelectionChanged = function () {
    let mainTableSelectedSubjects;
    let learnTableSelectedSubjects;

    if (this.onTableSelectionChanged) {
      if (this.getMainTable()) {
        mainTableSelectedSubjects = getMainTableSelectedSubjects(this);
      }

      if (this.getLearnTable && this.getLearnTable()) {
        learnTableSelectedSubjects = getLearnTableSelectedSubjects(this);
      }

      const selectedSubjects = {
        mainTableSelection: mainTableSelectedSubjects,
        learnTableSelection: learnTableSelectedSubjects
      };

      return Promise.resolve(this.onTableSelectionChanged(selectedSubjects));
    }

    return Promise.resolve();
  };

  /**
   * Gathers the necessary information needed to be passed to the double click event handler, mainly
   * the rows that were double-clicked.
   * @since Niagara 4.14
   * @private
   * @param {JQuery.Event} event the double click event
   * @returns {Boolean} returns false always to stop the event at this point
   */
  Manager.prototype.$mainTableDblClicked = function (event) {
    const subjects = this.getSubject($(event.target));
    Promise.resolve(this.onMainTableDblClicked(event, subjects)).catch(feDialogs.error);
    return false; // stop the event at this point
  };

  /**
   * The call back procedure that is overridden to control what happens when rows are double-clicked.
   *  Typically used to invoke a command on the selected rows.
   * @since Niagara 4.14
   * @param {JQuery.Event} event the double click event
   * @param {Array.<*>} subjects the selected subject of the table being clicked
   * @returns {Promise|*}
   */
  Manager.prototype.onMainTableDblClicked = function (event, subjects) {
    return Promise.resolve();
  };

  /**
   * The call back procedure that can be overridden to control what happens when a selection is made
   *  in one of the tables in a manager.
   * @since Niagara 4.14
   * @param {Object} selectedSubjects an object that holds the array of the selected subjects from the tables
   *  supported by the manager view.
   * @param {Array.<*>} selectedSubjects.mainTableSelection the current selected subjects
   *  in the main table.
   * @param {Array.<*>|undefined} selectedSubjects.learnTableSelection the current selected subjects
   *  in the learn or discovery table.  This will be undefined if the Manager does not support a
   *  learn table or the getLearnTable() function returns undefined.
   * @returns {Promise|*}
   */
  Manager.prototype.onTableSelectionChanged = function (selectedSubjects) {
    return Promise.resolve(this.$defaultTableSelectionChanged(selectedSubjects));
  };

  /**
   * The default behaviour that is to be executed when the table selection changes.
   *
   * @private
   * @since Niagara 4.14
   * @param {Object} selectedSubjects an object that holds the array of the selected subjects from the tables
   *  supported by the manager view.
   * @param {Array.<*>|undefined} selectedSubjects.mainTableSelection the current selected subjects
   *  in the main table.  This will be undefined if the getTable() function returns undefined.
   * @param {Array.<*>|undefined} selectedSubjects.learnTableSelection the current selected subjects
   *  in the learn or discovery table.  This will be undefined if the Manager does not support a
   *  learn table or the getLearnTable() function returns undefined.
   * @return {Promise|*}
   */
  Manager.prototype.$defaultTableSelectionChanged = function (selectedSubjects) {
    return Promise.resolve();
  };

  /**
   * Allows inserting nav nodes directly into this Manager's container.
   * Typical scenario is when a user is dragging a component from the Palette into 
   * the Manager's database table.
   * 
   * @private
   * @param {Array.<String>} names 
   * @param {Array.<baja.NavNode>} navNodes 
   * @returns {Promise}
   */
  Manager.prototype.$doInsert = function (names, navNodes) {
    const addSlotCommand = new AddSlotCommand(this.value(), { undoable: false });
    const params = {
      bulkValues: navNodes,
      names: names
    };

    return addSlotCommand.invoke(params);
  };

  return Manager;
});