mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-30 07:56:05 +00:00
Merge branch 'fix'
This commit is contained in:
@@ -34,7 +34,6 @@ use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\Email\Util;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<Email>
|
||||
|
||||
@@ -103,7 +103,7 @@ class LogicDefsBc implements AdditionalBuilder
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($dynamicLogic->$key)) {
|
||||
if (!isset($dynamicLogic->$key) || !is_object($dynamicLogic->$key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"storeSentEmails": "Emails will be stored in CRM."
|
||||
},
|
||||
"presetFilters": {
|
||||
"actual": "Actual",
|
||||
"actual": "Active",
|
||||
"complete": "Complete"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,13 @@ class PostgresqlQueryComposer extends BaseQueryComposer
|
||||
$offsetHoursString = substr($offsetHoursString, 1, -1);
|
||||
}
|
||||
|
||||
if (str_contains($offsetHoursString, '.')) {
|
||||
$minutes = (int) (floatval($offsetHoursString) * 60);
|
||||
$minutesString = (string) $minutes;
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL 'MINUTE $minutesString'";
|
||||
}
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL 'HOUR $offsetHoursString'";
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"oneOff": "Check if you are going to use this template only once. E.g. for Mass Email."
|
||||
},
|
||||
"presetFilters": {
|
||||
"actual": "Actual"
|
||||
"actual": "Active"
|
||||
},
|
||||
"placeholderTexts": {
|
||||
"today": "Today's date",
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
"Access": "Access",
|
||||
"Timeout": "Timeout",
|
||||
"No internet": "No internet",
|
||||
"Network error": "Network error",
|
||||
"Are you sure?": "Are you sure?",
|
||||
"Record has been removed": "Record has been removed",
|
||||
"Wrong username/password": "Wrong username/password",
|
||||
|
||||
@@ -86,9 +86,9 @@
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"orderBy": "createdAt",
|
||||
"order": "desc",
|
||||
"textFilterFields": ["name", "subject"]
|
||||
"orderBy": "name",
|
||||
"order": "asc",
|
||||
"textFilterFields": ["name"]
|
||||
},
|
||||
"optimisticConcurrencyControl": true
|
||||
}
|
||||
|
||||
@@ -172,6 +172,8 @@ class ConvertLeadView extends MainView {
|
||||
scopeList.forEach(scope => {
|
||||
const editView = /** @type {import('views/record/edit').default} */this.getView(scope);
|
||||
|
||||
editView.setConfirmLeaveOut(false);
|
||||
|
||||
editView.model.set(editView.fetch());
|
||||
notValid = editView.validate() || notValid;
|
||||
});
|
||||
@@ -196,7 +198,7 @@ class ConvertLeadView extends MainView {
|
||||
this.getRouter().confirmLeaveOut = false;
|
||||
this.getRouter().navigate('#Lead/view/' + this.model.id, {trigger: true});
|
||||
|
||||
this.notify('Converted', 'success');
|
||||
Espo.Ui.notify(this.translate('Converted', 'labels', 'Lead'));
|
||||
})
|
||||
.catch(xhr => {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel panel-default {{#if isStarred}} starred {{~/if}} ">
|
||||
<div class="panel-body">
|
||||
{{#each layoutDataList}}
|
||||
<div>
|
||||
|
||||
@@ -1308,7 +1308,11 @@ class App {
|
||||
let msg = '';
|
||||
|
||||
if (!label) {
|
||||
msg += this.language.translate('Error') + ' ' + xhr.status;
|
||||
if (xhr.status === 0) {
|
||||
msg += this.language.translate('Network error');
|
||||
} else {
|
||||
msg += this.language.translate('Error') + ' ' + xhr.status;
|
||||
}
|
||||
} else {
|
||||
msg += this.language.translate(label);
|
||||
}
|
||||
|
||||
71
client/src/helpers/misc/foreign-field-params.js
Normal file
71
client/src/helpers/misc/foreign-field-params.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/************************************************************************
|
||||
* 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 <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.
|
||||
************************************************************************/
|
||||
|
||||
import {inject} from 'di';
|
||||
import Metadata from 'metadata';
|
||||
|
||||
/**
|
||||
* @since 9.1.1
|
||||
* @internal For future use.
|
||||
*/
|
||||
export default class {
|
||||
|
||||
/**
|
||||
* @type {Metadata}
|
||||
* @private
|
||||
*/
|
||||
@inject(Metadata)
|
||||
metadata
|
||||
|
||||
/**
|
||||
* Get foreign field params.
|
||||
*
|
||||
* @param {string} entityType
|
||||
* @param {string} field
|
||||
* @return {Record|null}
|
||||
*/
|
||||
get(entityType, field) {
|
||||
/** @type {Record|null} */
|
||||
const params = this.metadata.get(`entityDefs.${entityType}.fields.${field}`);
|
||||
|
||||
if (!params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const foreignField = params.field;
|
||||
const link = params.link;
|
||||
|
||||
const foreignEntityType = this.metadata.get(`entityDefs.${entityType}.links.${link}.entity`);
|
||||
|
||||
if (!foreignEntityType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.metadata.get(`entityDefs.${foreignEntityType}.links.${foreignField}`);
|
||||
}
|
||||
}
|
||||
@@ -612,9 +612,11 @@ class Model {
|
||||
|
||||
const success = options.success;
|
||||
|
||||
const collection = this.collection;
|
||||
|
||||
const destroy = () => {
|
||||
this.stopListening();
|
||||
this.trigger('destroy', this, this.collection, options);
|
||||
this.trigger('destroy', this, collection, options);
|
||||
};
|
||||
|
||||
options.success = response => {
|
||||
@@ -633,8 +635,8 @@ class Model {
|
||||
|
||||
this.trigger('sync', this, response, syncOptions);
|
||||
|
||||
if (this.collection) {
|
||||
this.collection.trigger('model-sync', this, syncOptions);
|
||||
if (collection) {
|
||||
collection.trigger('model-sync', this, syncOptions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
/** @module view-helper */
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {marked, Lexer} from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
@@ -64,6 +64,46 @@ class ViewHelper {
|
||||
headerIds: false,
|
||||
});
|
||||
|
||||
// @todo Review after updating the marked library.
|
||||
marked.use({
|
||||
tokenizer: {
|
||||
tag(src) {
|
||||
const cap = Lexer.rules.inline.tag.exec(src);
|
||||
|
||||
if (!cap) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
raw: cap[0],
|
||||
inLink: this.lexer.state.inLink,
|
||||
inRawBlock: this.lexer.state.inRawBlock,
|
||||
text: Handlebars.Utils.escapeExpression(cap[0]),
|
||||
};
|
||||
},
|
||||
html(src) {
|
||||
const cap = Lexer.rules.block.html.exec(src);
|
||||
|
||||
if (!cap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = {
|
||||
type: 'paragraph',
|
||||
raw: cap[0],
|
||||
pre: (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
||||
text: Handlebars.Utils.escapeExpression(cap[0]),
|
||||
};
|
||||
|
||||
token.tokens = [];
|
||||
this.lexer.inline(token.text, token.tokens);
|
||||
|
||||
return token;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
|
||||
if (node instanceof HTMLAnchorElement) {
|
||||
if (node.getAttribute('target')) {
|
||||
@@ -742,9 +782,6 @@ class ViewHelper {
|
||||
transformMarkdownText(text, options) {
|
||||
text = text || '';
|
||||
|
||||
// noinspection RegExpRedundantEscape
|
||||
text = text.replace(/\</g, '\\<');
|
||||
|
||||
this.mdBeforeList.forEach(item => {
|
||||
text = text.replace(item.regex, item.value);
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ class LayoutIndexView extends View {
|
||||
'detail',
|
||||
'listSmall',
|
||||
'detailSmall',
|
||||
'defaultSidePanel',
|
||||
'bottomPanelsDetail',
|
||||
'filters',
|
||||
'massUpdate',
|
||||
@@ -435,10 +436,10 @@ class LayoutIndexView extends View {
|
||||
}
|
||||
|
||||
if (
|
||||
!this.getMetadata().get(['clientDefs', scope, 'defaultSidePanelDisabled']) &&
|
||||
!this.getMetadata().get(['clientDefs', scope, 'defaultSidePanelFieldList'])
|
||||
this.getMetadata().get(['clientDefs', scope, 'defaultSidePanelDisabled']) ||
|
||||
this.getMetadata().get(['clientDefs', scope, 'defaultSidePanelFieldList'])
|
||||
) {
|
||||
typeList.push('defaultSidePanel');
|
||||
typeList = typeList.filter(it => it !== 'defaultSidePanel');
|
||||
}
|
||||
|
||||
if (this.getMetadata().get(['clientDefs', scope, 'kanbanViewMode'])) {
|
||||
|
||||
@@ -101,12 +101,12 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
this.listenTo(this.model, 'change:status', () => this.controlSendButton());
|
||||
|
||||
if (this.model.get('status') !== 'Draft' && this.model.has('isRead') && !this.model.get('isRead')) {
|
||||
this.model.set('isRead', true);
|
||||
this.model.set('isRead', true, {sync: true});
|
||||
}
|
||||
|
||||
this.listenTo(this.model, 'sync', () => {
|
||||
if (!this.model.get('isRead') && this.model.get('status') !== 'Draft') {
|
||||
this.model.set('isRead', true);
|
||||
this.model.set('isRead', true, {sync: true});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -303,13 +303,13 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
actionMarkAsImportant() {
|
||||
Espo.Ajax.postRequest('Email/inbox/important', {id: this.model.id});
|
||||
|
||||
this.model.set('isImportant', true);
|
||||
this.model.set('isImportant', true, {sync: true});
|
||||
}
|
||||
|
||||
actionMarkAsNotImportant() {
|
||||
Espo.Ajax.deleteRequest('Email/inbox/important', {id: this.model.id});
|
||||
|
||||
this.model.set('isImportant', false);
|
||||
this.model.set('isImportant', false, {sync: true});
|
||||
}
|
||||
|
||||
actionMoveToTrash() {
|
||||
@@ -318,9 +318,9 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
});
|
||||
|
||||
if (this.model.attributes.groupFolderId) {
|
||||
this.model.set('groupStatusFolder', 'Trash');
|
||||
this.model.set('groupStatusFolder', 'Trash', {sync: true});
|
||||
} else {
|
||||
this.model.set('inTrash', true);
|
||||
this.model.set('inTrash', true, {sync: true});
|
||||
}
|
||||
|
||||
if (this.model.collection) {
|
||||
@@ -334,10 +334,10 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
Espo.Ui.warning(this.translate('Retrieved from Trash', 'labels', 'Email'));
|
||||
});
|
||||
|
||||
this.model.set('inTrash', false);
|
||||
this.model.set('inTrash', false, {sync: true});
|
||||
|
||||
if (this.model.attributes.groupFolderId) {
|
||||
this.model.set('groupStatusFolder', null);
|
||||
this.model.set('groupStatusFolder', null, {sync: true});
|
||||
}
|
||||
|
||||
if (this.model.collection) {
|
||||
@@ -406,8 +406,8 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
Espo.Ajax.postRequest(`Email/inbox/folders/archive`, {id: this.model.id})
|
||||
.then(() => {
|
||||
this.model.attributes.groupFolderId ?
|
||||
this.model.set('groupStatusFolder', 'Archive') :
|
||||
this.model.set('inArchive', true);
|
||||
this.model.set('groupStatusFolder', 'Archive', {sync: true}) :
|
||||
this.model.set('inArchive', true, {sync: true});
|
||||
|
||||
Espo.Ui.info(this.translate('Moved to Archive', 'labels', 'Email'));
|
||||
|
||||
@@ -576,7 +576,7 @@ class EmailDetailRecordView extends DetailRecordView {
|
||||
actionSend() {
|
||||
this.send()
|
||||
.then(() => {
|
||||
this.model.set('status', 'Sent');
|
||||
this.model.set('status', 'Sent', {sync: true});
|
||||
|
||||
if (this.mode !== this.MODE_DETAIL) {
|
||||
this.setDetailMode();
|
||||
|
||||
@@ -46,6 +46,12 @@ class EmailListRecordView extends ListRecordView {
|
||||
*/
|
||||
toRemoveIdList
|
||||
|
||||
collectionEventSyncList = [
|
||||
'moving-to-trash',
|
||||
'retrieving-from-trash',
|
||||
'moving-to-archive',
|
||||
]
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
|
||||
@@ -469,7 +469,14 @@ class AddressFieldView extends BaseFieldView {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
const actualAttributePartList = this.getMetadata().get(['fields', this.type, 'actualFields']) || [];
|
||||
const actualAttributePartList = this.getMetadata().get(['fields', this.type, 'actualFields']) ||
|
||||
[
|
||||
'street',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'postalCode',
|
||||
];
|
||||
|
||||
this.addressAttributeList = [];
|
||||
this.addressPartList = [];
|
||||
|
||||
@@ -53,7 +53,7 @@ class AssignedUsersFieldView extends LinkMultipleFieldView {
|
||||
getDetailLinkHtml(id, name) {
|
||||
const html = super.getDetailLinkHtml(id);
|
||||
|
||||
const avatarHtml = this.isDetailMode() ?
|
||||
const avatarHtml = this.isDetailMode() || this.isListMode() ?
|
||||
this.getHelper().getAvatarHtml(id, 'small', 18, 'avatar-link') : '';
|
||||
|
||||
if (!avatarHtml) {
|
||||
|
||||
@@ -101,6 +101,12 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
|
||||
searchTypeList = ['isNotEmpty', 'isEmpty']
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Object.<string, true>}
|
||||
*/
|
||||
uploadedIdMap
|
||||
|
||||
events = {
|
||||
/** @this AttachmentMultipleFieldView */
|
||||
'click a.remove-attachment': function (e) {
|
||||
@@ -275,6 +281,8 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
if (this.resizeIsBeingListened) {
|
||||
$(window).off('resize.' + this.cid);
|
||||
}
|
||||
|
||||
this.uploadedIdMap = {};
|
||||
});
|
||||
|
||||
this.on('inline-edit-off', () => {
|
||||
@@ -290,6 +298,8 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
this.uploadFiles(files);
|
||||
});
|
||||
}
|
||||
|
||||
this.uploadedIdMap = {};
|
||||
}
|
||||
|
||||
setupSearch() {
|
||||
@@ -434,6 +444,8 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
nameHash[attachment.id] = attachment.get('name');
|
||||
|
||||
this.model.set(this.nameHashName, nameHash, {ui: ui});
|
||||
|
||||
this.uploadedIdMap[attachment.id] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,9 +460,11 @@ class AttachmentMultipleFieldView extends BaseFieldView {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = (id in this.uploadedIdMap) ? undefined : 'small';
|
||||
|
||||
// noinspection HtmlRequiredAltAttribute,RequiredAttributes
|
||||
return $('<img>')
|
||||
.attr('src', this.getImageUrl(id, 'small'))
|
||||
.attr('src', this.getImageUrl(id, size))
|
||||
.attr('title', name)
|
||||
.attr('alt', name)
|
||||
.attr('draggable', 'false')
|
||||
|
||||
@@ -32,6 +32,13 @@ class ForeignPhoneFieldView extends PhoneFieldView {
|
||||
|
||||
type = 'foreign'
|
||||
readOnly = true
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
// Numeric search does not work for foreign.
|
||||
this.isNumeric = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default ForeignPhoneFieldView;
|
||||
|
||||
@@ -86,6 +86,12 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
*/
|
||||
validationRegExp
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNumeric
|
||||
|
||||
events = {
|
||||
/** @this PhoneFieldView */
|
||||
'click [data-action="switchPhoneProperty"]': function (e) {
|
||||
@@ -696,6 +702,8 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
});
|
||||
|
||||
this.validations.push(() => this.validateMaxCount());
|
||||
|
||||
this.isNumeric = this.getConfig().get('phoneNumberNumericSearch');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -819,9 +827,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
fetchSearch() {
|
||||
const type = this.fetchSearchType() || 'startsWith';
|
||||
|
||||
const isNumeric = this.getConfig().get('phoneNumberNumericSearch');
|
||||
|
||||
const name = isNumeric ?
|
||||
const name = this.isNumeric ?
|
||||
this.name + 'Numeric' :
|
||||
this.name;
|
||||
|
||||
@@ -852,7 +858,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
|
||||
const originalValue = value;
|
||||
|
||||
if (isNumeric && value) {
|
||||
if (this.isNumeric && value) {
|
||||
value = value.replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
|
||||
@@ -145,11 +145,11 @@ class SelectCategoryTreeRecordsModalView extends SelectRecordsModalView {
|
||||
checkAllResultDisabled: true,
|
||||
buttonsDisabled: true,
|
||||
}, listView => {
|
||||
listView.once('select', model => {
|
||||
this.trigger('select', model);
|
||||
listView.once('select', models => {
|
||||
this.trigger('select', models);
|
||||
|
||||
if (this.options.onSelect) {
|
||||
this.options.onSelect([model]);
|
||||
this.options.onSelect(models);
|
||||
}
|
||||
|
||||
this.close();
|
||||
|
||||
@@ -304,7 +304,13 @@ class NotificationBadgeView extends View {
|
||||
}, this.waitInterval * 1000);
|
||||
};
|
||||
|
||||
this.getHelper().webSocketManager.subscribe('newNotification', () => onWebSocketNewNotification());
|
||||
const webSocketManager = this.getHelper().webSocketManager;
|
||||
|
||||
webSocketManager.subscribe('newNotification', () => onWebSocketNewNotification());
|
||||
webSocketManager.subscribeToReconnect(onWebSocketNewNotification);
|
||||
|
||||
this.once('remove', () => webSocketManager.unsubscribe('newNotification'));
|
||||
this.once('remove', () => webSocketManager.unsubscribeFromReconnect(onWebSocketNewNotification));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,7 @@ class KanbanRecordItem extends View {
|
||||
return {
|
||||
layoutDataList: this.layoutDataList,
|
||||
rowActionsDisabled: this.rowActionsDisabled,
|
||||
isStarred: this.hasStars && this.model.attributes.isStarred,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ class KanbanRecordItem extends View {
|
||||
this.itemLayout = this.options.itemLayout;
|
||||
this.rowActionsView = this.options.rowActionsView;
|
||||
this.rowActionsDisabled = this.options.rowActionsDisabled;
|
||||
this.hasStars = this.options.hasStars;
|
||||
|
||||
this.layoutDataList = [];
|
||||
|
||||
|
||||
@@ -433,6 +433,12 @@ class KanbanRecordView extends ListRecordView {
|
||||
this.wait(
|
||||
this.getHelper().processSetupHandlers(this, 'record/kanban')
|
||||
);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.hasStars = this.getMetadata().get(`scopes.${this.entityType}.stars`) || false;
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
@@ -1026,6 +1032,7 @@ class KanbanRecordView extends ListRecordView {
|
||||
moveOverRowAction: this.moveOverRowAction,
|
||||
additionalRowActionList: this._additionalRowActionList,
|
||||
scope: this.scope,
|
||||
hasStars: this.hasStars,
|
||||
}, callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -599,6 +599,13 @@ class ListRecordView extends View {
|
||||
*/
|
||||
_columnResizeHelper
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {string[]}
|
||||
* @since 9.1.1
|
||||
*/
|
||||
collectionEventSyncList
|
||||
|
||||
/** @inheritDoc */
|
||||
events = {
|
||||
/**
|
||||
@@ -1046,6 +1053,12 @@ class ListRecordView extends View {
|
||||
this.headerDisabled = this.options.headerDisabled || this.headerDisabled;
|
||||
this.noDataDisabled = this.options.noDataDisabled || this.noDataDisabled;
|
||||
|
||||
if (!this.collectionEventSyncList) {
|
||||
this.collectionEventSyncList = [];
|
||||
} else {
|
||||
this.collectionEventSyncList = [...this.collectionEventSyncList];
|
||||
}
|
||||
|
||||
if (!this.headerDisabled) {
|
||||
this.header = _.isUndefined(this.options.header) ? this.header : this.options.header;
|
||||
} else {
|
||||
@@ -2177,7 +2190,19 @@ class ListRecordView extends View {
|
||||
model: collection.get(id),
|
||||
};
|
||||
|
||||
if (this.collectionEventSyncList) {
|
||||
this.listenTo(collection, 'all', (event, ...parameters) => {
|
||||
if (this.collectionEventSyncList.includes(event)) {
|
||||
this.collection.trigger(event, ...parameters);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.listenTo(collection, 'model-sync', (/** Model */m, /** Record */o) => {
|
||||
if (o.action === 'destroy') {
|
||||
this.removeRecordFromList(m.id);
|
||||
}
|
||||
|
||||
const model = this.collection.get(m.id);
|
||||
|
||||
if (!model) {
|
||||
|
||||
@@ -62,6 +62,12 @@ class WebSocketManager {
|
||||
*/
|
||||
this.config = config;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Function[]}
|
||||
*/
|
||||
this.subscribeToReconnectQueue = [];
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {{category: string, callback: Function}[]}
|
||||
@@ -171,6 +177,8 @@ class WebSocketManager {
|
||||
* @param {string} url
|
||||
*/
|
||||
connectInternal(auth, userId, url) {
|
||||
let wasConnected = false;
|
||||
|
||||
this.connection = new ab.Session(
|
||||
url,
|
||||
() => {
|
||||
@@ -182,7 +190,13 @@ class WebSocketManager {
|
||||
|
||||
this.subscribeQueue = [];
|
||||
|
||||
if (wasConnected) {
|
||||
this.subscribeToReconnectQueue.forEach(callback => callback());
|
||||
}
|
||||
|
||||
this.schedulePing();
|
||||
|
||||
wasConnected = true;
|
||||
},
|
||||
code => {
|
||||
if (code === ab.CONNECTION_CLOSED) {
|
||||
@@ -204,6 +218,26 @@ class WebSocketManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to reconnecting.
|
||||
*
|
||||
* @param {function(): void} callback A callback.
|
||||
* @since 9.1.1
|
||||
*/
|
||||
subscribeToReconnect(callback) {
|
||||
this.subscribeToReconnectQueue.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from reconnecting.
|
||||
*
|
||||
* @param {function(): void} callback A callback.
|
||||
* @since 9.1.1
|
||||
*/
|
||||
unsubscribeFromReconnect(callback) {
|
||||
this.subscribeToReconnectQueue = this.subscribeToReconnectQueue.filter(it => it !== callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a topic.
|
||||
*
|
||||
@@ -254,11 +288,19 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
this.subscribeQueue = this.subscribeQueue.filter(item => {
|
||||
return item.category !== category && item.callback !== callback;
|
||||
if (callback === undefined) {
|
||||
return item.category !== category;
|
||||
}
|
||||
|
||||
return item.category !== category || item.callback !== callback;
|
||||
});
|
||||
|
||||
this.subscriptions = this.subscriptions.filter(item => {
|
||||
return item.category !== category && item.callback !== callback;
|
||||
if (callback === undefined) {
|
||||
return item.category !== category;
|
||||
}
|
||||
|
||||
return item.category !== category || item.callback !== callback;
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -1411,6 +1411,27 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
.list-kanban {
|
||||
.panel.starred {
|
||||
> .panel-body {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: " ";
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% - var(--8px));
|
||||
height: var(--16px);
|
||||
width: var(--3px);
|
||||
background-color: var(--brand-warning);
|
||||
border-top-right-radius: var(--2px);
|
||||
border-bottom-right-radius: var(--2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list > {
|
||||
table > tbody,
|
||||
ul.list-group {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.1.0",
|
||||
"version": "9.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "9.0.8",
|
||||
"version": "9.1.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.1.0",
|
||||
"version": "9.1.1",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -278,4 +278,73 @@ class PostgresqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testSelectFunctionTz1()
|
||||
{
|
||||
$query = SelectBuilder::create()
|
||||
->from('Comment')
|
||||
->select(
|
||||
Expression::convertTimezone(
|
||||
Expression::column('createdAt'),
|
||||
5.5
|
||||
),
|
||||
'createdAt'
|
||||
)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'MINUTE 330\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testSelectFunctionTz2()
|
||||
{
|
||||
$query = SelectBuilder::create()
|
||||
->from('Comment')
|
||||
->select(
|
||||
Expression::convertTimezone(
|
||||
Expression::column('createdAt'),
|
||||
-5.5
|
||||
),
|
||||
'createdAt'
|
||||
)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'MINUTE -330\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testSelectFunctionTz3()
|
||||
{
|
||||
$query = SelectBuilder::create()
|
||||
->from('Comment')
|
||||
->select(
|
||||
Expression::convertTimezone(
|
||||
Expression::column('createdAt'),
|
||||
5
|
||||
),
|
||||
'createdAt'
|
||||
)
|
||||
->withDeleted()
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeSelect($query);
|
||||
|
||||
$expectedSql =
|
||||
'SELECT "comment"."created_at" + INTERVAL \'HOUR 5\' AS "createdAt" ' .
|
||||
'FROM "comment"';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user