Merge branch 'fix'

This commit is contained in:
Yuri Kuznetsov
2023-11-03 11:42:38 +02:00
8 changed files with 181 additions and 140 deletions
@@ -29,6 +29,7 @@
namespace Espo\Core\Record\Duplicator;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\ORM\Defs;
use Espo\ORM\Defs\FieldDefs;
@@ -44,7 +45,8 @@ class EntityDuplicator
public function __construct(
private Defs $defs,
private FieldDuplicatorFactory $fieldDuplicatorFactory,
private FieldUtil $fieldUtil
private FieldUtil $fieldUtil,
private Metadata $metadata
) {}
public function duplicate(Entity $entity): stdClass
@@ -68,7 +70,7 @@ class EntityDuplicator
$entityType = $entity->getEntityType();
$field = $fieldDefs->getName();
if ($fieldDefs->getParam('duplicateIgnore')) {
if ($this->toIgnoreField($entityType, $fieldDefs)) {
$attributeList = $this->fieldUtil->getAttributeList($entityType, $field);
foreach ($attributeList as $attribute) {
@@ -90,4 +92,20 @@ class EntityDuplicator
$valueMap->$attribute = $value;
}
}
private function toIgnoreField(string $entityType, FieldDefs $fieldDefs): bool
{
$type = $fieldDefs->getType();
// @todo Use FieldType constants.
if (in_array($type, ['autoincrement', 'number'])) {
return true;
}
if ($this->metadata->get(['scopes', $entityType, 'statusField']) === $fieldDefs->getName()) {
return true;
}
return (bool) $fieldDefs->getParam('duplicateIgnore');
}
}
@@ -194,7 +194,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"exportDisabled": true,
"noLoad": true
@@ -466,7 +466,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"noLoad": true
},
@@ -216,7 +216,6 @@
"type": "linkMultiple",
"layoutDetailDisabled": true,
"layoutListDisabled": true,
"layoutMassUpdateDisabled": true,
"importDisabled": true,
"noLoad": true
},
+66 -36
View File
@@ -886,6 +886,7 @@ Espo.Ui = {
* @property {'manual'|'click'|'hover'|'focus'} [trigger='manual'] A trigger type.
* @property {boolean} [noToggleInit=false] Skip init toggle on click.
* @property {boolean} [preventDestroyOnRender=false] Don't destroy on re-render.
* @property {boolean} [noHideOnOutsideClick=false] Don't hide on clicking outside.
* @property {function(): void} [onShow] On-show callback.
* @property {function(): void} [onHide] On-hide callback.
*/
@@ -896,18 +897,18 @@ Espo.Ui = {
* @param {Element|JQuery} element An element.
* @param {Espo.Ui~PopoverOptions} o Options.
* @param {module:view} [view] A view.
* @return {{hide: function(), destroy: function(), show: function(), detach: function()}}
*/
popover: function (element, o, view) {
let $el = $(element);
let $body = $('body')
let content = o.content || Handlebars.Utils.escapeExpression(o.text || '');
const $el = $(element);
const $body = $('body');
const content = o.content || Handlebars.Utils.escapeExpression(o.text || '');
let isShown = false;
let container = o.container;
if (!container) {
let $modalBody = $el.closest('.modal-body');
const $modalBody = $el.closest('.modal-body');
container = $modalBody.length ? $modalBody : 'body';
}
@@ -929,25 +930,27 @@ Espo.Ui = {
return;
}
$body.off('click.popover-' + view.cid);
$body.on('click.popover-' + view.cid, e => {
if ($(e.target).closest('.popover-content').get(0)) {
return;
}
if ($.contains($el.get(0), e.target)) {
return;
}
if ($el.get(0) === e.target) {
return;
}
if (view && !o.noHideOnOutsideClick) {
$body.off('click.popover-' + view.cid);
// noinspection JSUnresolvedReference
$el.popover('hide');
});
$body.on('click.popover-' + view.cid, e => {
if ($(e.target).closest('.popover-content').get(0)) {
return;
}
if ($.contains($el.get(0), e.target)) {
return;
}
if ($el.get(0) === e.target) {
return;
}
$body.off('click.popover-' + view.cid);
// noinspection JSUnresolvedReference
$el.popover('hide');
});
}
if (o.onShow) {
o.onShow();
@@ -968,26 +971,46 @@ Espo.Ui = {
});
}
if (view) {
let hide = () => {
if (!isShown) {
return;
}
let isDetached = false;
// noinspection JSUnresolvedReference
$el.popover('hide');
};
let destroy = () => {
// noinspection JSUnresolvedReference
$el.popover('destroy');
const detach = () => {
if (view) {
$body.off('click.popover-' + view.cid);
view.off('remove', destroy);
view.off('render', destroy);
view.off('render', hide);
};
}
isDetached = true;
};
const destroy = () => {
if (isDetached) {
return;
}
// noinspection JSUnresolvedReference
$el.popover('destroy');
detach();
};
const hide = () => {
if (!isShown) {
return;
}
// noinspection JSUnresolvedReference
$el.popover('hide');
};
const show = () => {
// noinspection JSUnresolvedReference
$el.popover('show');
};
if (view) {
view.once('remove', destroy);
if (!o.preventDestroyOnRender) {
@@ -998,6 +1021,13 @@ Espo.Ui = {
view.on('render', hide);
}
}
return {
hide: () => hide(),
destroy: () => destroy(),
show: () => show(),
detach: () => detach(),
};
},
/**
+44 -44
View File
@@ -74,21 +74,22 @@ const Select = {
* @param {Element|JQuery} element An element.
* @param {module:ui/select~Options} [options] Options.
*/
init: function (element, options) {
let $el = $(element);
init: function (element, options = {}) {
const score = options.score;
const $el = $(element);
options = Select.applyDefaultOptions(options || {});
let plugins = [];
const plugins = [];
Select.loadEspoSelectPlugin();
plugins.push('espo_select');
let itemClasses = {};
const itemClasses = {};
let allowedValues = $el.children().toArray().map(item => {
let value = item.getAttributeNode('value').value;
const allowedValues = $el.children().toArray().map(item => {
const value = item.getAttributeNode('value').value;
if (item.classList) {
itemClasses[value] = item.classList.toString();
@@ -99,13 +100,13 @@ const Select = {
let $relativeParent = null;
let $modalBody = $el.closest('.modal-body');
const $modalBody = $el.closest('.modal-body');
if ($modalBody.length) {
$relativeParent = $modalBody;
}
let selectizeOptions = {
const selectizeOptions = {
sortField: [{field: options.sortBy, direction: options.sortDirection}],
load: options.load,
loadThrottle: 1,
@@ -125,7 +126,7 @@ const Select = {
.get(0).outerHTML;
},
option: function (data) {
let $div = $('<div>')
const $div = $('<div>')
.addClass('option')
.addClass(data.value === '' ? 'selectize-dropdown-emptyoptionlabel' : '')
.addClass(itemClasses[data.value] || '')
@@ -153,7 +154,7 @@ const Select = {
if (!options.matchAnyWord) {
/** @this Selectize */
selectizeOptions.score = function (search) {
let score = this.getScoreFunction(search);
const score = this.getScoreFunction(search);
search = search.toLowerCase();
@@ -170,12 +171,12 @@ const Select = {
if (options.matchAnyWord) {
/** @this Selectize */
selectizeOptions.score = function (search) {
let score = this.getScoreFunction(search);
const score = this.getScoreFunction(search);
search = search.toLowerCase();
return function (item) {
let text = item.text.toLowerCase();
const text = item.text.toLowerCase();
if (
!text.split(' ').find(item => item.startsWith(search)) &&
@@ -190,7 +191,6 @@ const Select = {
}
if (options.score) {
let score = options.score;
selectizeOptions.score = function (search) {
return function (item) {
@@ -209,7 +209,7 @@ const Select = {
* @param {{noTrigger?: boolean}} [options] Options.
*/
focus: function (element, options) {
let $el = $(element);
const $el = $(element);
options = options || {};
@@ -220,7 +220,7 @@ const Select = {
return;
}
let selectize = $el[0].selectize;
const selectize = $el[0].selectize;
if (options.noTrigger) {
selectize.focusNoTrigger = true;
@@ -240,9 +240,9 @@ const Select = {
* @param {{value: string, text: string}[]} options Options.
*/
setOptions: function (element, options) {
let $el = $(element);
const $el = $(element);
let selectize = $el.get(0).selectize;
const selectize = $el.get(0).selectize;
selectize.clearOptions(true);
selectize.load(callback => {
@@ -264,7 +264,7 @@ const Select = {
* @param {string} value A value.
*/
setValue: function ($el, value) {
let selectize = $el.get(0).selectize;
const selectize = $el.get(0).selectize;
selectize.setValue(value, true);
},
@@ -290,14 +290,14 @@ const Select = {
applyDefaultOptions: function (options) {
options = Espo.Utils.clone(options);
let defaults = {
const defaults = {
selectOnTab: false,
matchAnyWord: false,
sortBy: '$order',
sortDirection: 'asc',
};
for (let key in defaults) {
for (const key in defaults) {
if (key in options) {
continue;
}
@@ -320,10 +320,10 @@ const Select = {
const KEY_BACKSPACE = 8;
Selectize.define('espo_select', function () {
let self = this;
const self = this;
this.setup = (function () {
let original = self.setup;
const original = self.setup;
return function () {
original.apply(this, arguments);
@@ -367,7 +367,7 @@ const Select = {
})();*/
this.refreshOptions = (function () {
let original = self.refreshOptions;
const original = self.refreshOptions;
return function () {
if (self.focusNoTrigger) {
@@ -380,7 +380,7 @@ const Select = {
})();
this.blur = (function () {
let original = self.blur;
const original = self.blur;
return function () {
// Prevent closing on mouse down.
@@ -393,7 +393,7 @@ const Select = {
})();
this.close = (function () {
let original = self.close;
const original = self.close;
return function () {
if (self.preventClose) {
@@ -405,7 +405,7 @@ const Select = {
})();
this.onOptionSelect = (function () {
let original = self.onOptionSelect;
const original = self.onOptionSelect;
return function (e) {
if (e.type === 'mousedown' || e.type === 'click') {
@@ -428,10 +428,10 @@ const Select = {
})();
this.open = (function() {
let original = self.open;
const original = self.open;
return function () {
let toProcess = !(self.isLocked || self.isOpen);
const toProcess = !(self.isLocked || self.isOpen);
original.apply(this, arguments);
@@ -439,8 +439,8 @@ const Select = {
return;
}
let $dropdownContent = self.$dropdown.children().first();
let $selected = $dropdownContent.find('.selected');
const $dropdownContent = self.$dropdown.children().first();
const $selected = $dropdownContent.find('.selected');
if (!$selected.length) {
return;
@@ -456,7 +456,7 @@ const Select = {
})();
this.onMouseDown = (function() {
let original = self.onMouseDown;
const original = self.onMouseDown;
return function (e) {
// Prevent flicking when clicking on input.
@@ -473,7 +473,7 @@ const Select = {
})();
this.onFocus = (function() {
let original = self.onFocus;
const original = self.onFocus;
return function (e) {
if (self.preventReOpenOnFocus) {
@@ -504,7 +504,7 @@ const Select = {
};
this.onBlur = (function() {
let original = self.onBlur;
const original = self.onBlur;
return function () {
// Prevent closing on mouse down.
@@ -521,7 +521,7 @@ const Select = {
})();
this.onKeyDown = (function() {
let original = self.onKeyDown;
const original = self.onKeyDown;
return function (e) {
if (IS_MAC ? e.metaKey : e.ctrlKey) {
@@ -558,7 +558,7 @@ const Select = {
RegExp(/^\p{L}/, 'u').test(e.key) // is letter
)
) {
let keyCode = e.keyCode;
const keyCode = e.keyCode;
e.keyCode = KEY_BACKSPACE;
self.deleteSelection(e);
e.keyCode = keyCode;
@@ -578,30 +578,30 @@ const Select = {
};
return function() {
let $control = self.$control;
const $control = self.$control;
let offset = this.settings.dropdownParent === 'body' ?
const offset = this.settings.dropdownParent === 'body' ?
$control.offset() :
$control.position();
offset.top += $control.outerHeight(true);
let dropdownHeight = self.$dropdown.prop('scrollHeight') + 5;
let controlPosTop = self.$control.get(0).getBoundingClientRect().top;
let wrapperHeight = self.$wrapper.height();
const dropdownHeight = self.$dropdown.prop('scrollHeight') + 5;
const controlPosTop = self.$control.get(0).getBoundingClientRect().top;
const wrapperHeight = self.$wrapper.height();
let controlPosBottom = self.$control.get(0).getBoundingClientRect().bottom;
const controlPosBottom = self.$control.get(0).getBoundingClientRect().bottom;
let boundaryTop = !this.settings.$relativeParent ? 0 :
const boundaryTop = !this.settings.$relativeParent ? 0 :
this.settings.$relativeParent.get(0).getBoundingClientRect().top;
let position =
const position =
controlPosTop + dropdownHeight + wrapperHeight > window.innerHeight &&
controlPosBottom - dropdownHeight - wrapperHeight >= boundaryTop ?
POSITION.top :
POSITION.bottom;
let styles = {
const styles = {
width: $control.outerWidth(),
left: offset.left,
};
+29 -36
View File
@@ -1450,8 +1450,10 @@ class BaseFieldView extends View {
*
* @param {string} message A message.
* @param {string|JQuery|Element} [target] A target element or selector.
* @param {module:view} [view] A child view that contains the target. The closest view should to passed.
* Should be omitted if there is no child views or the target is not rendered by a child view.
*/
showValidationMessage(message, target) {
showValidationMessage(message, target, view) {
if (this.validationMessageSuspended) {
return;
}
@@ -1475,7 +1477,7 @@ class BaseFieldView extends View {
}
if ($el.length) {
let rect = $el.get(0).getBoundingClientRect();
const rect = $el.get(0).getBoundingClientRect();
this.lastValidationMessage = message;
@@ -1484,51 +1486,42 @@ class BaseFieldView extends View {
}
}
$el
.popover({
placement: 'bottom',
container: 'body',
content: this.getHelper().transformMarkdownText(message).toString(),
html: true,
trigger: 'manual'
})
.popover('show');
this._popoverMap = this._popoverMap || new WeakMap();
const element = $el.get(0);
let isDestroyed = false;
if (!element) {
return;
}
$el.closest('.field').one('mousedown click', () => {
if (isDestroyed) {
return;
if (this._popoverMap[element]) {
try {
this._popoverMap[element].detach();
}
catch (e) {}
}
$el.popover('destroy');
isDestroyed = true;
});
const popover = Espo.Ui.popover($el, {
placement: 'bottom',
container: 'body',
content: this.getHelper().transformMarkdownText(message).toString(),
trigger: 'manual',
noToggleInit: true,
noHideOnOutsideClick: true,
}, view || this);
this.once('render remove', () => {
if (isDestroyed) {
return;
}
popover.show();
if ($el) {
$el.popover('destroy');
isDestroyed = true;
}
});
this._popoverMap[element] = popover;
$el.closest('.field').one('mousedown click', () => popover.destroy());
this.once('render remove', () => popover.destroy());
if (this._timeout) {
clearTimeout(this._timeout);
}
this._timeout = setTimeout(() => {
if (isDestroyed) {
return;
}
$el.popover('destroy');
isDestroyed = true;
}, this.VALIDATION_POPOVER_TIMEOUT);
this._timeout = setTimeout(() => popover.destroy(), this.VALIDATION_POPOVER_TIMEOUT);
}
/**
+22 -19
View File
@@ -28,6 +28,7 @@
import ModalView from 'views/modal';
import MassActionHelper from 'helpers/mass-action';
import Select from 'ui/select';
class MassUpdateModalView extends ModalView {
@@ -52,7 +53,7 @@ class MassUpdateModalView extends ModalView {
events = {
/** @this MassUpdateModalView */
'click a[data-action="add-field"]': function (e) {
let field = $(e.currentTarget).data('name');
const field = $(e.currentTarget).data('name');
this.addField(field);
},
@@ -86,7 +87,7 @@ class MassUpdateModalView extends ModalView {
this.hasActionMap = {};
let totalCount = this.options.totalCount;
const totalCount = this.options.totalCount;
this.helper = new MassActionHelper(this);
@@ -139,11 +140,11 @@ class MassUpdateModalView extends ModalView {
this.addedFieldList.push(name);
let label = this.getHelper().escapeString(
const label = this.getHelper().escapeString(
this.translate(name, 'fields', this.entityType)
);
let $cell =
const $cell =
$('<div>')
.addClass('cell form-group')
.attr('data-name', name)
@@ -158,7 +159,7 @@ class MassUpdateModalView extends ModalView {
.attr('data-name', name)
);
let $row =
const $row =
$('<div>')
.addClass('item grid-auto-fill-md')
.attr('data-name', name)
@@ -166,13 +167,13 @@ class MassUpdateModalView extends ModalView {
this.$el.find('.fields-container').append($row);
let type = this.model.getFieldType(name);
let viewName = this.model.getFieldParam(name, 'view') || this.getFieldManager().getViewName(type);
const type = this.model.getFieldType(name);
const viewName = this.model.getFieldParam(name, 'view') || this.getFieldManager().getViewName(type);
let actionList = this.getMetadata().get(['entityDefs', this.entityType, name, 'massUpdateActionList']) ||
const actionList = this.getMetadata().get(['entityDefs', this.entityType, name, 'massUpdateActionList']) ||
this.getMetadata().get(['fields', type, 'massUpdateActionList']);
let hasActionDropdown = actionList !== null;
const hasActionDropdown = actionList !== null;
this.hasActionMap[name] = hasActionDropdown;
@@ -192,13 +193,13 @@ class MassUpdateModalView extends ModalView {
});
if (hasActionDropdown) {
let $select =
const $select =
$('<select>')
.addClass('item-action form-control')
.attr('data-name', name);
actionList.forEach(action => {
let label = this.translate(Espo.Utils.upperCaseFirst(action));
const label = this.translate(Espo.Utils.upperCaseFirst(action));
$select.append(
$('<option>')
@@ -207,7 +208,7 @@ class MassUpdateModalView extends ModalView {
);
});
let $cellAction =
const $cellAction =
$('<div>')
.addClass('cell call-action form-group')
.attr('data-name', name)
@@ -224,6 +225,8 @@ class MassUpdateModalView extends ModalView {
);
$row.append($cellAction);
Select.init($select.get(0));
}
}
@@ -239,14 +242,14 @@ class MassUpdateModalView extends ModalView {
actionUpdate() {
this.disableButton('update');
let attributes = {};
let actions = {};
const attributes = {};
const actions = {};
this.addedFieldList.forEach(field => {
let action = this.fetchAction(field);
let itemAttributes = this.getFieldView(field).fetch();
const action = this.fetchAction(field);
const itemAttributes = this.getFieldView(field).fetch();
let itemActualAttributes = {};
const itemActualAttributes = {};
this.getFieldManager()
.getEntityTypeFieldActualAttributeList(this.entityType, field)
@@ -264,7 +267,7 @@ class MassUpdateModalView extends ModalView {
let notValid = false;
this.addedFieldList.forEach(field => {
let view = this.getFieldView(field);
const view = this.getFieldView(field);
notValid = view.validate() || notValid;
});
@@ -328,7 +331,7 @@ class MassUpdateModalView extends ModalView {
return this.ACTION_UPDATE;
}
let $dropdown = this.$el.find('select.item-action[data-name="' + name + '"]');
const $dropdown = this.$el.find('select.item-action[data-name="' + name + '"]');
return $dropdown.val() || this.ACTION_UPDATE;
}