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/fields/link.js
T
2026-01-12 11:02:47 +02:00

1393 lines
40 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-2026 EspoCRM, Inc.
* 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 views/fields/link */
import BaseFieldView from 'views/fields/base';
import RecordModal from 'helpers/record-modal';
import Autocomplete from 'ui/autocomplete';
/**
* A link field (belongs-to relation).
*
* @extends BaseFieldView<module:views/fields/link~params>
*/
class LinkFieldView extends BaseFieldView {
/**
* @typedef {Object} module:views/fields/link~options
* @property {
* module:views/fields/link~params &
* module:views/fields/base~params &
* Record
* } [params] Parameters.
* @property {boolean} [createDisabled] Disable create button in the select modal.
*/
/**
* @typedef {Object} module:views/fields/link~params
* @property {boolean} [required] Required.
* @property {boolean} [autocompleteOnEmpty] Autocomplete on empty input.
* @property {boolean} [createButton] Show 'Create' button.
* @property {string} [entity] An entity type. As of 9.1.0.
*/
/**
* @param {
* module:views/fields/link~options &
* module:views/fields/base~options
* } options Options.
*/
constructor(options) {
super(options);
}
/** @inheritDoc */
type = 'link'
/** @inheritDoc */
listTemplate = 'fields/link/list'
/** @inheritDoc */
detailTemplate = 'fields/link/detail'
/** @inheritDoc */
editTemplate = 'fields/link/edit'
/** @inheritDoc */
searchTemplate = 'fields/link/search'
/**
* A name attribute name.
*
* @type {string}
*/
nameName
/**
* An ID attribute name.
*
* @type {string}
*/
idName
/**
* A foreign entity type.
*
* @type {string|null}
*/
foreignScope = null
/**
* A select-record view.
*
* @protected
* @type {string}
*/
selectRecordsView = 'views/modals/select-records'
/**
* Autocomplete disabled.
*
* @protected
* @type {boolean}
*/
autocompleteDisabled = false
/**
* Create disabled.
*
* @protected
* @type {boolean}
*/
createDisabled = false
/**
* To display the create button.
*
* @protected
* @type {boolean}
*/
createButton = false
/**
* Force create button even is disabled in clientDefs > relationshipPanels.
*
* @protected
* @type {boolean}
*/
forceCreateButton = false
/**
* A search type list.
*
* @protected
* @type {string[]}
*/
searchTypeList = [
'is',
'isEmpty',
'isNotEmpty',
'isNot',
'isOneOf',
'isNotOneOf',
]
/**
* A primary filter list that will be available when selecting a record.
*
* @protected
* @type {string[]|null}
*/
selectFilterList = null
/**
* A select primary filter.
*
* @protected
* @type {string|null}
*/
selectPrimaryFilterName = null
/**
* A select bool filter list.
*
* @protected
* @type {string[]|null}
*/
selectBoolFilterList = null
/**
* An autocomplete max record number.
*
* @protected
* @type {number|null}
*/
autocompleteMaxCount = null
/**
* Select all attributes.
*
* @protected
* @type {boolean}
*/
forceSelectAllAttributes = false
/**
* @protected
* @type {string[]|null}
*/
mandatorySelectAttributeList = null
/**
* Trigger autocomplete on empty input.
*
* @protected
* @type {boolean}
*/
autocompleteOnEmpty = false
/**
* A link element class name. Applicable in the detail mode.
*
* @protected
* @since 9.1.6
*/
linkClass
/**
* @protected
* @type {string}
* @since 9.2.5
*/
foreignNameAttribute
/** @inheritDoc */
events = {
/** @this LinkFieldView */
'auxclick a[href]:not([role="button"])': function (e) {
if (!this.isReadMode()) {
return;
}
const isCombination = e.button === 1 && (e.ctrlKey || e.metaKey);
if (!isCombination) {
return;
}
e.preventDefault();
e.stopPropagation();
this.quickView();
},
}
// noinspection JSCheckFunctionSignatures
/** @inheritDoc */
data() {
let nameValue = this.model.has(this.nameName) ?
this.model.get(this.nameName) :
this.model.get(this.idName);
if (nameValue === null) {
nameValue = this.model.get(this.idName);
}
if (this.isReadMode() && !nameValue && this.model.get(this.idName)) {
nameValue = this.translate(this.foreignScope, 'scopeNames');
}
let iconHtml = null;
if (this.isDetailMode() || this.isListMode()) {
iconHtml = this.getHelper().getScopeColorIconHtml(this.foreignScope);
}
const createButton = this.createButton && (!this.createDisabled || this.forceCreateButton);
// noinspection JSValidateTypes
return {
...super.data(),
idName: this.idName,
nameName: this.nameName,
idValue: this.model.get(this.idName),
nameValue: nameValue,
foreignScope: this.foreignScope,
valueIsSet: this.model.has(this.idName),
iconHtml: iconHtml,
url: this.getUrl(),
createButton: createButton,
linkClass: this.linkClass,
};
}
/**
* @protected
* @return {?string}
*/
getUrl() {
const id = this.model.get(this.idName);
if (!id) {
return null;
}
return '#' + this.foreignScope + '/view/' + id;
}
/**
* Get advanced filters (field filters) to be applied when select a record.
* Can be extended.
*
* @protected
* @return {Object.<string,module:search-manager~advancedFilter>|null}
*/
getSelectFilters() {
return null;
}
/**
* Get a select bool filter list. Applied when select a record.
* Can be extended.
*
* @protected
* @return {string[]|null}
*/
getSelectBoolFilterList() {
return this.selectBoolFilterList;
}
/**
* Get a select primary filter. Applied when select a record.
* Can be extended.
*
* @protected
* @return {string|null}
*/
getSelectPrimaryFilterName() {
return this.selectPrimaryFilterName;
}
/**
* Get a primary filter list that will be available when selecting a record.
* Can be extended.
*
* @return {string[]|null}
*/
getSelectFilterList() {
return this.selectFilterList;
}
/**
* Attributes to pass to a model when creating a new record.
* Can be extended.
*
* @return {Object.<string,*>|null}
*/
getCreateAttributes() {
const attributeMap = this.getMetadata()
.get(['clientDefs', this.entityType, 'relationshipPanels', this.name, 'createAttributeMap']) || {};
const attributes = {};
Object.keys(attributeMap).forEach(attr => attributes[attributeMap[attr]] = this.model.get(attr));
return attributes;
}
/** @inheritDoc */
setup() {
this.nameName = this.name + 'Name';
this.idName = this.name + 'Id';
this.foreignScope = this.options.foreignScope || this.foreignScope;
this.foreignScope =
this.foreignScope ||
this.params.entity ||
this.model.getFieldParam(this.name, 'entity') ||
this.model.getLinkParam(this.name, 'entity');
this.foreignNameAttribute = this.model.getLinkParam(this.name, 'foreignName') ??
this.getMetadata().get(`clientDefs.${this.foreignScope}.nameAttribute`) ??
'name';
if ('createDisabled' in this.options) {
this.createDisabled = this.options.createDisabled;
}
if (!this.isListMode()) {
this.addActionHandler('selectLink', () => this.actionSelect());
this.addActionHandler('clearLink', () => this.clearLink());
}
if (this.isSearchMode()) {
this.addActionHandler('selectLinkOneOf', () => this.actionSelectOneOf());
this.addActionHandler('clearLinkOneOf', (e, target) => this.deleteLinkOneOf(target.dataset.id));
}
this.autocompleteOnEmpty = this.params.autocompleteOnEmpty || this.autocompleteOnEmpty;
this.createButton = this.params.createButton || this.createButton;
if (this.createButton && !this.getAcl().checkScope(this.foreignScope, 'create')) {
this.createButton = false;
}
if (this.createButton) {
this.addActionHandler('createLink', () => this.actionCreateLink());
}
/** @type {Object.<string, *>} */
this.panelDefs = this.getMetadata()
.get(['clientDefs', this.entityType, 'relationshipPanels', this.name]) || {};
if (this.panelDefs.createDisabled) {
this.createDisabled = true;
}
}
/**
* Select.
*
* @param {import('model').default} model A model.
* @protected
* @return {Promise|void}
*/
select(model) {
this.$elementName.val(model.get(this.foreignNameAttribute) || model.id);
this.$elementId.val(model.id);
if (this.mode === this.MODE_SEARCH) {
this.searchData.idValue = model.id;
this.searchData.nameValue = model.get(this.foreignNameAttribute) || model.id;
}
this.trigger('change');
this.controlCreateButtonVisibility();
const attributes = {};
for (const [foreign, field] of Object.entries(this.getDependantForeignMap())) {
attributes[field] = model.get(foreign);
}
this.getSelectFieldHandler().then(async handler => {
this.model.set({
...attributes,
...(await handler.getAttributes(model)),
}, {fromField: this.name});
});
}
/**
* Clear.
*/
clearLink() {
this.$elementName.val('');
this.$elementId.val('');
this.trigger('change');
this.controlCreateButtonVisibility();
for (const [, field] of Object.entries(this.getDependantForeignMap())) {
this.model.unset(field, {fromField: this.name})
}
this.getSelectFieldHandler().then(handler => {
handler.getClearAttributes()
.then(attributes => {
this.model.set(attributes, {fromField: this.name});
});
});
}
/** @private */
controlCreateButtonVisibility() {
if (!this.createButton) {
return;
}
const $btn = this.$el.find('[data-action="createLink"]');
this.model.get(this.idName) ?
$btn.addClass('hidden') :
$btn.removeClass('hidden');
}
/**
* @private
* @return {Promise<{
* getAttributes: function (module:model): Promise<Object.<string, *>>,
* getClearAttributes: function(): Promise<Object.<string, *>>,
* }>}
*/
getSelectFieldHandler() {
if (!this.panelDefs.selectFieldHandler) {
return Promise.resolve({
getClearAttributes: () => Promise.resolve({}),
getAttributes: () => Promise.resolve({}),
});
}
return new Promise(resolve => {
Espo.loader.requirePromise(this.panelDefs.selectFieldHandler)
.then(Handler => {
// Model is passed as of v8.2.
const handler = new Handler(this.getHelper(), this.model);
resolve(handler);
});
});
}
/** @inheritDoc */
setupSearch() {
this.searchData.oneOfIdList = this.getSearchParamsData().oneOfIdList ||
this.searchParams.oneOfIdList || [];
this.searchData.oneOfNameHash = this.getSearchParamsData().oneOfNameHash ||
this.searchParams.oneOfNameHash || {};
if (~['is', 'isNot', 'equals'].indexOf(this.getSearchType())) {
this.searchData.idValue = this.getSearchParamsData().idValue ||
this.searchParams.idValue || this.searchParams.value;
this.searchData.nameValue = this.getSearchParamsData().nameValue ||
this.searchParams.nameValue || this.searchParams.valueName;
}
this.events['change select.search-type'] = e => {
const type = $(e.currentTarget).val();
this.handleSearchType(type);
};
}
/**
* Handle a search type.
*
* @protected
* @param {string} type A type.
*/
handleSearchType(type) {
if (~['is', 'isNot', 'isNotAndIsNotEmpty'].indexOf(type)) {
this.$el.find('div.primary').removeClass('hidden');
}
else {
this.$el.find('div.primary').addClass('hidden');
}
if (~['isOneOf', 'isNotOneOf', 'isNotOneOfAndIsNotEmpty'].indexOf(type)) {
this.$el.find('div.one-of-container').removeClass('hidden');
}
else {
this.$el.find('div.one-of-container').addClass('hidden');
}
}
/**
* Get an autocomplete max record number. Can be extended.
*
* @protected
* @return {number}
*/
getAutocompleteMaxCount() {
if (this.autocompleteMaxCount) {
return this.autocompleteMaxCount;
}
return this.getConfig().get('recordsPerPage');
}
/**
* @private
* @return {string[]}
*/
getMandatorySelectAttributeList() {
const list = this.mandatorySelectAttributeList || this.panelDefs.selectMandatoryAttributeList || []
const map = this.getDependantForeignMap();
return [...list, ...Object.keys(map)];
}
/**
* @private
* @return {Record<string>}
*/
getDependantForeignMap() {
if (this._dependantForeignMap) {
return this._dependantForeignMap;
}
const map = {};
this.model.getFieldList()
.filter(it => {
return this.model.getFieldType(it) === 'foreign' &&
this.model.getFieldParam(it, 'link') === this.name;
})
.forEach(field => {
const foreign = this.model.getFieldParam(field, 'field');
if (!foreign) {
return;
}
map[foreign] = field ;
});
this._dependantForeignMap = map;
return map;
}
// noinspection JSUnusedLocalSymbols
/**
* Compose an autocomplete URL. Can be extended.
*
* @protected
* @param {string} [q] A query.
* @return {string|Promise<string>}
*/
getAutocompleteUrl(q) {
let url = this.foreignScope + '?maxSize=' + this.getAutocompleteMaxCount();
if (!this.forceSelectAllAttributes) {
const mandatorySelectAttributeList = this.getMandatorySelectAttributeList();
let select = ['id', this.foreignNameAttribute];
if (mandatorySelectAttributeList) {
select = select.concat(mandatorySelectAttributeList);
}
url += '&select=' + select.join(',');
}
if (this.panelDefs.selectHandler) {
return new Promise(resolve => {
this._getSelectFilters().then(filters => {
if (filters.bool) {
url += '&' + $.param({'boolFilterList': filters.bool});
}
if (filters.primary) {
url += '&' + $.param({'primaryFilter': filters.primary});
}
if (filters.advanced && Object.keys(filters.advanced).length) {
url += '&' + $.param({'where': filters.advanced});
}
const orderBy = filters.orderBy || this.panelDefs.selectOrderBy;
const orderDirection = filters.orderBy ? filters.order : this.panelDefs.selectOrderDirection;
if (orderBy) {
url += '&' + $.param({
orderBy: orderBy,
order: orderDirection || 'asc',
});
}
resolve(url);
});
});
}
const boolList = [
...(this.getSelectBoolFilterList() || []),
...(this.panelDefs.selectBoolFilterList || []),
];
const primary = this.getSelectPrimaryFilterName() || this.panelDefs.selectPrimaryFilterName;
if (boolList.length) {
url += '&' + $.param({'boolFilterList': boolList});
}
if (primary) {
url += '&' + $.param({'primaryFilter': primary});
}
if (this.panelDefs.selectOrderBy) {
const direction = this.panelDefs.selectOrderDirection || 'asc';
url += '&' + $.param({
orderBy: this.panelDefs.selectOrderBy,
order: direction,
});
}
return url;
}
/** @inheritDoc */
afterRender() {
if (this.isEditMode() || this.isSearchMode()) {
this.$elementId = this.$el.find('input[data-name="' + this.idName + '"]');
this.$elementName = this.$el.find('input[data-name="' + this.nameName + '"]');
this.$elementName.on('change', () => {
if (this.$elementName.val() === '') {
this.clearLink();
}
});
this.$elementName.on('blur', e => {
setTimeout(() => {
if (this.mode === this.MODE_EDIT && this.model.has(this.nameName)) {
e.currentTarget.value = this.model.get(this.nameName) || this.model.get(this.idName);
}
}, 100);
});
const $elementName = this.$elementName;
if (!this.autocompleteDisabled) {
/** @type {module:ajax.AjaxPromise & Promise<any>} */
let lastAjaxPromise;
const autocomplete = new Autocomplete(this.$elementName.get(0), {
name: this.name,
handleFocusMode: 2,
autoSelectFirst: true,
forceHide: true,
triggerSelectOnValidInput: false,
catchFastEnter: true,
onSelect: item => {
this.getModelFactory().create(this.foreignScope, async model => {
model.set(item.attributes);
await (this.select(model) ?? await Promise.resolve());
this.$elementName.focus();
});
},
lookupFunction: query => {
if (!this.autocompleteOnEmpty && query.length === 0) {
const onEmptyPromise = this.getOnEmptyAutocomplete();
if (onEmptyPromise) {
return onEmptyPromise.then(list => this._transformAutocompleteResult({list: list}));
}
return Promise.resolve([]);
}
return Promise.resolve(this.getAutocompleteUrl(query))
.then(url => {
if (lastAjaxPromise && lastAjaxPromise.getReadyState() < 4) {
lastAjaxPromise.abort();
}
lastAjaxPromise = Espo.Ajax.getRequest(url, {q: query});
return lastAjaxPromise;
})
.then(response => this._transformAutocompleteResult(response));
},
});
this.once('render remove', () => autocomplete.dispose());
if (this.isSearchMode()) {
const $elementOneOf = this.$el.find('input.element-one-of');
/** @type {module:ajax.AjaxPromise & Promise<any>} */
let lastAjaxPromise;
const autocomplete = new Autocomplete($elementOneOf.get(0), {
minChars: 1,
focusOnSelect: true,
handleFocusMode: 3,
autoSelectFirst: true,
triggerSelectOnValidInput: false,
forceHide: true,
onSelect: item => {
this.getModelFactory().create(this.foreignScope, model => {
model.set(item.attributes);
this.selectOneOf([model]);
$elementOneOf.val('');
setTimeout(() => $elementOneOf.focus(), 50);
});
},
lookupFunction: query => {
return Promise.resolve(this.getAutocompleteUrl(query))
.then(url => {
if (lastAjaxPromise && lastAjaxPromise.getReadyState() < 4) {
lastAjaxPromise.abort();
}
lastAjaxPromise = Espo.Ajax.getRequest(url, {q: query});
return lastAjaxPromise;
})
.then(/** {list: Record[]} */response => {
return response.list.map(item => ({
value: item.name,
attributes: item,
}));
});
},
});
this.once('render remove', () => autocomplete.dispose());
this.$el.find('select.search-type').on('change', () => {
this.trigger('change');
});
}
}
$elementName.on('change', () => {
if (!this.isSearchMode() && !this.model.get(this.idName)) {
$elementName.val(this.model.get(this.nameName));
}
});
}
if (this.isSearchMode()) {
const type = this.$el.find('select.search-type').val();
this.handleSearchType(type);
if (~['isOneOf', 'isNotOneOf', 'isNotOneOfAndIsNotEmpty'].indexOf(type)) {
this.searchData.oneOfIdList.forEach(id => {
this.addLinkOneOfHtml(id, this.searchData.oneOfNameHash[id]);
});
}
}
}
/**
* @private
*/
_transformAutocompleteResult(response) {
const list = [];
response.list.forEach(item => {
const name = item[this.foreignNameAttribute] || item.name || item.id;
const attributes = item;
if (this.foreignNameAttribute !== 'name') {
attributes[this.foreignNameAttribute] = name;
}
list.push({
id: item.id,
name: name,
data: item.id,
value: name,
attributes: attributes,
});
});
return list;
}
/** @inheritDoc */
getValueForDisplay() {
return this.model.get(this.nameName);
}
/** @inheritDoc */
validateRequired() {
if (this.isRequired()) {
if (this.model.get(this.idName) == null) {
const msg = this.translate('fieldIsRequired', 'messages')
.replace('{field}', this.getLabelText());
this.showValidationMessage(msg);
return true;
}
}
}
/**
* Delete a one-of item. For search mode.
*
* @param {string} id An ID.
*/
deleteLinkOneOf(id) {
this.deleteLinkOneOfHtml(id);
const index = this.searchData.oneOfIdList.indexOf(id);
if (index > -1) {
this.searchData.oneOfIdList.splice(index, 1);
}
delete this.searchData.oneOfNameHash[id];
this.trigger('change');
}
/**
* Add a one-of item. For search mode.
*
* @param {string} id An ID.
* @param {string} name A name.
*/
addLinkOneOf(id, name) {
if (!~this.searchData.oneOfIdList.indexOf(id)) {
this.searchData.oneOfIdList.push(id);
this.searchData.oneOfNameHash[id] = name;
this.addLinkOneOfHtml(id, name);
this.trigger('change');
}
}
/**
* @protected
* @param {string} id An ID.
*/
deleteLinkOneOfHtml(id) {
this.$el.find('.link-one-of-container .link-' + id).remove();
}
/**
* @protected
* @param {string} id An ID.
* @param {string} name A name.
* @return {JQuery}
*/
addLinkOneOfHtml(id, name) {
const $container = this.$el.find('.link-one-of-container');
const $el = $('<div>')
.addClass('link-' + id)
.addClass('list-group-item');
$el.append(
$('<a>')
.attr('role', 'button')
.addClass('pull-right')
.attr('data-id', id)
.attr('data-action', 'clearLinkOneOf')
.append(
$('<span>').addClass('fas fa-times')
),
$('<span>').text(name),
' '
);
$container.append($el);
return $el;
}
/** @inheritDoc */
fetch() {
const data = {};
data[this.nameName] = this.$el.find('[data-name="'+this.nameName+'"]').val() || null;
data[this.idName] = this.$el.find('[data-name="'+this.idName+'"]').val() || null;
return data;
}
/** @inheritDoc */
fetchSearch() {
const type = this.$el.find('select.search-type').val();
const value = this.$el.find('[data-name="' + this.idName + '"]').val();
if (~['isOneOf', 'isNotOneOf'].indexOf(type) && !this.searchData.oneOfIdList.length) {
return {
type: 'isNotNull',
attribute: 'id',
data: {
type: type,
},
};
}
if (type === 'isEmpty') {
return {
type: 'isNull',
attribute: this.idName,
data: {
type: type,
}
};
}
if (type === 'isNotEmpty') {
return {
type: 'isNotNull',
attribute: this.idName,
data: {
type: type,
},
};
}
if (type === 'isOneOf') {
return {
type: 'in',
attribute: this.idName,
value: this.searchData.oneOfIdList,
data: {
type: type,
oneOfIdList: this.searchData.oneOfIdList,
oneOfNameHash: this.searchData.oneOfNameHash,
},
};
}
if (type === 'isNotOneOf') {
return {
type: 'or',
value: [
{
type: 'notIn',
attribute: this.idName,
value: this.searchData.oneOfIdList,
},
{
type: 'isNull',
attribute: this.idName,
},
],
data: {
type: type,
oneOfIdList: this.searchData.oneOfIdList,
oneOfNameHash: this.searchData.oneOfNameHash,
}
};
}
if (type === 'isNotOneOfAndIsNotEmpty') {
return {
type: 'notIn',
attribute: this.idName,
value: this.searchData.oneOfIdList,
data: {
type: type,
oneOfIdList: this.searchData.oneOfIdList,
oneOfNameHash: this.searchData.oneOfNameHash,
},
};
}
if (type === 'isNot') {
if (!value) {
return false;
}
const nameValue = this.$el.find('[data-name="' + this.nameName + '"]').val();
return {
type: 'or',
value: [
{
type: 'notEquals',
attribute: this.idName,
value: value
},
{
type: 'isNull',
attribute: this.idName,
}
],
data: {
type: type,
idValue: value,
nameValue: nameValue,
}
};
}
if (type === 'isNotAndIsNotEmpty') {
if (!value) {
return false;
}
const nameValue = this.$el.find('[data-name="' + this.nameName + '"]').val();
return {
type: 'notEquals',
attribute: this.idName,
value: value,
data: {
type: type,
idValue: value,
nameValue: nameValue,
},
};
}
if (!value) {
return false;
}
const nameValue = this.$el.find('[data-name="' + this.nameName + '"]').val();
return {
type: 'equals',
attribute: this.idName,
value: value,
data: {
type: type,
idValue: value,
nameValue: nameValue,
}
};
}
/** @inheritDoc */
getSearchType() {
return this.getSearchParamsData().type ||
this.searchParams.typeFront ||
this.searchParams.type;
}
/**
* @protected
*/
quickView() {
const id = this.model.get(this.idName);
if (!id) {
return;
}
const entityType = this.foreignScope;
const helper = new RecordModal();
helper.showDetail(this, {
id: id,
entityType: entityType,
});
}
/**
* @protected
* @return {function(): Promise<Object.<string, *>>}
*/
getCreateAttributesProvider() {
return () => {
const attributes = this.getCreateAttributes() || {};
if (!this.panelDefs.createHandler) {
return Promise.resolve(attributes);
}
return new Promise(resolve => {
Espo.loader.requirePromise(this.panelDefs.createHandler)
.then(Handler => new Handler(this.getHelper()))
.then(/** import('handlers/create-related').default */handler => {
handler.getAttributes(this.model, this.name)
.then(additionalAttributes => {
resolve({
...attributes,
...additionalAttributes,
});
});
});
});
};
}
/**
* @protected
*/
async actionSelect() {
const viewName = this.panelDefs.selectModalView ||
this.getMetadata().get(`clientDefs.${this.foreignScope}.modalViews.select`) ||
this.selectRecordsView;
const mandatorySelectAttributeList = this.getMandatorySelectAttributeList();
const createButton = this.isEditMode() && (!this.createDisabled || this.forceCreateButton);
const createAttributesProvider = createButton ?
this.getCreateAttributesProvider() : null;
Espo.Ui.notifyWait();
const filters = await this._getSelectFilters();
const orderBy = filters.orderBy || this.panelDefs.selectOrderBy;
const orderDirection = filters.orderBy ? filters.order : this.panelDefs.selectOrderDirection;
/** @type {module:views/modals/select-records~Options} */
const options = {
entityType: this.foreignScope,
createButton: createButton,
filters: filters.advanced,
boolFilterList: filters.bool,
primaryFilterName: filters.primary,
mandatorySelectAttributeList: mandatorySelectAttributeList,
forceSelectAllAttributes: this.forceSelectAllAttributes,
filterList: this.getSelectFilterList(),
createAttributesProvider: createAttributesProvider,
layoutName: this.panelDefs.selectLayout,
orderBy: orderBy,
orderDirection: orderDirection,
onSelect: models => {
this.select(models[0]);
},
};
const view = await this.createView('modal', viewName, options);
await view.render();
Espo.Ui.notify();
}
/**
* @param {Object} advanced
* @private
*/
_applyAdditionalFilter(advanced) {
const foreignLink = this.model.getLinkParam(this.name, 'foreign');
if (!foreignLink) {
return;
}
if (advanced[foreignLink]) {
return;
}
const linkType = this.model.getLinkParam(this.name, 'type');
const foreignLinkType = this.getMetadata()
.get(['entityDefs', this.foreignScope, 'links', foreignLink, 'type']);
const foreignFieldType = this.getMetadata()
.get(['entityDefs', this.foreignScope, 'fields', foreignLink, 'type']);
if (!foreignFieldType) {
return;
}
const isOneToOne =
(linkType === 'hasOne' || foreignLinkType === 'hasOne') &&
['link', 'linkOne'].includes(foreignFieldType);
if (!isOneToOne) {
return;
}
advanced[foreignLink] = {
type: 'isNull',
attribute: foreignLink + 'Id',
data: {
type: 'isEmpty',
},
};
}
/**
* @private
* @return {Promise<{
* bool?: string[],
* advanced?: Object,
* primary?: string,
* orderBy?: string,
* order?: 'asc'|'desc',
* }>}
*/
_getSelectFilters() {
const handler = this.panelDefs.selectHandler;
const localBoolFilterList = this.getSelectBoolFilterList();
if (!handler || this.isSearchMode()) {
const boolFilterList = (localBoolFilterList || this.panelDefs.selectBoolFilterList) ?
[
...(localBoolFilterList || []),
...(this.panelDefs.selectBoolFilterList || []),
] :
undefined;
const advanced = this.getSelectFilters() || {};
this._applyAdditionalFilter(advanced);
return Promise.resolve({
primary: this.getSelectPrimaryFilterName() || this.panelDefs.selectPrimaryFilterName,
bool: boolFilterList,
advanced: advanced,
});
}
return new Promise(resolve => {
Espo.loader.requirePromise(handler)
.then(Handler => new Handler(this.getHelper()))
.then(/** module:handlers/select-related */handler => {
return handler.getFilters(this.model);
})
.then(filters => {
const advanced = {...(this.getSelectFilters() || {}), ...(filters.advanced || {})};
const primaryFilter = this.getSelectPrimaryFilterName() ||
filters.primary || this.panelDefs.selectPrimaryFilterName;
const boolFilterList = (localBoolFilterList || filters.bool || this.panelDefs.selectBoolFilterList) ?
[
...(localBoolFilterList || []),
...(filters.bool || []),
...(this.panelDefs.selectBoolFilterList || []),
] :
undefined;
this._applyAdditionalFilter(advanced);
const orderBy = filters.orderBy;
const order = orderBy ? filters.order : undefined;
resolve({
bool: boolFilterList,
primary: primaryFilter,
advanced: advanced,
orderBy: orderBy,
order: order,
});
});
});
}
actionSelectOneOf() {
Espo.Ui.notifyWait();
const viewName = this.getMetadata().get(['clientDefs', this.foreignScope, 'modalViews', 'select']) ||
this.selectRecordsView;
this.createView('dialog', viewName, {
scope: this.foreignScope,
createButton: false,
filters: this.getSelectFilters(),
boolFilterList: this.getSelectBoolFilterList(),
primaryFilterName: this.getSelectPrimaryFilterName(),
multiple: true,
layoutName: this.panelDefs.selectLayout,
}, view => {
view.render();
Espo.Ui.notify(false);
this.listenToOnce(view, 'select', models => {
this.clearView('dialog');
if (Object.prototype.toString.call(models) !== '[object Array]') {
models = [models];
}
this.selectOneOf(models);
});
});
}
/**
* Get an empty autocomplete result.
*
* @protected
* @return {Promise<[{name: ?string, id: string} & Record]>}
*/
getOnEmptyAutocomplete() {
return undefined;
}
/**
* @protected
*/
async actionCreateLink() {
const helper = new RecordModal();
const attributes = await this.getCreateAttributesProvider()();
await helper.showCreate(this, {
entityType: this.foreignScope,
fullFormDisabled: true,
attributes: attributes,
afterSave: model => this.select(model),
});
}
/**
* @protected
* @param {module:model[]} models
* @since 8.0.4
*/
selectOneOf(models) {
models.forEach(model => {
this.addLinkOneOf(model.id, model.get(this.foreignNameAttribute));
});
}
}
export default LinkFieldView;