This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
espocrm-base/client/src/views/list.js
T
Yuri Kuznetsov a5a6c8a0a1 cleanup
2024-03-19 10:19:45 +02:00

937 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/** @module module:views/list */
import MainView from 'views/main';
import SearchManager from 'search-manager';
/**
* A list view.
*/
class ListView extends MainView {
/** @inheritDoc */
template = 'list'
/** @inheritDoc */
name = 'List'
/** @inheritDoc */
optionsToPass = []
/**
* A header view name.
*
* @type {string}
* @protected
*/
headerView = 'views/header'
/**
* A search view name.
*
* @type {string}
* @protected
*/
searchView = 'views/record/search'
/**
* A record/list view name.
*
* @type {string}
* @protected
*/
recordView = 'views/record/list'
/**
* A record/kanban view name.
*
* @type {string}
* @protected
*/
recordKanbanView = 'views/record/kanban'
/**
* Has a search panel.
*
* @type {boolean}
* @protected
*/
searchPanel = true
/**
* @type {module:search-manager}
* @protected
*/
searchManager = null
/**
* Has a create button.
*
* @type {boolean}
* @protected
*/
createButton = true
/**
* To use a modal dialog when creating a record.
*
* @type {boolean}
* @protected
*/
quickCreate = false
/**
* After create a view will be stored, so it can be re-used after.
* Useful to avoid re-rendering when come back the list view.
*
* @type {boolean}
*/
storeViewAfterCreate = false
/**
* After update a view will be stored, so it can be re-used after.
* Useful to avoid re-rendering when come back the list view.
*
* @type {boolean}
*/
storeViewAfterUpdate = true
/**
* Use a current URL as a root URL when open a record. To be able to return to the same URL.
*/
keepCurrentRootUrl = false
/**
* A view mode. 'list', 'kanban'.
*
* @type {string}
*/
viewMode = ''
/**
* An available view mode list.
*
* @type {string[]|null}
*/
viewModeList = null
/**
* A default view mode.
*
* @type {string}
*/
defaultViewMode = 'list'
/** @const */
MODE_LIST = 'list'
/** @const */
MODE_KANBAN = 'kanban'
/** @inheritDoc */
shortcutKeys = {
/** @this ListView */
'Control+Space': function (e) {
this.handleShortcutKeyCtrlSpace(e);
},
/** @this ListView */
'Control+Slash': function (e) {
this.handleShortcutKeyCtrlSlash(e);
},
/** @this ListView */
'Control+Comma': function (e) {
this.handleShortcutKeyCtrlComma(e);
},
/** @this ListView */
'Control+Period': function (e) {
this.handleShortcutKeyCtrlPeriod(e);
},
}
/** @inheritDoc */
setup() {
this.collection.maxSize = this.getConfig().get('recordsPerPage') || this.collection.maxSize;
this.collectionUrl = this.collection.url;
this.collectionMaxSize = this.collection.maxSize;
/**
* @type {string}
* @protected
*/
this._primaryFilter = this.options.params.primaryFilter;
this._fromAdmin = this.options.params.fromAdmin;
this.setupModes();
this.setViewMode(this.viewMode);
if (this.getMetadata().get(['clientDefs', this.scope, 'searchPanelDisabled'])) {
this.searchPanel = false;
}
if (this.getUser().isPortal()) {
if (this.getMetadata().get(['clientDefs', this.scope, 'searchPanelInPortalDisabled'])) {
this.searchPanel = false;
}
}
if (this.getMetadata().get(['clientDefs', this.scope, 'createDisabled'])) {
this.createButton = false;
}
this.entityType = this.collection.entityType;
this.headerView = this.options.headerView || this.headerView;
this.recordView = this.options.recordView || this.recordView;
this.searchView = this.options.searchView || this.searchView;
this.setupHeader();
this.defaultOrderBy = this.defaultOrderBy || this.collection.orderBy;
this.defaultOrder = this.defaultOrder || this.collection.order;
this.collection.setOrder(this.defaultOrderBy, this.defaultOrder, true);
if (this.searchPanel) {
this.setupSearchManager();
}
this.setupSorting();
if (this.searchPanel) {
this.setupSearchPanel();
}
if (this.createButton) {
this.setupCreateButton();
}
if (this._fromAdmin || this._primaryFilter) {
this.keepCurrentRootUrl = true;
}
}
setupFinal() {
super.setupFinal();
this.wait(
this.getHelper().processSetupHandlers(this, 'list')
);
}
/**
* Set up modes.
*/
setupModes() {
this.defaultViewMode = this.options.defaultViewMode ||
this.getMetadata().get(['clientDefs', this.scope, 'listDefaultViewMode']) ||
this.defaultViewMode;
this.viewMode = this.viewMode || this.defaultViewMode;
const viewModeList = this.options.viewModeList ||
this.viewModeList ||
this.getMetadata().get(['clientDefs', this.scope, 'listViewModeList']);
if (viewModeList) {
this.viewModeList = viewModeList;
}
else {
this.viewModeList = [this.MODE_LIST];
if (this.getMetadata().get(['clientDefs', this.scope, 'kanbanViewMode'])) {
if (!~this.viewModeList.indexOf(this.MODE_KANBAN)) {
this.viewModeList.push(this.MODE_KANBAN);
}
}
}
if (this.viewModeList.length > 1) {
let viewMode = null;
const modeKey = 'listViewMode' + this.scope;
if (this.getStorage().has('state', modeKey)) {
const storedViewMode = this.getStorage().get('state', modeKey);
if (storedViewMode && this.viewModeList.includes(storedViewMode)) {
viewMode = storedViewMode;
}
}
if (!viewMode) {
viewMode = this.defaultViewMode;
}
this.viewMode = /** @type {string} */viewMode;
}
}
/**
* Set up a header.
*/
setupHeader() {
this.createView('header', this.headerView, {
collection: this.collection,
fullSelector: '#main > .page-header',
scope: this.scope,
isXsSingleRow: true,
});
}
/**
* Set up a create button.
*/
setupCreateButton() {
if (this.quickCreate) {
this.menu.buttons.unshift({
action: 'quickCreate',
iconHtml: '<span class="fas fa-plus fa-sm"></span>',
text: this.translate('Create ' + this.scope, 'labels', this.scope),
style: 'default',
acl: 'create',
aclScope: this.entityType || this.scope,
title: 'Ctrl+Space',
});
return;
}
this.menu.buttons.unshift({
link: '#' + this.scope + '/create',
action: 'create',
iconHtml: '<span class="fas fa-plus fa-sm"></span>',
text: this.translate('Create ' + this.scope, 'labels', this.scope),
style: 'default',
acl: 'create',
aclScope: this.entityType || this.scope,
title: 'Ctrl+Space',
});
}
/**
* Set up a search panel.
*
* @protected
*/
setupSearchPanel() {
this.createSearchView();
}
/**
* Create a search view.
*
* @return {Promise<module:view>}
* @protected
*/
createSearchView() {
// noinspection JSValidateTypes
return this.createView('search', this.searchView, {
collection: this.collection,
fullSelector: '#main > .search-container',
searchManager: this.searchManager,
scope: this.scope,
viewMode: this.viewMode,
viewModeList: this.viewModeList,
isWide: true,
disableSavePreset: !!this._primaryFilter,
primaryFiltersDisabled: !!this._primaryFilter,
}, view => {
this.listenTo(view, 'reset', () => this.resetSorting());
if (this.viewModeList.length > 1) {
this.listenTo(view, 'change-view-mode', mode => this.switchViewMode(mode));
}
});
}
/**
* Switch a view mode.
*
* @param {string} mode
*/
switchViewMode(mode) {
this.clearView('list');
this.collection.isFetched = false;
this.collection.reset();
this.applyStoredSorting();
this.setViewMode(mode, true);
this.loadList();
}
/**
* Set a view mode.
*
* @param {string} mode A mode.
* @param {boolean} [toStore=false] To preserve a mode being set.
*/
setViewMode(mode, toStore) {
this.viewMode = mode;
this.collection.url = this.collectionUrl;
this.collection.maxSize = this.collectionMaxSize;
if (toStore) {
const modeKey = 'listViewMode' + this.scope;
this.getStorage().set('state', modeKey, mode);
}
if (this.searchView && this.getView('search')) {
this.getSearchView().setViewMode(mode);
}
if (this.viewMode === this.MODE_KANBAN) {
this.setViewModeKanban();
return;
}
const methodName = 'setViewMode' + Espo.Utils.upperCaseFirst(this.viewMode);
if (this[methodName]) {
this[methodName]();
}
}
/**
* Called when the kanban mode is set.
*/
setViewModeKanban() {
this.collection.url = 'Kanban/' + this.scope;
this.collection.maxSize = this.getConfig().get('recordsPerPageKanban');
this.collection.resetOrderToDefault();
}
/**
* Reset sorting in a storage.
*/
resetSorting() {
this.getStorage().clear('listSorting', this.collection.entityType);
}
/**
* Get default search data.
*
* @returns {Object}
*/
getSearchDefaultData() {
return this.getMetadata().get('clientDefs.' + this.scope + '.defaultFilterData');
}
/**
* Set up a search manager.
*/
setupSearchManager() {
const collection = this.collection;
let key = 'list';
if (this._primaryFilter) {
key += 'Filter' + Espo.Utils.upperCaseFirst(this._primaryFilter);
}
const searchManager = new SearchManager(
collection,
key,
this.getStorage(),
this.getDateTime(),
this.getSearchDefaultData()
);
searchManager.scope = this.scope;
searchManager.loadStored();
if (this._primaryFilter) {
searchManager.clearPreset();
searchManager.setPrimary(this._primaryFilter);
}
collection.where = searchManager.getWhere();
this.searchManager = searchManager;
}
/**
* Set up sorting.
*/
setupSorting() {
if (!this.searchPanel) {
return;
}
this.applyStoredSorting();
}
/**
* Apply stored sorting.
*/
applyStoredSorting() {
const sortingParams = this.getStorage().get('listSorting', this.collection.entityType) || {};
if ('orderBy' in sortingParams) {
this.collection.orderBy = sortingParams.orderBy;
}
if ('order' in sortingParams) {
this.collection.order = sortingParams.order;
}
}
/**
* @protected
* @return {module:views/record/search}
*/
getSearchView() {
return this.getView('search');
}
/**
* @protected
* @return {module:view}
*/
getRecordView() {
return this.getView('list');
}
/**
* Get a record view name.
*
* @returns {string}
*/
getRecordViewName() {
let viewName = this.getMetadata().get(['clientDefs', this.scope, 'recordViews', this.viewMode]);
if (viewName) {
return viewName;
}
if (this.viewMode === this.MODE_LIST) {
return this.recordView;
}
if (this.viewMode === this.MODE_KANBAN) {
return this.recordKanbanView;
}
const propertyName = 'record' + Espo.Utils.upperCaseFirst(this.viewMode) + 'View';
viewName = this[propertyName];
if (!viewName) {
throw new Error("No record view.");
}
return viewName;
}
/** @inheritDoc */
cancelRender() {
if (this.hasView('list')) {
this.getRecordView();
if (this.getRecordView().isBeingRendered()) {
this.getRecordView().cancelRender();
}
}
super.cancelRender();
}
/**
* @inheritDoc
*/
afterRender() {
Espo.Ui.notify(false);
if (!this.hasView('list')) {
this.loadList();
}
// noinspection JSUnresolvedReference
this.$el.get(0).focus({preventScroll: true});
}
/**
* Load a record list view.
*/
loadList() {
if ('isFetched' in this.collection && this.collection.isFetched) {
this.createListRecordView(false);
return;
}
Espo.Ui.notify(' ... ');
this.createListRecordView(true);
}
/**
* Prepare record view options. Options can be modified in an extended method.
*
* @param {Object} options Options
*/
prepareRecordViewOptions(options) {}
/**
* Create a record list view.
*
* @param {boolean} [fetch=false] To fetch after creation.
* @return {Promise<module:views/record/list>}
*/
createListRecordView(fetch) {
const o = {
collection: this.collection,
selector: '.list-container',
scope: this.scope,
skipBuildRows: true,
shortcutKeysEnabled: true,
forceDisplayTopBar: true,
additionalRowActionList: this.getMetadata().get(`clientDefs.${this.scope}.rowActionList`),
settingsEnabled: true,
};
if (this.getHelper().isXsScreen()) {
o.type = 'listSmall';
}
this.optionsToPass.forEach(option => {
o[option] = this.options[option];
});
if (this.keepCurrentRootUrl) {
o.keepCurrentRootUrl = true;
}
if (
this.getConfig().get('listPagination') ||
this.getMetadata().get(['clientDefs', this.scope, 'listPagination'])
) {
o.pagination = true;
}
this.prepareRecordViewOptions(o);
const listViewName = this.getRecordViewName();
// noinspection JSValidateTypes
return this.createView('list', listViewName, o, view => {
if (!this.hasParentView()) {
view.undelegateEvents();
return;
}
this.listenTo(view, 'after:paginate', () => window.scrollTo({top: 0}));
this.listenToOnce(view, 'after:render', () => {
if (!this.hasParentView()) {
view.undelegateEvents();
this.clearView('list');
}
});
if (!fetch) {
Espo.Ui.notify(false);
}
if (this.searchPanel) {
this.listenTo(view, 'sort', obj => {
this.getStorage().set('listSorting', this.collection.entityType, obj);
});
}
if (!fetch) {
view.render();
return;
}
view.getSelectAttributeList(selectAttributeList => {
if (this.options.mediator && this.options.mediator.abort) {
return;
}
if (selectAttributeList) {
this.collection.data.select = selectAttributeList.join(',');
}
Espo.Ui.notify(' ... ');
this.collection.fetch({main: true})
.then(() => Espo.Ui.notify(false));
});
});
}
/**
* @inheritDoc
*/
getHeader() {
const $root = $('<span>')
.text(this.getLanguage().translate(this.scope, 'scopeNamesPlural'));
if (this._fromAdmin) {
const $root = $('<a>')
.attr('href', '#Admin')
.text(this.translate('Administration', 'labels', 'Admin'));
const $scope = $('<span>')
.text(this.getLanguage().translate(this.scope, 'scopeNamesPlural'));
return this.buildHeaderHtml([$root, $scope]);
}
const headerIconHtml = this.getHeaderIconHtml();
if (headerIconHtml) {
$root.prepend(headerIconHtml);
}
if (this._primaryFilter) {
const label = this.translate(this._primaryFilter, 'presetFilters', this.entityType);
$root.append(' · ' + label);
}
return this.buildHeaderHtml([$root]);
}
/**
* @inheritDoc
*/
updatePageTitle() {
this.setPageTitle(this.getLanguage().translate(this.scope, 'scopeNamesPlural'));
}
/**
* Create attributes for an entity being created.
*
* @return {Object}
*/
getCreateAttributes() {}
/**
* Prepare return dispatch parameters to pass to a view when creating a record.
* To pass some data to restore when returning to the list view.
*
* Example:
* ```
* params.options.categoryId = this.currentCategoryId;
* params.options.categoryName = this.currentCategoryName;
* ```
*
* @param {Object} params Parameters to be modified.
*/
prepareCreateReturnDispatchParams(params) {}
/**
* Action `quickCreate`.
*
* @param {Object.<string,*>} [data]
* @returns {Promise<module:views/modals/edit>}
*/
actionQuickCreate(data) {
data = data || {};
const attributes = this.getCreateAttributes() || {};
Espo.Ui.notify(' ... ');
const viewName = this.getMetadata().get('clientDefs.' + this.scope + '.modalViews.edit') ||
'views/modals/edit';
let options = {
scope: this.scope,
attributes: attributes,
};
if (this.keepCurrentRootUrl) {
options.rootUrl = this.getRouter().getCurrentUrl();
}
if (data.focusForCreate) {
options.focusForCreate = true;
}
const returnDispatchParams = {
controller: this.scope,
action: null,
options: {isReturn: true},
};
this.prepareCreateReturnDispatchParams(returnDispatchParams);
options = {
...options,
returnUrl: this.getRouter().getCurrentUrl(),
returnDispatchParams: returnDispatchParams,
};
// noinspection JSValidateTypes
return this.createView('quickCreate', viewName, options, (view) => {
view.render();
view.notify(false);
this.listenToOnce(view, 'after:save', () => {
this.collection.fetch();
});
});
}
/**
* Action 'create'.
*
* @param {Object.<string,*>} [data]
*/
actionCreate(data) {
data = data || {};
const router = this.getRouter();
const url = '#' + this.scope + '/create';
const attributes = this.getCreateAttributes() || {};
let options = {attributes: attributes};
if (this.keepCurrentRootUrl) {
options.rootUrl = this.getRouter().getCurrentUrl();
}
if (data.focusForCreate) {
options.focusForCreate = true;
}
const returnDispatchParams = {
controller: this.scope,
action: null,
options: {isReturn: true},
};
this.prepareCreateReturnDispatchParams(returnDispatchParams);
options = {
...options,
returnUrl: this.getRouter().getCurrentUrl(),
returnDispatchParams: returnDispatchParams,
};
router.navigate(url, {trigger: false});
router.dispatch(this.scope, 'create', options);
}
/**
* Whether the view is actual to be reused.
*
* @returns {boolean}
*/
isActualForReuse() {
return 'isFetched' in this.collection && this.collection.isFetched;
}
/**
* @protected
* @param {JQueryKeyEventObject} e
*/
handleShortcutKeyCtrlSpace(e) {
if (!this.createButton) {
return;
}
/*if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
return;
}*/
if (!this.getAcl().checkScope(this.scope, 'create')) {
return;
}
e.preventDefault();
e.stopPropagation();
if (this.quickCreate) {
this.actionQuickCreate({focusForCreate: true});
return;
}
this.actionCreate({focusForCreate: true});
}
/**
* @protected
* @param {JQueryKeyEventObject} e
*/
handleShortcutKeyCtrlSlash(e) {
if (!this.searchPanel) {
return;
}
const $search = this.$el.find('input.text-filter').first();
if (!$search.length) {
return;
}
e.preventDefault();
e.stopPropagation();
$search.focus();
}
// noinspection JSUnusedLocalSymbols
/**
* @protected
* @param {JQueryKeyEventObject} e
*/
handleShortcutKeyCtrlComma(e) {
if (!this.getSearchView()) {
return;
}
this.getSearchView().selectPreviousPreset();
}
// noinspection JSUnusedLocalSymbols
/**
* @protected
* @param {JQueryKeyEventObject} e
*/
handleShortcutKeyCtrlPeriod(e) {
if (!this.getSearchView()) {
return;
}
this.getSearchView().selectNextPreset();
}
}
export default ListView;