diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 97f2448b11..f97cc9edeb 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -236,7 +236,8 @@ "autocompleteOnEmpty": "Autocomplete on empty input", "relateOnImport": "Relate on Import", "aclScope": "ACL Scope", - "onlyAdmin": "Only for Admin" + "onlyAdmin": "Only for Admin", + "notStorable": "Not Storable" }, "strings" : { "rebuildRequired": "Rebuild is required" diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json index 8f2302bebf..303e1ef8a6 100644 --- a/application/Espo/Resources/i18n/en_US/FieldManager.json +++ b/application/Espo/Resources/i18n/en_US/FieldManager.json @@ -3,7 +3,8 @@ "Dynamic Logic": "Dynamic Logic", "Name": "Name", "Label": "Label", - "Type": "Type" + "Type": "Type", + "View Details": "View Details" }, "options": { "dateTimeDefault": { @@ -170,5 +171,8 @@ "fieldAlreadyExists": "Field '{field}' already exists in '{entityType}'.", "linkWithSameNameAlreadyExists": "Link with the name '{field}' already exists in '{entityType}'.", "namingFieldLinkConflict": "Name '{field}' conflicts with link." + }, + "otherFields": { + "attributes": "Attributes" } } diff --git a/client/res/templates/admin/field-manager/list.tpl b/client/res/templates/admin/field-manager/list.tpl index 7f9519ee51..6c6298e419 100644 --- a/client/res/templates/admin/field-manager/list.tpl +++ b/client/res/templates/admin/field-manager/list.tpl @@ -46,13 +46,22 @@ {{name}} {{translate type category='fieldTypes' scope='Admin'}} - {{#if isCustom}} -
- - +
{{/each}} diff --git a/client/src/views/admin/field-manager/detail-fields/attributes.js b/client/src/views/admin/field-manager/detail-fields/attributes.js new file mode 100644 index 0000000000..f6387e9776 --- /dev/null +++ b/client/src/views/admin/field-manager/detail-fields/attributes.js @@ -0,0 +1,84 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 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 . + * + * 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. + ************************************************************************/ + +import BaseFieldView from 'views/fields/base'; + +export default class FieldAttributesFieldView extends BaseFieldView { + + // language=Handlebars + detailTemplateContent = ` + {{#if dataList.length}} + + + + + + + + + + + {{#each dataList}} + + + + + + + {{/each}} + +
{{translate 'name' category='fields' scope='Admin'}}{{translate 'type' category='fields' scope='Admin'}}{{translate 'notStorable' category='fields' scope='Admin'}}{{translate 'readOnly' category='fields' scope='Admin'}}
{{name}}{{type}} + + + +
+ {{else}} + {{translate 'None'}} + {{/if}} + ` + + data() { + return { + dataList: this.model.attributes.attributes || [], + }; + } +} diff --git a/client/src/views/admin/field-manager/list.js b/client/src/views/admin/field-manager/list.js index ce2688f786..0c6bff68a6 100644 --- a/client/src/views/admin/field-manager/list.js +++ b/client/src/views/admin/field-manager/list.js @@ -27,6 +27,7 @@ ************************************************************************/ import View from 'view'; +import ViewDetailsModalView from 'views/admin/field-manager/modals/view-details'; class FieldManagerListView extends View { @@ -55,6 +56,8 @@ class FieldManagerListView extends View { } setup() { + this.addActionHandler('viewDetails', (e, target) => this.viewDetails(target.dataset.name)); + this.scope = this.options.scope; this.isCustomizable = @@ -200,6 +203,21 @@ class FieldManagerListView extends View { $row.removeClass('hidden'); }); } + + /** + * @private + * @param {string} name + */ + async viewDetails(name) { + const view = new ViewDetailsModalView({ + field: name, + entityType: this.scope, + }); + + await this.assignView('modal', view); + + await view.render(); + } } export default FieldManagerListView; diff --git a/client/src/views/admin/field-manager/modals/view-details.js b/client/src/views/admin/field-manager/modals/view-details.js new file mode 100644 index 0000000000..918e9ec19e --- /dev/null +++ b/client/src/views/admin/field-manager/modals/view-details.js @@ -0,0 +1,433 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 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 . + * + * 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. + ************************************************************************/ + +import ModalView from 'views/modal'; +import Model from 'model'; +import VarcharFieldView from 'views/fields/varchar'; +import FieldAttributesFieldView from 'views/admin/field-manager/detail-fields/attributes'; +import DetailRecordView from 'views/record/detail'; +import BoolFieldView from 'views/fields/bool'; + +export default class ViewDetailsModalView extends ModalView { + + templateContent = ` +
{{{record}}}
+ ` + + backdrop = true + + /** + * @private + * @type {DetailRecordView} + */ + recordView + + /** + * @private + * @type {Model} + */ + formModel + + /** + * @private + * @type {string} + */ + fieldType + + /** + * @private + * @type {{ + * type?: string|undefined, + * notStorable?: boolean, + * } & Record} + */ + fieldDefs + + /** + * @param {{ + * field: string, + * entityType: string, + * }} options + */ + constructor(options) { + super(options); + + this.field = options.field; + this.entityType = options.entityType; + } + + setup() { + this.headerText = + this.translate(this.entityType, 'scopeNames') + ' · ' + + this.translate(this.field, 'fields', this.entityType); + + /** @type {{type: string|undefined}} */ + this.fieldDefs = this.getMetadata().get(`entityDefs.${this.entityType}.fields.${this.field}`) || {}; + + this.fieldType = this.fieldDefs.type; + + this.formModel = new Model({ + name: this.field, + attributes: this.getFieldAttributes(), + readOnly: this.fieldDefs.readOnly || false, + type: this.fieldDefs.type, + }); + + this.recordView = new DetailRecordView({ + model: this.formModel, + detailLayout: [ + { + rows: [ + [ + { + view: new VarcharFieldView({ + name: 'name', + labelText: this.translate('name', 'fields'), + }), + }, + false + ], + [ + { + view: new VarcharFieldView({ + name: 'type', + labelText: this.translate('type', 'fields', 'Admin'), + }), + }, + false + ], + [ + { + view: new BoolFieldView({ + name: 'readOnly', + labelText: this.translate('readOnly', 'fields', 'Admin'), + }), + }, + false + ], + [ + { + view: new FieldAttributesFieldView({ + name: 'attributes', + labelText: this.translate('attributes', 'otherFields', 'FieldManager'), + }), + }, + ], + ] + } + ], + readOnly: true, + sideView: null, + isWide: true, + buttonsDisabled: true, + }); + + this.assignView('record', this.recordView); + + + } + + /** + * @private + * @return {{ + * name: string, + * type: string, + * notStorable: boolean, + * readOnly: boolean, + * }[]} + */ + getFieldAttributes() { + if ( + this.fieldType === 'link' || + this.fieldType === 'linkOne' || + this.fieldType === 'file' || + this.fieldType === 'image' + ) { + return [ + { + name: this.field + 'Id', + type: 'varchar', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Name', + type: 'varchar', + notStorable: true, + readOnly: true, + }, + ] + } + + if (this.fieldType === 'linkParent') { + return [ + { + name: this.field + 'Id', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Type', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Name', + type: 'string', + readOnly: true, + }, + ]; + } + + if ( + this.fieldType === 'linkMultiple' || + this.fieldType === 'attachmentMultiple' + ) { + const attributes = [ + { + name: this.field + 'Ids', + type: 'string[]', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Names', + type: 'Record.', + notStorable: true, + readOnly: this.fieldDefs.readOnly || true, + }, + ]; + + if (this.fieldDefs.columns) { + attributes.push({ + name: this.field + 'Columns', + type: 'Record.>', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }); + } + + return attributes; + } + + if (this.fieldType === 'currency') { + return [ + { + name: this.field, + type: this.fieldDefs.decimal ? 'string' : 'float', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Currency', + type: 'string', + notStorable: this.fieldDefs.notStorable || false || this.fieldDefs.onlyDefaultCurrency, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if (this.fieldType === 'personName') { + return [ + { + name: this.field, + type: 'string', + notStorable: true, + readOnly: true, + }, + { + name: 'salutation' + Espo.Utils.upperCaseFirst(this.field), + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: 'first' + Espo.Utils.upperCaseFirst(this.field), + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: 'last' + Espo.Utils.upperCaseFirst(this.field), + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: 'middle' + Espo.Utils.upperCaseFirst(this.field), + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if (this.fieldType === 'address') { + return [ + { + name: this.field, + type: 'string', + notStorable: true, + readOnly: true, + }, + { + name: this.field + 'Street', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'City', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'Country', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'State', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + { + name: this.field + 'PostalCode', + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if ( + [ + 'varchar', + 'text', + 'wysiwyg', + 'colorpicker', + 'barcode', + 'url', + 'enum', + 'date', + 'datetime', + 'datetimeOptional', + ].includes(this.fieldType) + ) { + return [ + { + name: this.field, + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if (this.fieldType === 'bool') { + return [ + { + name: this.field, + type: 'boolean', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + + if (this.fieldType === 'int' || this.fieldType === 'enumInt') { + return [ + { + name: this.field, + type: 'int', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if (this.fieldType === 'float' || this.fieldType === 'enumFloat') { + return [ + { + name: this.field, + type: 'float', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + if (this.fieldType === 'autoincrement') { + return [ + { + name: this.field, + type: 'int', + notStorable: this.fieldDefs.notStorable || false, + readOnly: true, + }, + ]; + } + + if (this.fieldType === 'number') { + return [ + { + name: this.field, + type: 'string', + notStorable: this.fieldDefs.notStorable || false, + readOnly: true, + }, + ]; + } + + if ( + [ + 'multiEnum', + 'checklist', + 'array', + 'urlMultiple' + ].includes(this.fieldType) + ) { + return [ + { + name: this.field, + type: 'string[]', + notStorable: this.fieldDefs.notStorable || false, + readOnly: this.fieldDefs.readOnly || false, + }, + ]; + } + + return undefined; + } +}