Source: argos-sdk/src/_ListBase.js

/* Copyright (c) 2010, Sage Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import declare from 'dojo/_base/declare';
import lang from 'dojo/_base/lang';
import connect from 'dojo/_base/connect';
import string from 'dojo/string';
import Utility from './Utility';
import ErrorManager from './ErrorManager';
import View from './View';
import SearchWidget from './SearchWidget';
import ConfigurableSelectionModel from './ConfigurableSelectionModel';
import _PullToRefreshMixin from './_PullToRefreshMixin';
import getResource from './I18n';
import convert from 'argos/Convert';


const resource = getResource('listBase');

/**
 * @classdesc A List View is a view used to display a collection of entries in an easy to skim list. The List View also has a
 * selection model built in for selecting rows from the list and may be used in a number of different manners.
 * @class argos._ListBase
 * @extends argos.View
 * @mixins argos._PullToRefreshMixin
 */
const __class = declare('argos._ListBase', [View, _PullToRefreshMixin], /** @lends argos._ListBase# */{
  /**
   * @property {Object}
   * Creates a setter map to html nodes, namely:
   *
   * * listContent => contentNode's innerHTML
   * * remainingContent => remainingContentNode's innerHTML
   */
  attributeMap: {
    listContent: {
      node: 'contentNode',
      type: 'innerHTML',
    },
    remainingContent: {
      node: 'remainingContentNode',
      type: 'innerHTML',
    },
  },
  /**
   * @property {Object}
   *
   *  Maps to Utility Class
   */
  utility: Utility,
  /**
   * @property {Simplate}
   * The template used to render the view's main DOM element when the view is initialized.
   * This template includes emptySelectionTemplate, moreTemplate and listActionTemplate.
   *
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      id                   main container div id
   *      title                main container div title attr
   *      cls                  additional class string added to the main container div
   *      resourceKind         set to data-resource-kind
   *
   */
  widgetTemplate: new Simplate([`
    <div id="{%= $.id %}" title="{%= $.titleText %}" class="list {%= $.cls %}" {% if ($.resourceKind) { %}data-resource-kind="{%= $.resourceKind %}"{% } %}>
      <div class="page-container scrollable{% if ($$.isNavigationDisabled()) { %} is-multiselect is-selectable is-toolbar-open {% } %} {% if (!$$.isCardView) { %} listview {% } %}"
        {% if ($$.isNavigationDisabled()) { %}
        data-selectable="multiple"
        {% } else { %}
        data-selectable="false"
        {% } %}
        data-dojo-attach-point="scrollerNode">
          <div class="toolbar has-title-button" role="toolbar" aria-label="List Toolbar">
            <div class="title">
              <h1></h1>
            </div>
            <div class="buttonset" data-dojo-attach-point="toolNode">
              {% if($.enableSearch) { %}
              <div data-dojo-attach-point="searchNode"></div>
              {% } %}
              {% if($.hasSettings) { %}
              <button class="btn" type="button" data-action="openSettings" aria-controls="list_toolbar_setting_{%= $.id %}">
                <svg class="icon" role="presentation"><use xlink:href="#icon-settings"></use></svg>
                <span class="audible">List Settings</span>
              </button>
              {% } %}
            </div>
          </div>
          {% if ($$.isNavigationDisabled()) { %}
          <div class="contextual-toolbar toolbar is-hidden">
            <div class="buttonset">
              <button class="btn-tertiary" title="Assign Selected Items" type="button">Assign</button>
              <button class="btn-tertiary" id="remove" title="Remove Selected Items" type="button">Remove</button>
            </div>
          </div>
          {% } %}
        {%! $.emptySelectionTemplate %}
        {% if ($$.isCardView) { %}
          <div role="presentation" data-dojo-attach-point="contentNode"></div>
        {% } else { %}
          <ul class="list-content" role="presentation" data-dojo-attach-point="contentNode"></ul>
        {% } %}
        {%! $.moreTemplate %}
      </div>
    </div>
    `,
  ]),
  /**
   * @property {Simplate}
   * The template used to render the loading message when the view is requesting more data.
   *
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      loadingText         The text to display while loading.
   */
  loadingTemplate: new Simplate([
    '<div class="busy-indicator-container" aria-live="polite">',
    '<div class="busy-indicator active">',
    '<div class="bar one"></div>',
    '<div class="bar two"></div>',
    '<div class="bar three"></div>',
    '<div class="bar four"></div>',
    '<div class="bar five"></div>',
    '</div>',
    '<span>{%: $.loadingText %}</span>',
    '</div>',
  ]),
  /**
   * @property {Simplate}
   * The template used to render the pager at the bottom of the view.  This template is not directly rendered, but is
   * included in {@link #viewTemplate}.
   *
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      moreText            The text to display on the more button.
   *
   * The default template exposes the following actions:
   *
   * * more
   */
  moreTemplate: new Simplate([
    '<div class="list-more" data-dojo-attach-point="moreNode">',
    '<p class="list-remaining"><span data-dojo-attach-point="remainingContentNode"></span></p>',
    '<button class="btn" data-action="more">',
    '<span>{%= $.moreText %}</span>',
    '</button>',
    '</div>',
  ]),
  /**
   * @property {Boolean}
   * Indicates whether a template is a card view or a list
   */
  isCardView: true,

  /**
   * @property {Boolean}
   * Indicates if there is a list settings modal.
   */
  hasSettings: false,
  /**
   * listbase calculated property based on actions available
   */
  visibleActions: [],
  /**
   * @property {Simplate}
   * Template used on lookups to have empty Selection option.
   * This template is not directly rendered but included in {@link #viewTemplate}.
   *
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      emptySelectionText  The text to display on the empty Selection button.
   *
   * The default template exposes the following actions:
   *
   * * emptySelection
   */
  emptySelectionTemplate: new Simplate([
    '<div class="list-empty-opt" data-dojo-attach-point="emptySelectionNode">',
    '<button class="button" data-action="emptySelection">',
    '<span>{%= $.emptySelectionText %}</span>',
    '</button>',
    '</div>',
  ]),
  /**
   * @property {Simplate}
   * The template used to render a row in the view.  This template includes {@link #itemTemplate}.
   */
  rowTemplate: new Simplate([`
    <div data-action="activateEntry" data-key="{%= $$.getItemActionKey($) %}" data-descriptor="{%: $$.getItemDescriptor($) %}">
      <div class="widget">
        <div class="widget-header">
          {%! $$.itemIconTemplate %}
          <h2 class="widget-title">{%: $$.getTitle($, $$.labelProperty) %}</h2>
          {% if($$.visibleActions.length > 0 && $$.enableActions) { %}
            <button class="btn-actions" type="button" data-key="{%= $$.getItemActionKey($) %}">
              <span class="audible">Actions</span>
              <svg class="icon" focusable="false" aria-hidden="true" role="presentation">
                <use xlink:href="#icon-more"></use>
              </svg>
            </button>
            {%! $$.listActionTemplate %}
          {% } %}
        </div>
        <div class="card-content">
          {%! $$.itemRowContentTemplate %}
        </div>
      </div>
    </div>
    `,
  ]),
  liRowTemplate: new Simplate([
    '<li data-action="activateEntry" data-key="{%= $[$$.idProperty] %}" data-descriptor="{%: $$.utility.getValue($, $$.labelProperty) %}">',
    '{% if ($$.icon || $$.selectIcon) { %}',
    '<button type="button" class="btn-icon hide-focus list-item-selector" data-action="selectEntry">',
    `<svg class="icon" focusable="false" aria-hidden="true" role="presentation">
        <use xlink:href="#icon-{%= $$.icon || $$.selectIcon %}" />
      </svg>`,
    '</button>',
    '{% } %}',
    '</button>',
    '<div class="list-item-content">{%! $$.itemTemplate %}</div>',
    '</li>',
  ]),
  /**
   * @cfg {Simplate}
   * The template used to render the content of a row.  This template is not directly rendered, but is
   * included in {@link #rowTemplate}.
   *
   * This property should be overridden in the derived class.
   * @template
   */
  itemTemplate: new Simplate([
    '<p>{%: $[$$.labelProperty] %}</p>',
    '<p class="micro-text">{%: $[$$.idProperty] %}</p>',
  ]),
  /**
   * @property {Simplate}
   * The template used to render a message if there is no data available.
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      noDataText          The text to display if there is no data.
   */
  noDataTemplate: new Simplate([
    '<div class="no-data">',
    '<p>{%= $.noDataText %}</p>',
    '</div>',
  ]),
  /**
   * @property {Simplate}
   * The template used to render the single list action row.
   */
  listActionTemplate: new Simplate([
    '<ul id="popupmenu-{%= $$.getItemActionKey($) %}" data-dojo-attach-point="actionsNode" class="actions-row popupmenu actions top">',
    '{%! $$.loadingTemplate %}',
    '</ul>',
  ]),
  /**
   * @property {Simplate}
   * The template used to render a list action item.
   * The default template uses the following properties:
   *
   *      name                description
   *      ----------------------------------------------------------------
   *      actionIndex         The correlating index number of the action collection
   *      title               Text used for ARIA-labeling
   *      icon                Relative path to the icon to use
   *      cls                 CSS class to use instead of an icon
   *      id                  Unique name of action, also used for alt image text
   *      label               Text added below the icon
   */
  listActionItemTemplate: new Simplate([`
    <li><a></a><button class="popupitem" type="button" data-action="invokeActionItem" data-id="{%= $.actionIndex %}"
    aria-label="{%: $.title || $.id %}">{%: $.label %}</button></li>`]),
  /**
   * @property {Simplate}
   * The template used to render row content template
   */
  itemRowContentTemplate: new Simplate([
    '<div class="top_item_indicators list-item-indicator-content"></div>',
    '<div class="list-item-content">{%! $$.itemTemplate %}</div>',
    '<div class="bottom_item_indicators list-item-indicator-content"></div>',
    '<div class="list-item-content-related"></div>',
  ]),
  itemIconTemplate: new Simplate([
    '{% if ($$.getItemIconClass($)) { %}',
    `<button type="button" class="btn-icon hide-focus" class="list-item-selector button">
        <svg class="icon" focusable="false" aria-hidden="true" role="presentation">
            <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-{%= $$.getItemIconClass($) || 'alert' %}"></use>
        </svg>
    </button>`,
    '{% } else if ($$.getItemIconSource($)) { %}',
    '<button data-action="selectEntry" class="list-item-selector button">',
    '<img id="list-item-image_{%: $.$key %}" src="{%: $$.getItemIconSource($) %}" alt="{%: $$.getItemIconAlt($) %}" class="icon" />',
    '</button>',
    '{% } %}',
  ]),
  /**
   * @property {Simplate}
   * The template used to render item indicator
   */
  itemIndicatorTemplate: new Simplate([
    '<button type="button" class="btn-icon hide-focus" title="{%= $.label %}">',
    `<svg class="icon" focusable="false" aria-hidden="true" role="presentation">
        <use xlink:href="#icon-{%= $.cls %}" />
      </svg>`,
    '</button>',
  ]),
  /**
   * @property {HTMLElement}
   * Attach point for the main view content
   */
  contentNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the remaining entries content
   */
  remainingContentNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the search widget
   */
  searchNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the empty, or no selection, container
   */
  emptySelectionNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the remaining entries container
   */
  remainingNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the request more entries container
   */
  moreNode: null,
  /**
   * @property {HTMLElement}
   * Attach point for the list actions container
   */
  actionsNode: null,

  /**
   * @cfg {String} id
   * The id for the view, and it's main DOM element.
   */
  id: 'generic_list',

  /**
   * @cfg {String}
   * The SData resource kind the view is responsible for.  This will be used as the default resource kind
   * for all SData requests.
   */
  resourceKind: '',
  store: null,
  entries: null,
  /**
   * @property {Number}
   * The number of entries to request per SData payload.
   */
  pageSize: 21,
  /**
   * @property {Boolean}
   * Controls the addition of a search widget.
   */
  enableSearch: true,
  /**
   * @property {Boolean}
   * Flag that determines if the list actions panel should be in use.
   */
  enableActions: false,
  /**
   * @property {Boolean}
   * Controls the visibility of the search widget.
   */
  hideSearch: false,
  /**
   * @property {Boolean}
   * True to allow selections via the SelectionModel in the view.
   */
  allowSelection: false,
  /**
   * @property {Boolean}
   * True to clear the selection model when the view is shown.
   */
  autoClearSelection: true,
  /**
   * @property {String/View}
   * The id of the detail view, or view instance, to show when a row is clicked.
   */
  detailView: null,

  /**
   * @property {String}
   * The id of the configure view for quick action preferences
   */
  quickActionConfigureView: 'configure_quickactions',
  /**
   * @property {String}
   * The view id to show if there is no `insertView` specified, when
   * the {@link #navigateToInsertView} action is invoked.
   */
  editView: null,
  /**
   * @property {String}
   * The view id to show when the {@link #navigateToInsertView} action is invoked.
   */
  insertView: null,
  /**
   * @property {String}
   * The view id to show when the {@link #navigateToContextView} action is invoked.
   */
  contextView: false,
  /**
   * @property {Object}
   * A dictionary of hash tag search queries.  The key is the hash tag, without the symbol, and the value is
   * either a query string, or a function that returns a query string.
   */
  hashTagQueries: null,
  /**
   * The text displayed in the more button.
   * @type {String}
   */
  moreText: resource.moreText,
  /**
   * @property {String}
   * The text displayed in the emptySelection button.
   */
  emptySelectionText: resource.emptySelectionText,
  /**
   * @property {String}
   * The text displayed as the default title.
   */
  titleText: resource.titleText,
  /**
   * @property {String}
   * The text displayed for quick action configure.
   */
  configureText: resource.configureText,
  /**
   * @property {String}
   * The error message to display if rendering a row template is not successful.
   */
  errorRenderText: resource.errorRenderText,
  /**
   * @property {Simplate}
   *
   */
  rowTemplateError: new Simplate([
    '<div data-action="activateEntry" data-key="{%= $[$$.idProperty] %}" data-descriptor="{%: $[$$.labelProperty] %}">',
    '<div class="list-item-content">{%: $$.errorRenderText %}</div>',
    '</div>',
  ]),
  /**
   * @property {String}
   * The format string for the text displayed for the remaining record count.  This is used in a {@link String#format} call.
   */
  remainingText: resource.remainingText,
  /**
   * @property {String}
   * The text displayed on the cancel button.
   * @deprecated
   */
  cancelText: resource.cancelText,
  /**
   * @property {String}
   * The text displayed on the insert button.
   * @deprecated
   */
  insertText: resource.insertText,
  /**
   * @property {String}
   * The text displayed when no records are available.
   */
  noDataText: resource.noDataText,
  /**
   * @property {String}
   * The text displayed when data is being requested.
   */
  loadingText: resource.loadingText,
  /**
   * @property {String}
   * The text displayed in tooltip for the new button.
   */
  newTooltipText: resource.newTooltipText,
  /**
   * @property {String}
   * The text displayed in tooltip for the refresh button.
   */
  refreshTooltipText: resource.refreshTooltipText,
  /**
   * @property {String}
   * The customization identifier for this class. When a customization is registered it is passed
   * a path/identifier which is then matched to this property.
   */
  customizationSet: 'list',
  /**
   * @property {String}
   * The relative path to the checkmark or select icon for row selector
   */
  selectIcon: 'check',
  /**
   * @property {String}
   * CSS class to use for checkmark or select icon for row selector. Overrides selectIcon.
   */
  selectIconClass: '',
  /**
   * @property {Object}
   * The search widget instance for the view
   */
  searchWidget: null,
  /**
   * @property {SearchWidget}
   * The class constructor to use for the search widget
   */
  searchWidgetClass: SearchWidget,
  /**
   * @property {Boolean}
   * Flag to indicate the default search term has been set.
   */
  defaultSearchTermSet: false,

  /**
   * @property {String}
   * The default search term to use
   */
  defaultSearchTerm: '',

  /**
   * @property {String}
   * The current search term being used for the current requestData().
   */
  currentSearchExpression: '',
  /**
   * @property {Object}
   * The selection model for the view
   */
  _selectionModel: null,
  /**
   * @property {Object}
   * The selection event connections
   */
  _selectionConnects: null,
  /**
   * @property {Object}
   * The toolbar layout definition for all toolbar entries.
   */
  tools: null,
  /**
   * The list action layout definition for the list action bar.
   */
  actions: null,
  /**
   * @property {Boolean} If true, will remove the loading button and auto fetch more data when the user scrolls to the bottom of the page.
   */
  continuousScrolling: true,
  /**
   * @property {Boolean} Indicates if the list is loading
   */
  listLoading: false,
  /**
   * @property {Boolean}
   * Flags if the view is multi column or single column.
   */
  multiColumnView: true,
  /**
   * @property {string}
   * SoHo class to be applied on multi column.
   */
  multiColumnClass: 'four',
  /**
   * @property {number}
   * Number of columns in view
   */
  multiColumnCount: 3,
  // Store properties
  itemsProperty: '',
  idProperty: '',
  labelProperty: '',
  entityProperty: '',
  versionProperty: '',
  isRefreshing: false,
  /**
   * Sets the title to card
   */
  getTitle: function getTitle(entry, labelProperty) {
    return this.utility.getValue(entry, labelProperty);
  },
  /**
   * Setter method for the selection model, also binds the various selection model select events
   * to the respective List event handler for each.
   * @param {SelectionModel} selectionModel The selection model instance to save to the view
   * @private
   */
  _setSelectionModelAttr: function _setSelectionModelAttr(selectionModel) {
    if (this._selectionConnects) {
      this._selectionConnects.forEach(this.disconnect, this);
    }

    this._selectionModel = selectionModel;
    this._selectionConnects = [];

    if (this._selectionModel) {
      this._selectionConnects.push(
        this.connect(this._selectionModel, 'onSelect', this._onSelectionModelSelect),
        this.connect(this._selectionModel, 'onDeselect', this._onSelectionModelDeselect),
        this.connect(this._selectionModel, 'onClear', this._onSelectionModelClear)
      );
    }
  },
  /**
   * Getter nmethod for the selection model
   * @return {SelectionModel}
   * @private
   */
  _getSelectionModelAttr: function _getSelectionModelAttr() {
    return this._selectionModel;
  },
  constructor: function constructor(options) {
    this.entries = {};
    this._loadedSelections = {};

    // backward compatibility for disableRightDrawer property. To be removed after 4.0
    if (options && options.disableRightDrawer) {
      console.warn('disableRightDrawer property is depracated. Use hasSettings property instead. disableRightDrawer = !hasSettings');  //eslint-disable-line
      this.hasSettings = false;
    }
  },
  initSoho: function initSoho() {
    const toolbar = $('.toolbar', this.domNode).first();
    toolbar.toolbar();
    this.toolbar = toolbar.data('toolbar');
    $('[data-action=openSettings]', this.domNode).on('click', () => {
      this.openSettings();
    });
  },
  openSettings: function openSettings() {
  },
  updateSoho: function updateSoho() {
    this.toolbar.updated();
  },
  _onListViewSelected: function _onListViewSelected() {
    console.dir(arguments); //eslint-disable-line
  },
  postCreate: function postCreate() {
    this.inherited(arguments);

    if (this._selectionModel === null) {
      this.set('selectionModel', new ConfigurableSelectionModel());
    }
    this.subscribe('/app/refresh', this._onRefresh);

    if (this.enableSearch) {
      const SearchWidgetCtor = lang.isString(this.searchWidgetClass) ? lang.getObject(this.searchWidgetClass, false) : this.searchWidgetClass;

      this.searchWidget = this.searchWidget || new SearchWidgetCtor({
        class: 'list-search',
        owner: this,
        onSearchExpression: this._onSearchExpression.bind(this),
      });
      this.searchWidget.placeAt(this.searchNode, 'replace');
    } else {
      this.searchWidget = null;
    }

    if (this.hideSearch || !this.enableSearch) {
      $(this.domNode).addClass('list-hide-search');
    }

    this.clear();

    this.initPullToRefresh(this.scrollerNode);
  },
  shouldStartPullToRefresh: function shouldStartPullToRefresh() {
    // Get the base results
    const shouldStart = this.inherited(arguments);
    const selected = $(this.domNode).attr('selected');
    return shouldStart && selected === 'selected' && !this.listLoading;
  },
  forceRefresh: function forceRefresh() {
    this.clear();
    this.refreshRequired = true;
    this.refresh();
  },
  onPullToRefreshComplete: function onPullToRefreshComplete() {
    this.forceRefresh();
  },
  onConnectionStateChange: function onConnectionStateChange(state) {
    if (state === true && this.enableOfflineSupport) {
      this.refreshRequired = true;
    }
  },
  /**
   * Called on application startup to configure the search widget if present and create the list actions.
   */
  startup: function startup() {
    this.inherited(arguments);

    if (this.searchWidget) {
      this.searchWidget.configure({
        hashTagQueries: this._createCustomizedLayout(this.createHashTagQueryLayout(), 'hashTagQueries'),
        formatSearchQuery: this.formatSearchQuery.bind(this),
      });
    }
  },
  /**
   * Extends dijit Widget to destroy the search widget before destroying the view.
   */
  destroy: function destroy() {
    if (this.searchWidget) {
      if (!this.searchWidget._destroyed) {
        this.searchWidget.destroyRecursive();
      }

      delete this.searchWidget;
    }

    delete this.store;
    this.inherited(arguments);
  },
  _getStoreAttr: function _getStoreAttr() {
    return this.store || (this.store = this.createStore());
  },
  /**
   * Shows overrides the view class to set options for the list view and then calls the inherited show method on the view.
   * @param {Object} options The navigation options passed from the previous view.
   * @param transitionOptions {Object} Optional transition object that is forwarded to ReUI.
   */
  show: function show(options /* , transitionOptions*/) {
    if (options) {
      if (options.resetSearch) {
        this.defaultSearchTermSet = false;
      }

      if (options.allowEmptySelection === false && this._selectionModel) {
        this._selectionModel.requireSelection = true;
      }
    }

    this.inherited(arguments);
  },
  /**
   * Sets and returns the toolbar item layout definition, this method should be overriden in the view
   * so that you may define the views toolbar entries.
   * @return {Object} this.tools
   * @template
   */
  createToolLayout: function createToolLayout() {
    const toolbar = this.tools || (this.tools = {
      tbar: [{
        id: 'new',
        svg: 'add',
        title: this.newTooltipText,
        action: 'navigateToInsertView',
        security: this.app.getViewSecurity(this.insertView, 'insert'),
      }],
    });
    if ((toolbar.tbar && !this._refreshAdded) && !window.App.supportsTouch()) {
      this.tools.tbar.push({
        id: 'refresh',
        svg: 'refresh',
        title: this.refreshTooltipText,
        action: '_refreshList',
      });
      this._refreshAdded = true;
    }
    return this.tools;
  },
  createErrorHandlers: function createErrorHandlers() {
    this.errorHandlers = this.errorHandlers || [{
      name: 'Aborted',
      test: function testAborted(error) {
        return error.aborted;
      },
      handle: function handleAborted(error, next) {
        this.clear();
        this.refreshRequired = true;
        next();
      },
    }, {
      name: 'AlertError',
      test: function testError(error) {
        return !error.aborted;
      },
      handle: function handleError(error, next) {
        alert(this.getErrorMessage(error)); // eslint-disable-line
        next();
      },
    }, {
      name: 'CatchAll',
      test: function testCatchAll() {
        return true;
      },
      handle: function handleCatchAll(error, next) {
        this._logError(error);
        this._clearLoading();
        next();
      },
    }];

    return this.errorHandlers;
  },
  /**
   * Sets and returns the list-action actions layout definition, this method should be overriden in the view
   * so that you may define the action entries for that view.
   * @return {Object} this.acttions
   */
  createActionLayout: function createActionLayout() {
    return this.actions || [];
  },
  /**
   * Creates the action bar and adds it to the DOM. Note that it replaces `this.actions` with the passed
   * param as the passed param should be the result of the customization mixin and `this.actions` needs to be the
   * final actions state.
   * @param {Object[]} actions
   */
  createActions: function createActions(a) {
    let actions = a;
    this.actions = actions.reduce(this._removeActionDuplicates, []);
    this.visibleActions = [];


    this.ensureQuickActionPrefs();

    // Pluck out our system actions that are NOT saved in preferences
    let systemActions = actions.filter((action) => {
      return action && action.systemAction;
    });

    systemActions = systemActions.reduce(this._removeActionDuplicates, []);

    // Grab quick actions from the users preferences (ordered and made visible according to user)
    let prefActions;
    if (this.app.preferences && this.app.preferences.quickActions) {
      prefActions = this.app.preferences.quickActions[this.id];
    }

    if (systemActions && prefActions) {
      // Display system actions first, then the order of what the user specified
      actions = systemActions.concat(prefActions);
    }

    const visibleActions = [];

    for (let i = 0; i < actions.length; i++) {
      const action = actions[i];

      if (!action.visible) {
        continue;
      }

      if (!action.security) {
        const orig = a.find(x => x.id === action.id);
        if (orig && orig.security) {
          action.security = orig.security; // Reset the security value
        }
      }

      const options = {
        actionIndex: visibleActions.length,
        hasAccess: (!action.security || (action.security && this.app.hasAccessTo(this.expandExpression(action.security)))) ? true : false,
      };

      lang.mixin(action, options);

      const actionTemplate = action.template || this.listActionItemTemplate;
      action.templateDom = $(actionTemplate.apply(action, action.id));

      visibleActions.push(action);
    }
    this.visibleActions = visibleActions;
  },
  createSystemActionLayout: function createSystemActionLayout(actions = []) {
    const systemActions = actions.filter((action) => {
      return action.systemAction === true;
    });

    const others = actions.filter((action) => {
      return !action.systemAction;
    });

    if (!others.length) {
      return [];
    }

    if (systemActions.length) {
      return systemActions.concat(others);
    }

    return [{
      id: '__editPrefs__',
      cls: 'settings',
      label: this.configureText,
      action: 'configureQuickActions',
      systemAction: true,
      visible: true,
    }].concat(others);
  },
  configureQuickActions: function configureQuickActions() {
    const view = App.getView(this.quickActionConfigureView);
    if (view) {
      view.show({
        viewId: this.id,
        actions: this.actions.filter((action) => {
          // Exclude system actions
          return action && action.systemAction !== true;
        }),
      });
    }
  },
  selectEntrySilent: function selectEntrySilent(key) {
    const enableActions = this.enableActions; // preserve the original value
    const selectionModel = this.get('selectionModel');
    let selection;

    if (key) {
      this.enableActions = false; // Set to false so the quick actions menu doesn't pop up
      selectionModel.clear();
      selectionModel.toggle(key, this.entries[key]);
      const selectedItems = selectionModel.getSelections();
      this.enableActions = enableActions;

      // We know we are single select, so just grab the first selection
      for (const prop in selectedItems) {
        if (selectedItems.hasOwnProperty(prop)) {
          selection = selectedItems[prop];
          break;
        }
      }
    }

    return selection;
  },
  invokeActionItemBy: function invokeActionItemBy(actionPredicate, key) {
    const actions = this.visibleActions.filter(actionPredicate);
    const selection = this.selectEntrySilent(key);
    this.checkActionState();
    actions.forEach((action) => {
      this._invokeAction(action, selection);
    });
  },
  /**
   * This is the data-action handler for list-actions, it will locate the action instance viw the data-id attribute
   * and invoke either the `fn` with `scope` or the named `action` on the current view.
   *
   * The resulting function being called will be passed not only the action item definition but also
   * the first (only) selection from the lists selection model.
   *
   * @param {Object} parameters Collection of data- attributes already gathered from the node
   * @param {Event} evt The click/tap event
   * @param {HTMLElement} node The node that invoked the action
   */
  invokeActionItem: function invokeActionItem(parameters, evt, node) {
    const popupmenu = $(node)
      .parent('li')
      .parent('.actions-row')
      .parent('.popupmenu-wrapper')
      .prev()
      .data('popupmenu');
    if (popupmenu) {
      setTimeout(() => {
        popupmenu.close();
      }, 100);
    }

    const index = parameters.id;
    const action = this.visibleActions[index];
    const selectedItems = this.get('selectionModel')
      .getSelections();
    let selection = null;

    for (const key in selectedItems) {
      if (selectedItems.hasOwnProperty(key)) {
        selection = selectedItems[key];
        this._selectionModel.deselect(key);
        break;
      }
    }
    this._invokeAction(action, selection);
  },
  _invokeAction: function _invokeAction(action, selection) {
    if (!action.isEnabled) {
      return;
    }

    if (action.fn) {
      action.fn.call(action.scope || this, action, selection);
    } else {
      if (action.action) {
        if (this.hasAction(action.action)) {
          this.invokeAction(action.action, action, selection);
        }
      }
    }
  },
  /**
   * Called when showing the action bar for a newly selected row, it sets the disabled state for each action
   * item using the currently selected row as context by passing the action instance the selected row to the
   * action items `enabled` property.
   */
  checkActionState: function checkActionState(rowNode) {
    const selectedItems = this.get('selectionModel')
      .getSelections();
    let selection = null;

    for (const key in selectedItems) {
      if (selectedItems.hasOwnProperty(key)) {
        selection = selectedItems[key];
        break;
      }
    }

    this._applyStateToActions(selection, rowNode);
  },
  getQuickActionPrefs: function getQuickActionPrefs() {
    return this.app && this.app.preferences && this.app.preferences.quickActions;
  },
  _removeActionDuplicates: function _removeActionDuplicates(acc, cur) {
    const hasID = acc.some((item) => {
      return item.id === cur.id;
    });

    if (!hasID) {
      acc.push(cur);
    }

    return acc;
  },
  ensureQuickActionPrefs: function ensureQuickActionPrefs() {
    const appPrefs = this.app && this.app.preferences;
    let actionPrefs = this.getQuickActionPrefs();
    const filtered = this.actions.filter((action) => {
      return action && action.systemAction !== true;
    });

    if (!this.actions || !appPrefs) {
      return;
    }

    if (!actionPrefs) {
      appPrefs.quickActions = {};
      actionPrefs = appPrefs.quickActions;
    }

    // If it doesn't exist, or there is a count mismatch (actions created on upgrades perhaps?)
    // re-create the preferences store
    if (!actionPrefs[this.id] ||
      (actionPrefs[this.id] && actionPrefs[this.id].length !== filtered.length)) {
      actionPrefs[this.id] = filtered.map((action) => {
        action.visible = true;
        return action;
      });

      this.app.persistPreferences();
    }
  },
  /**
   * Called from checkActionState method and sets the state of the actions from what was selected from the selected row, it sets the disabled state for each action
   * item using the currently selected row as context by passing the action instance the selected row to the
   * action items `enabled` property.
   * @param {Object} selection
   */
  _applyStateToActions: function _applyStateToActions(selection, rowNode) {
    let actionRow;
    if (rowNode) {
      actionRow = $(rowNode).find('.actions-row')[0];
      $(actionRow).empty();
    }

    for (let i = 0; i < this.visibleActions.length; i++) {
      // The visible action is from our local storage preferences, where the action from the layout
      // contains functions that will get stripped out converting it to JSON, get the original action
      // and mix it into the visible so we can work with it.
      // TODO: This will be a problem throughout visible actions, come up with a better solution
      const visibleAction = this.visibleActions[i];
      const action = lang.mixin(visibleAction, this._getActionById(visibleAction.id));

      action.isEnabled = (typeof action.enabled === 'undefined') ? true : this.expandExpression(action.enabled, action, selection);

      if (!action.hasAccess) {
        action.isEnabled = false;
      }
      if (rowNode) {
        $(visibleAction.templateDom)
          .clone()
          .toggleClass('toolButton-disabled', !action.isEnabled)
          .appendTo(actionRow);
      }
    }

    if (rowNode) {
      const popupmenuNode = $(rowNode).find('.btn-actions')[0];
      const popupmenu = $(popupmenuNode).data('popupmenu');
      setTimeout(() => {
        popupmenu.position();
      }, 1);
    }
  },
  _getActionById: function _getActionById(id) {
    return this.actions.filter((action) => {
      return action && action.id === id;
    })[0];
  },
  /**
   * Handler for showing the list-action panel/bar - it needs to do several things:
   *
   * 1. Check each item for context-enabledment
   * 1. Move the action panel to the current row and show it
   * 1. Adjust the scrolling if needed (if selected row is at bottom of screen, the action-bar shows off screen
   * which is bad)
   *
   * @param {HTMLElement} rowNode The currently selected row node
   */
  showActionPanel: function showActionPanel(rowNode) {
    const actionNode = $(rowNode).find('.actions-row');
    this.checkActionState(rowNode);
    this.onApplyRowActionPanel(actionNode, rowNode);
  },
  onApplyRowActionPanel: function onApplyRowActionPanel(/* actionNodePanel, rowNode*/) {},
  /**
   * Sets the `this.options.source` to passed param after adding the views resourceKind. This function is used so
   * that when the next view queries the navigation context we can include the passed param as a data point.
   *
   * @param {Object} source The object to set as the options.source.
   */
  setSource: function setSource(source) {
    lang.mixin(source, {
      resourceKind: this.resourceKind,
    });

    this.options.source = source;
  },
  /**
   * @deprecated
   * Hides the passed list-action row/panel by removing the selected styling
   * @param {HTMLElement} rowNode The currently selected row.
   */
  hideActionPanel: function hideActionPanel() {
  },
  /**
   * Determines if the view is a navigatible view or a selection view by returning `this.selectionOnly` or the
   * navigation `this.options.selectionOnly`.
   * @return {Boolean}
   */
  isNavigationDisabled: function isNavigationDisabled() {
    return ((this.options && this.options.selectionOnly) || (this.selectionOnly));
  },
  /**
   * Determines if the selections are disabled by checking the `allowSelection` and `enableActions`
   * @return {Boolean}
   */
  isSelectionDisabled: function isSelectionDisabled() {
    return !((this.options && this.options.selectionOnly) || this.enableActions || this.allowSelection);
  },
  /**
   * Handler for when the selection model adds an item. Adds the selected state to the row or shows the list
   * actions panel.
   * @param {String} key The extracted key from the selected row.
   * @param {Object} data The actual row's matching data point
   * @param {String/HTMLElement} tag An indentifier, may be the actual row node or some other id.
   * @private
   */
  _onSelectionModelSelect: function _onSelectionModelSelect(key, data, tag) { // eslint-disable-line
    const node = $(tag);

    if (this.enableActions) {
      this.showActionPanel(node.get(0));
      return;
    }

    node.addClass('list-item-selected');
    node.removeClass('list-item-de-selected');
  },
  /**
   * Handler for when the selection model removes an item. Removes the selected state to the row or hides the list
   * actions panel.
   * @param {String} key The extracted key from the de-selected row.
   * @param {Object} data The actual row's matching data point
   * @param {String/HTMLElement} tag An indentifier, may be the actual row node or some other id.
   * @private
   */
  _onSelectionModelDeselect: function _onSelectionModelDeselect(key, data, tag) {
    const node = $(tag) || $(`[data-key="${key}"]`, this.contentNode).first();
    if (!node.length) {
      return;
    }

    node.removeClass('list-item-selected');
    node.addClass('list-item-de-selected');
  },
  /**
   * Handler for when the selection model clears the selections.
   * @private
   */
  _onSelectionModelClear: function _onSelectionModelClear() {},

  /**
   * Cache of loaded selections
   */
  _loadedSelections: null,

  /**
   * Attempts to activate entries passed in `this.options.previousSelections` where previousSelections is an array
   * of data-keys or data-descriptors to search the list rows for.
   * @private
   */
  _loadPreviousSelections: function _loadPreviousSelections() {
    const previousSelections = this.options && this.options.previousSelections;
    if (previousSelections) {
      for (let i = 0; i < previousSelections.length; i++) {
        const key = previousSelections[i];

        // Set initial state of previous selection to unloaded (false)
        if (!this._loadedSelections.hasOwnProperty(key)) {
          this._loadedSelections[key] = false;
        }

        const row = $(`[data-key="${key}"], [data-descriptor="${key}"]`, this.contentNode)[0];

        if (row && this._loadedSelections[key] !== true) {
          this.activateEntry({
            key,
            descriptor: key,
            $source: row,
          });

          // Flag that this previous selection has been loaded, since this function can be called
          // multiple times, while paging through long lists. clear() will reset.
          this._loadedSelections[key] = true;
        }
      }
    }
  },
  applyRowIndicators: function applyRowIndicators(entry, rowNode) {
    if (this.itemIndicators && this.itemIndicators.length > 0) {
      const topIndicatorsNode = $('.top_item_indicators', rowNode);
      const bottomIndicatorsNode = $('.bottom_item_indicators', rowNode);
      if (bottomIndicatorsNode[0] && topIndicatorsNode[0]) {
        if (bottomIndicatorsNode[0].childNodes.length === 0 && topIndicatorsNode[0].childNodes.length === 0) {
          const customizeLayout = this._createCustomizedLayout(this.itemIndicators, 'indicators');
          this.createIndicators(topIndicatorsNode[0], bottomIndicatorsNode[0], customizeLayout, entry);
        }
      }
    }
  },
  createIndicatorLayout: function createIndicatorLayout() {
    return this.itemIndicators || (this.itemIndicators = [{
      id: 'touched',
      cls: 'flag',
      onApply: function onApply(entry, parent) {
        this.isEnabled = parent.hasBeenTouched(entry);
      },
    }]);
  },
  hasBeenTouched: function hasBeenTouched(entry) {
    if (entry.ModifyDate) {
      const modifiedDate = moment(convert.toDateFromString(entry.ModifyDate));
      const currentDate = moment().endOf('day');
      const weekAgo = moment().subtract(1, 'weeks');

      return modifiedDate.isAfter(weekAgo) &&
        modifiedDate.isBefore(currentDate);
    }
    return false;
  },
  _refreshList: function _refreshList() {
    this.forceRefresh();
  },
  /**
   * Returns this.options.previousSelections that have not been loaded or paged to
   * @return {Array}
   */
  getUnloadedSelections: function getUnloadedSelections() {
    return Object.keys(this._loadedSelections)
      .filter((key) => {
        return this._loadedSelections[key] === false;
      });
  },
  /**
   * Handler for the global `/app/refresh` event. Sets `refreshRequired` to true if the resourceKind matches.
   * @param {Object} options The object published by the event.
   * @private
   */
  _onRefresh: function _onRefresh(/* options*/) {},
  onScroll: function onScroll(/* evt*/) {
    const scrollerNode = this.scrollerNode;
    const height = $(scrollerNode).height(); // viewport height (what user sees)
    const scrollHeight = scrollerNode.scrollHeight; // Entire container height
    const scrollTop = scrollerNode.scrollTop; // How far we are scrolled down
    const remaining = scrollHeight - scrollTop; // Height we have remaining to scroll
    const selected = $(this.domNode).attr('selected');
    const diff = Math.abs(remaining - height);

    // Start auto fetching more data if the user is on the last half of the remaining screen
    if (diff <= height / 2) {
      if (selected === 'selected' && this.hasMoreData() && !this.listLoading) {
        this.more();
      }
    }
  },
  /**
   * Handler for the select or action node data-action. Finds the nearest node with the data-key attribute and
   * toggles it in the views selection model.
   *
   * If singleSelectAction is defined, invoke the singleSelectionAction.
   *
   * @param {Object} params Collection of `data-` attributes from the node.
   * @param {Event} evt The click/tap event.
   * @param {HTMLElement} node The element that initiated the event.
   */
  selectEntry: function selectEntry(params) {
    const row = $(`[data-key='${params.key}']`, this.contentNode).first();
    const key = row ? row.attr('data-key') : false;

    if (this._selectionModel && key) {
      this._selectionModel.select(key, this.entries[key], row.get(0));
    }

    if (this.options.singleSelect && this.options.singleSelectAction && !this.enableActions) {
      this.invokeSingleSelectAction();
    }
  },
  /**
   * Handler for each row.
   *
   * If a selection model is defined and navigation is disabled then toggle the entry/row
   * in the model and if singleSelectionAction is true invoke the singleSelectAction.
   *
   * Else navigate to the detail view for the extracted data-key.
   *
   * @param {Object} params Collection of `data-` attributes from the node.
   */
  activateEntry: function activateEntry(params) {
    // dont navigate if clicked on QA button
    if (params.$event && params.$event.target.className && params.$event.target.className.indexOf('btn-actions') !== -1) {
      return;
    }
    if (params.key) {
      if (this._selectionModel && this.isNavigationDisabled()) {
        this._selectionModel.toggle(params.key, this.entries[params.key] || params.descriptor, params.$source);
        if (this.options.singleSelect && this.options.singleSelectAction) {
          this.invokeSingleSelectAction();
        }
      } else {
        this.navigateToDetailView(params.key, params.descriptor);
      }
    }
  },
  /**
   * Invokes the corresponding top toolbar tool using `this.options.singleSelectAction` as the name.
   * If autoClearSelection is true, clear the selection model.
   */
  invokeSingleSelectAction: function invokeSingleSelectAction() {
    if (this.app.bars.tbar) {
      this.app.bars.tbar.invokeTool({
        tool: this.options.singleSelectAction,
      });
    }

    if (this.autoClearSelection) {
      this._selectionModel.clear();
      this._loadedSelections = {};
    }
  },
  /**
   * Called to transform a textual query into an SData query compatible search expression.
   *
   * Views should override this function to provide their own formatting tailored to their entity.
   *
   * @param {String} searchQuery User inputted text from the search widget.
   * @return {String/Boolean} An SData query compatible search expression.
   * @template
   */
  formatSearchQuery: function formatSearchQuery(/* searchQuery*/) {
    return false;
  },
  /**
   * Replaces a single `"` with two `""` for proper SData query expressions.
   * @param {String} searchQuery Search expression to be escaped.
   * @return {String}
   */
  escapeSearchQuery: function escapeSearchQuery(searchQuery) {
    return Utility.escapeSearchQuery(searchQuery);
  },
  /**
   * Handler for the search widgets search.
   *
   * Prepares the view by clearing it and setting `this.query` to the given search expression. Then calls
   * {@link #requestData requestData} which start the request process.
   *
   * @param {String} expression String expression as returned from the search widget
   * @private
   */
  _onSearchExpression: function _onSearchExpression(expression) {
    this.clear(false);
    this.queryText = '';
    this.query = expression;

    this.requestData();
  },
  /**
   * Sets the default search expression (acting as a pre-filter) to `this.options.query` and configures the
   * search widget by passing in the current view context.
   */
  configureSearch: function configureSearch() {
    this.query = this.options && this.options.query || this.query || null;
    if (this.searchWidget) {
      this.searchWidget.configure({
        context: this.getContext(),
      });
    }

    this._setDefaultSearchTerm();
  },
  _setDefaultSearchTerm: function _setDefaultSearchTerm() {
    if (!this.defaultSearchTerm || this.defaultSearchTermSet) {
      return;
    }

    if (typeof this.defaultSearchTerm === 'function') {
      this.setSearchTerm(this.defaultSearchTerm());
    } else {
      this.setSearchTerm(this.defaultSearchTerm);
    }

    this._updateQuery();

    this.defaultSearchTermSet = true;
  },
  _updateQuery: function _updateQuery() {
    const searchQuery = this.getSearchQuery();
    if (searchQuery) {
      this.query = searchQuery;
    } else {
      this.query = '';
    }
  },
  getSearchQuery: function getSearchQuery() {
    let results = null;

    if (this.searchWidget) {
      results = this.searchWidget.getFormattedSearchQuery();
    }

    return results;
  },
  /**
   * Helper method for list actions. Takes a view id, data point and where format string, sets the nav options
   * `where` to the formatted expression using the data point and shows the given view id with that option.
   * @param {Object} action Action instance, not used.
   * @param {Object} selection Data entry for the selection.
   * @param {String} viewId View id to be shown
   * @param {String} whereQueryFmt Where expression format string to be passed. `${0}` will be the `idProperty`
   * @param {Object} additionalOptions Additional options to be passed into the next view
   * property of the passed selection data.
   */
  navigateToRelatedView: function navigateToRelatedView(action, selection, viewId, whereQueryFmt, additionalOptions) {
    const view = this.app.getView(viewId);
    let options = {
      where: string.substitute(whereQueryFmt, [selection.data[this.idProperty]]),
      selectedEntry: selection.data,
    };

    if (additionalOptions) {
      options = lang.mixin(options, additionalOptions);
    }

    this.setSource({
      entry: selection.data,
      descriptor: selection.data[this.labelProperty],
      key: selection.data[this.idProperty],
    });

    if (view) {
      view.show(options);
    }
  },
  /**
   * Navigates to the defined `this.detailView` passing the params as navigation options.
   * @param {String} key Key of the entry to be shown in detail
   * @param {String} descriptor Description of the entry, will be used as the top toolbar title text
   * @param {Object} additionalOptions Additional options to be passed into the next view
   */
  navigateToDetailView: function navigateToDetailView(key, descriptor, additionalOptions) {
    const view = this.app.getView(this.detailView);
    let options = {
      descriptor, // keep for backwards compat
      title: descriptor,
      key,
      fromContext: this,
    };

    if (additionalOptions) {
      options = lang.mixin(options, additionalOptions);
    }

    if (view) {
      view.show(options);
    }
  },
  /**
   * Helper method for list-actions. Navigates to the defined `this.editView` passing the given selections `idProperty`
   * property in the navigation options (which is then requested and result used as default data).
   * @param {Object} action Action instance, not used.
   * @param {Object} selection Data entry for the selection.
   * @param {Object} additionalOptions Additional options to be passed into the next view.
   */
  navigateToEditView: function navigateToEditView(action, selection, additionalOptions) {
    const view = this.app.getView(this.editView || this.insertView);
    const key = selection.data[this.idProperty];
    let options = {
      key,
      selectedEntry: selection.data,
      fromContext: this,
    };

    if (additionalOptions) {
      options = lang.mixin(options, additionalOptions);
    }

    if (view) {
      view.show(options);
    }
  },
  /**
   * Navigates to the defined `this.insertView`, or `this.editView` passing the current views id as the `returnTo`
   * option and setting `insert` to true.
   * @param {Object} additionalOptions Additional options to be passed into the next view.
   */
  navigateToInsertView: function navigateToInsertView(additionalOptions) {
    const view = this.app.getView(this.insertView || this.editView);
    let options = {
      returnTo: this.id,
      insert: true,
    };

    // Pass along the selected entry (related list could get it from a quick action)
    if (this.options.selectedEntry) {
      options.selectedEntry = this.options.selectedEntry;
    }

    if (additionalOptions) {
      options = lang.mixin(options, additionalOptions);
    }

    if (view) {
      view.show(options);
    }
  },
  /**
   * Deterimines if there is more data to be shown.
   * @return {Boolean} True if the list has more data; False otherwise. Default is true.
   */
  hasMoreData: function hasMoreData() {},
  _setLoading: function _setLoading() {
    $(this.domNode).addClass('list-loading');
    this.listLoading = true;
  },
  _clearLoading: function _clearLoading() {
    $(this.domNode).removeClass('list-loading');
    this.listLoading = false;
  },
  /**
   * Initiates the data request.
   */
  requestData: function requestData() {
    const store = this.get('store');

    if (!store && !this._model) {
      console.warn('Error requesting data, no store was defined. Did you mean to mixin _SDataListMixin to your list view?'); // eslint-disable-line
      return null;
    }

    if (this.searchWidget) {
      this.currentSearchExpression = this.searchWidget.getSearchExpression();
    }

    this._setLoading();

    let queryResults;
    let queryOptions;
    let queryExpression;
    if (this._model) {
      // Todo: find a better way to transfer this state.
      this.options.count = this.pageSize;
      this.options.start = this.position;
      queryOptions = {};
      this._applyStateToQueryOptions(queryOptions);
      queryExpression = this._buildQueryExpression() || null;
      queryResults = this.requestDataUsingModel(queryExpression, queryOptions);
    } else {
      queryOptions = {};
      this._applyStateToQueryOptions(queryOptions);

      queryExpression = this._buildQueryExpression() || null;
      queryResults = this.requestDataUsingStore(queryExpression, queryOptions);
    }
    $.when(queryResults)
    .done((results) => {
      this._onQueryComplete(queryResults, results);
    })
    .fail(() => {
      this._onQueryError(queryResults, queryOptions);
    });

    return queryResults;
  },
  requestDataUsingModel: function requestDataUsingModel(queryExpression, options) {
    const queryOptions = {
      returnQueryResults: true,
      queryModelName: this.queryModelName,
    };
    lang.mixin(queryOptions, options);
    return this._model.getEntries(queryExpression, queryOptions);
  },
  requestDataUsingStore: function requestDataUsingStore(queryExpression, queryOptions) {
    const store = this.get('store');
    return store.query(queryExpression, queryOptions);
  },
  postMixInProperties: function postMixInProperties() {
    this.inherited(arguments);
    this.createIndicatorLayout();
  },
  getItemActionKey: function getItemActionKey(entry) {
    return this.getIdentity(entry);
  },
  getItemDescriptor: function getItemDescriptor(entry) {
    return entry.$descriptor || entry[this.labelProperty];
  },
  getItemIconClass: function getItemIconClass() {
    return this.itemIconClass;
  },
  getItemIconSource: function getItemIconSource() {
    return this.itemIcon || this.icon;
  },
  getItemIconAlt: function getItemIconAlt() {
    return this.itemIconAltText;
  },
  createIndicators: function createIndicators(topIndicatorsNode, bottomIndicatorsNode, indicators, entry) {
    const self = this;
    for (let i = 0; i < indicators.length; i++) {
      const indicator = indicators[i];
      const iconPath = indicator.iconPath || self.itemIndicatorIconPath;
      if (indicator.onApply) {
        try {
          indicator.onApply(entry, self);
        } catch (err) {
          indicator.isEnabled = false;
        }
      }
      const options = {
        indicatorIndex: i,
        indicatorIcon: indicator.icon ? iconPath + indicator.icon : '',
        iconCls: indicator.cls || '',
      };

      const indicatorTemplate = indicator.template || self.itemIndicatorTemplate;

      lang.mixin(indicator, options);

      if (indicator.isEnabled === false) {
        if (indicator.cls) {
          indicator.iconCls = `${indicator.cls} disabled`;
        } else {
          indicator.indicatorIcon = indicator.icon ? `${iconPath}disabled_${indicator.icon}` : '';
        }
      } else {
        indicator.indicatorIcon = indicator.icon ? iconPath + indicator.icon : '';
      }

      if (indicator.isEnabled === false && indicator.showIcon === false) {
        return;
      }

      if (self.itemIndicatorShowDisabled || indicator.isEnabled) {
        if (indicator.isEnabled === false && indicator.showIcon === false) {
          return;
        }
        const indicatorHTML = indicatorTemplate.apply(indicator, indicator.id);
        if (indicator.location === 'top') {
          $(topIndicatorsNode).append(indicatorHTML);
        } else {
          $(bottomIndicatorsNode).append(indicatorHTML);
        }
      }
    }
  },
  _onQueryComplete: function _onQueryComplete(queryResults, entries) {
    try {
      const start = this.position;

      try {
        $.when(queryResults.total)
        .done((result) => {
          this._onQueryTotal(result);
        })
        .fail((error) => {
          this._onQueryTotalError(error);
        });

        /* todo: move to a more appropriate location */
        if (this.options && this.options.allowEmptySelection) {
          $(this.domNode).addClass('list-has-empty-opt');
        }

        /* remove the loading indicator so that it does not get re-shown while requesting more data */
        if (start === 0) {
          // Check entries.length so we don't clear out the "noData" template
          if (entries && entries.length > 0) {
            this.set('listContent', '');
          }

          $(this.loadingIndicatorNode).remove();
        }

        this.processData(entries);
      } finally {
        this._clearLoading();
        this.isRefreshing = false;
      }

      if (!this._onScrollHandle && this.continuousScrolling) {
        this._onScrollHandle = this.connect(this.scrollerNode, 'onscroll', this.onScroll);
      }

      this.onContentChange();
      connect.publish('/app/toolbar/update', []);

      if (this._selectionModel) {
        this._loadPreviousSelections();
      }
    } catch (e) {
      console.error(e); // eslint-disable-line
      this._logError({
        message: e.message,
        stack: e.stack,
      }, e.message);
    }
  },
  createStore: function createStore() {
    return null;
  },
  onContentChange: function onContentChange() {},
  _processEntry: function _processEntry(entry) {
    return entry;
  },
  _onQueryTotalError: function _onQueryTotalError(error) {
    this.handleError(error);
  },
  _onQueryTotal: function _onQueryTotal(size) {
    this.total = size;
    if (size === 0) {
      this.set('listContent', this.noDataTemplate.apply(this));
    } else {
      const remaining = this.getRemainingCount();
      if (remaining !== -1) {
        this.set('remainingContent', string.substitute(this.remainingText, [remaining]));
        this.remaining = remaining;
      }

      $(this.domNode).toggleClass('list-has-more', (remaining === -1 || remaining > 0));

      this.position = this.position + this.pageSize;
    }
  },
  getRemainingCount: function getRemainingCount() {
    const remaining = this.total > -1 ? this.total - (this.position + this.pageSize) : -1;
    return remaining;
  },
  onApplyRowTemplate: function onApplyRowTemplate(entry, rowNode) {
    this.applyRowIndicators(entry, rowNode);
    this.initRowQuickActions(rowNode);
  },
  initRowQuickActions: function initRowQuickActions(rowNode) {
    if (this.isCardView && this.visibleActions.length) {
      // initialize popupmenus on each card
      const btn = $(rowNode).find('.btn-actions');
      $(btn).popupmenu();
      $(btn).on('beforeopen', (evt) => {
        this.selectEntry({ key: evt.target.attributes['data-key'].value });
      });
    }
  },
  processData: function processData(entries) {
    if (!entries) {
      return;
    }

    const count = entries.length;

    if (count > 0) {
      const docfrag = document.createDocumentFragment();
      let row = [];
      for (let i = 0; i < count; i++) {
        const entry = this._processEntry(entries[i]);
        // If key comes back with nothing, check that the store is properly
        // setup with an idProperty
        this.entries[this.getIdentity(entry, i)] = entry;

        const rowNode = this.createItemRowNode(entry);

        if (this.isCardView && this.multiColumnView) {
          const column = $(`<div class="${this.multiColumnClass} columns">`).append(rowNode);
          row.push(column);
          if ((i + 1) % this.multiColumnCount === 0 || i === count - 1) {
            const rowTemplate = $('<div class="row"></div>');
            row.forEach((element) => {
              rowTemplate.append(element);
            });
            docfrag.appendChild(rowTemplate.get(0));
            row = [];
          }
        } else {
          docfrag.appendChild(rowNode);
        }
        this.onApplyRowTemplate(entry, rowNode);
      }

      if (docfrag.childNodes.length > 0) {
        $(this.contentNode).append(docfrag);
      }
    }
  },
  createItemRowNode: function createItemRowNode(entry) {
    let rowNode = null;
    try {
      if (this.isCardView) {
        rowNode = $(this.rowTemplate.apply(entry, this));
      } else {
        rowNode = $(this.liRowTemplate.apply(entry, this));
      }
    } catch (err) {
      console.error(err); // eslint-disable-line
      rowNode = $(this.rowTemplateError.apply(entry, this));
    }
    return rowNode.get(0);
  },
  getIdentity: function getIdentity(entry, defaultId) {
    let modelId;
    let storeId;

    if (this._model) {
      modelId = this._model.getEntityId(entry);
    }

    if (modelId) {
      return modelId;
    }

    const store = this.get('store');
    if (store) {
      storeId = store.getIdentity(entry, this.idProperty);
    }

    if (storeId) {
      return storeId;
    }

    return defaultId;
  },
  _logError: function _logError(error, message) {
    const fromContext = this.options.fromContext;
    this.options.fromContext = null;
    const errorItem = {
      viewOptions: this.options,
      serverError: error,
    };

    ErrorManager.addError(message || this.getErrorMessage(error), errorItem);
    this.options.fromContext = fromContext;
  },
  _onQueryError: function _onQueryError(queryOptions, error) {
    this.handleError(error);
    this.isRefreshing = false;
  },
  _buildQueryExpression: function _buildQueryExpression() {
    return lang.mixin(this.query || {}, this.options && (this.options.query || this.options.where));
  },
  _applyStateToQueryOptions: function _applyStateToQueryOptions(/* queryOptions*/) {},
  /**
   * Handler for the more button. Simply calls {@link #requestData requestData} which already has the info for
   * setting the start index as needed.
   */
  more: function more() {
    if (this.continuousScrolling) {
      this.set('remainingContent', this.loadingTemplate.apply(this));
    }

    this.requestData();
  },
  /**
   * Handler for the none/no selection button is pressed. Used in selection views when not selecting is an option.
   * Invokes the `this.options.singleSelectAction` tool.
   */
  emptySelection: function emptySelection() {
    this._selectionModel.clear();
    this._loadedSelections = {};

    if (this.app.bars.tbar) {
      this.app.bars.tbar.invokeTool({
        tool: this.options.singleSelectAction,
      }); // invoke action of tool
    }
  },
  /**
   * Determines if the view should be refresh by inspecting and comparing the passed navigation options with current values.
   * @param {Object} options Passed navigation options.
   * @return {Boolean} True if the view should be refreshed, false if not.
   */
  refreshRequiredFor: function refreshRequiredFor(options) {
    if (this.options) {
      if (options) {
        if (this.expandExpression(this.options.stateKey) !== this.expandExpression(options.stateKey)) {
          return true;
        }
        if (this.expandExpression(this.options.where) !== this.expandExpression(options.where)) {
          return true;
        }
        if (this.expandExpression(this.options.query) !== this.expandExpression(options.query)) {
          return true;
        }
        if (this.expandExpression(this.options.resourceKind) !== this.expandExpression(options.resourceKind)) {
          return true;
        }
        if (this.expandExpression(this.options.resourcePredicate) !== this.expandExpression(options.resourcePredicate)) {
          return true;
        }
      }

      return false;
    }

    return this.inherited(arguments);
  },
  /**
   * Returns the current views context by expanding upon the {@link View#getContext parent implementation} to include
   * the views resourceKind.
   * @return {Object} context.
   */
  getContext: function getContext() {
    return this.inherited(arguments);
  },
  /**
   * Extends the {@link View#beforeTransitionTo parent implementation} by also toggling the visibility of the views
   * components and clearing the view and selection model as needed.
   */
  beforeTransitionTo: function beforeTransitionTo() {
    this.inherited(arguments);

    $(this.domNode).toggleClass('list-hide-search', (this.options && typeof this.options.hideSearch !== 'undefined') ? this.options.hideSearch : this.hideSearch || !this.enableSearch);

    $(this.domNode).toggleClass('list-show-selectors', !this.isSelectionDisabled() && !this.options.singleSelect);

    if (this._selectionModel && !this.isSelectionDisabled()) {
      this._selectionModel.useSingleSelection(this.options.singleSelect);
    }

    if (typeof this.options.enableActions !== 'undefined') {
      this.enableActions = this.options.enableActions;
    }

    $(this.domNode).toggleClass('list-show-actions', this.enableActions);
    if (this.enableActions) {
      this._selectionModel.useSingleSelection(true);
    }

    if (this.refreshRequired) {
      this.clear();
    } else {
      // if enabled, clear any pre-existing selections
      if (this._selectionModel && this.autoClearSelection && !this.enableActions) {
        this._selectionModel.clear();
        this._loadedSelections = {};
      }
    }
  },
  /**
   * Extends the {@link View#transitionTo parent implementation} to also configure the search widget and
   * load previous selections into the selection model.
   */
  transitionTo: function transitionTo() {
    this.configureSearch();

    if (this._selectionModel) {
      this._loadPreviousSelections();
    }

    this.inherited(arguments);
  },
  /**
   * Generates the hash tag layout by taking the hash tags defined in `this.hashTagQueries` and converting them
   * into individual objects in an array to be used in the customization engine.
   * @return {Object[]}
   */
  createHashTagQueryLayout: function createHashTagQueryLayout() {
    // todo: always regenerate this layout? always regenerating allows for all existing customizations
    // to still work, at expense of potential (rare) performance issues if many customizations are registered.
    const layout = [];
    for (const name in this.hashTagQueries) {
      if (this.hashTagQueries.hasOwnProperty(name)) {
        layout.push({
          key: name,
          tag: (this.hashTagQueriesText && this.hashTagQueriesText[name]) || name,
          query: this.hashTagQueries[name],
        });
      }
    }

    return layout;
  },
  /**
   * Called when the view needs to be reset. Invokes the request data process.
   */
  refresh: function refresh() {
    if (this.isRefreshing) {
      return;
    }
    this.createActions(this._createCustomizedLayout(this.createSystemActionLayout(this.createActionLayout()), 'actions'));
    this.isRefreshing = true;
    this.query = this.getSearchQuery() || this.query;
    this.requestData();
  },
  /**
   * Clears the view by:
   *
   *  * clearing the selection model, but without it invoking the event handlers;
   *  * clears the views data such as `this.entries` and `this.entries`;
   *  * clears the search width if passed true; and
   *  * applies the default template.
   *
   * @param {Boolean} all If true, also clear the search widget.
   */
  clear: function clear(all) {
    if (this._selectionModel) {
      this._selectionModel.suspendEvents();
      this._selectionModel.clear();
      this._selectionModel.resumeEvents();
    }

    this._loadedSelections = {};
    this.requestedFirstPage = false;
    this.entries = {};
    this.position = 0;

    if (this._onScrollHandle) {
      this.disconnect(this._onScrollHandle);
      this._onScrollHandle = null;
    }

    if (all === true && this.searchWidget) {
      this.searchWidget.clear();
      this.query = false; // todo: rename to searchQuery
      this.hasSearched = false;
    }

    $(this.domNode).removeClass('list-has-more');

    this.set('listContent', this.loadingTemplate.apply(this));
  },
  search: function search() {
    if (this.searchWidget) {
      this.searchWidget.search();
    }
  },
  /**
   * Sets the query value on the serach widget
   */
  setSearchTerm: function setSearchTerm(value) {
    if (this.searchWidget) {
      this.searchWidget.set('queryValue', value);
    }
  },
  /**
   * Returns a promise with the list's count.
   */
  getListCount: function getListCount(/* options, callback*/) {},
});

export default __class;