From bc0bcf359444a7083d5a204fc71d47157a8aa4ef Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 16:58:07 +0300 Subject: [PATCH 01/23] fix notify --- client/modules/crm/src/views/lead/convert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/modules/crm/src/views/lead/convert.js b/client/modules/crm/src/views/lead/convert.js index bd01225c86..0614c3444a 100644 --- a/client/modules/crm/src/views/lead/convert.js +++ b/client/modules/crm/src/views/lead/convert.js @@ -196,7 +196,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); From 3fbaeab2646ade1592542f5ae596da7f76693d59 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 17:04:14 +0300 Subject: [PATCH 02/23] fix convert lead leav out confirm --- client/modules/crm/src/views/lead/convert.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/modules/crm/src/views/lead/convert.js b/client/modules/crm/src/views/lead/convert.js index 0614c3444a..32fd2ae689 100644 --- a/client/modules/crm/src/views/lead/convert.js +++ b/client/modules/crm/src/views/lead/convert.js @@ -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; }); From 68f568949e61e1b34386444e1b1dd8d35c4997a5 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 17:45:38 +0300 Subject: [PATCH 03/23] fix list remove sync --- client/src/model.js | 8 +++++--- client/src/views/record/list.js | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/model.js b/client/src/model.js index 9557c975c6..fdb4675664 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -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); } } }; diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 58693dbe10..027e4462f8 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -2178,6 +2178,10 @@ class ListRecordView extends View { }; 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) { From ffbe6f5efae9a79905be67d9dd2e8b1d4a7b7e7a Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 18:11:08 +0300 Subject: [PATCH 04/23] email list detail sync --- client/src/views/email/record/detail.js | 22 +++++++++++----------- client/src/views/email/record/list.js | 6 ++++++ client/src/views/record/list.js | 21 +++++++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/client/src/views/email/record/detail.js b/client/src/views/email/record/detail.js index 60fbb6114f..25cdf1c4b6 100644 --- a/client/src/views/email/record/detail.js +++ b/client/src/views/email/record/detail.js @@ -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(); diff --git a/client/src/views/email/record/list.js b/client/src/views/email/record/list.js index 61a69eba75..52ceac30e2 100644 --- a/client/src/views/email/record/list.js +++ b/client/src/views/email/record/list.js @@ -46,6 +46,12 @@ class EmailListRecordView extends ListRecordView { */ toRemoveIdList + collectionEventSyncList = [ + 'moving-to-trash', + 'retrieving-from-trash', + 'moving-to-archive', + ] + setup() { super.setup(); diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 027e4462f8..96efea1bb9 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -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,6 +2190,14 @@ 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); From aabe966bcf92196cfaed7140efc009c20d036103 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 20:15:53 +0300 Subject: [PATCH 05/23] Netowrk error message --- application/Espo/Resources/i18n/en_US/Global.json | 1 + client/src/app.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index aa754fcb36..cff358af14 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -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", diff --git a/client/src/app.js b/client/src/app.js index 04795e03c5..0599caf2c1 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -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); } From 7ac977ef6ce6f310c113beca9c13ea0523794883 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 21:12:02 +0300 Subject: [PATCH 06/23] change layout order --- client/src/views/admin/layouts/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/views/admin/layouts/index.js b/client/src/views/admin/layouts/index.js index d8b9d09cc1..4ce3a333ef 100644 --- a/client/src/views/admin/layouts/index.js +++ b/client/src/views/admin/layouts/index.js @@ -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'])) { From 9bb1b1a36cbbfe190f7d083db909b6fee7190fc6 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 6 May 2025 23:04:30 +0300 Subject: [PATCH 07/23] cleanup --- application/Espo/Classes/RecordHooks/Email/BeforeSave.php | 1 - 1 file changed, 1 deletion(-) diff --git a/application/Espo/Classes/RecordHooks/Email/BeforeSave.php b/application/Espo/Classes/RecordHooks/Email/BeforeSave.php index 7fabf2301e..d318b60e89 100644 --- a/application/Espo/Classes/RecordHooks/Email/BeforeSave.php +++ b/application/Espo/Classes/RecordHooks/Email/BeforeSave.php @@ -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 From e394779737b833b0d789ec6396a9c041bdaab433 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 7 May 2025 08:58:14 +0300 Subject: [PATCH 08/23] markdown use custom tokenizer to escape html tags --- client/src/view-helper.js | 45 +++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/client/src/view-helper.js b/client/src/view-helper.js index 9595cfcc86..5263942f97 100644 --- a/client/src/view-helper.js +++ b/client/src/view-helper.js @@ -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(/\ { text = text.replace(item.regex, item.value); }); From 73593675695cdb41a42adcf5ee33140bef83dde7 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 7 May 2025 09:17:42 +0300 Subject: [PATCH 09/23] websocket reconnect subscribe --- client/src/views/notification/badge.js | 8 ++++- client/src/web-socket-manager.js | 46 ++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js index 8284317f50..98e712d292 100644 --- a/client/src/views/notification/badge.js +++ b/client/src/views/notification/badge.js @@ -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)); } /** diff --git a/client/src/web-socket-manager.js b/client/src/web-socket-manager.js index 21cd81adb8..1d10d7c9d2 100644 --- a/client/src/web-socket-manager.js +++ b/client/src/web-socket-manager.js @@ -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 { From a7016ca153b97d2e61c5b0bdf3b073bf7b9ec7d6 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 7 May 2025 10:08:18 +0300 Subject: [PATCH 10/23] kanban starred --- client/res/templates/record/kanban-item.tpl | 2 +- client/src/views/record/kanban-item.js | 2 ++ client/src/views/record/kanban.js | 7 +++++++ frontend/less/espo/custom.less | 21 +++++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/client/res/templates/record/kanban-item.tpl b/client/res/templates/record/kanban-item.tpl index ffaeec783a..786689bd0a 100644 --- a/client/res/templates/record/kanban-item.tpl +++ b/client/res/templates/record/kanban-item.tpl @@ -1,4 +1,4 @@ -
+
{{#each layoutDataList}}
diff --git a/client/src/views/record/kanban-item.js b/client/src/views/record/kanban-item.js index 1acb7c96a1..a57a2e48f6 100644 --- a/client/src/views/record/kanban-item.js +++ b/client/src/views/record/kanban-item.js @@ -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 = []; diff --git a/client/src/views/record/kanban.js b/client/src/views/record/kanban.js index 99d18942f6..8f40ee2975 100644 --- a/client/src/views/record/kanban.js +++ b/client/src/views/record/kanban.js @@ -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); } diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index c4b4b1b3ef..b3da135c9d 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -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 { From 6e8e2abd5f8cb6f069b65528f9b93a6ef6e9df7e Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 7 May 2025 11:45:16 +0300 Subject: [PATCH 11/23] type guard --- .../Espo/Core/Utils/Metadata/AdditionalBuilder/LogicDefsBc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Espo/Core/Utils/Metadata/AdditionalBuilder/LogicDefsBc.php b/application/Espo/Core/Utils/Metadata/AdditionalBuilder/LogicDefsBc.php index ff8d9b0ac9..88ec5c0fbe 100644 --- a/application/Espo/Core/Utils/Metadata/AdditionalBuilder/LogicDefsBc.php +++ b/application/Espo/Core/Utils/Metadata/AdditionalBuilder/LogicDefsBc.php @@ -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; } From 469f0e1913c8cc6a7a9c8a1b2ef3efcb04efb31a Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 8 May 2025 10:12:38 +0300 Subject: [PATCH 12/23] assigned users avatars in list mode --- client/src/views/fields/assigned-users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/views/fields/assigned-users.js b/client/src/views/fields/assigned-users.js index 577a6f0e8a..853641bdb2 100644 --- a/client/src/views/fields/assigned-users.js +++ b/client/src/views/fields/assigned-users.js @@ -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) { From 3cf87f8bd988a7c12a64f8c38b691fd3279c2be0 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 9 May 2025 18:16:21 +0300 Subject: [PATCH 13/23] uploaded image preview in full size --- client/src/views/fields/attachment-multiple.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/src/views/fields/attachment-multiple.js b/client/src/views/fields/attachment-multiple.js index a8fb76eeec..21d7c41438 100644 --- a/client/src/views/fields/attachment-multiple.js +++ b/client/src/views/fields/attachment-multiple.js @@ -101,6 +101,12 @@ class AttachmentMultipleFieldView extends BaseFieldView { searchTypeList = ['isNotEmpty', 'isEmpty'] + /** + * @private + * @type {Object.} + */ + 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 $('') - .attr('src', this.getImageUrl(id, 'small')) + .attr('src', this.getImageUrl(id, size)) .attr('title', name) .attr('alt', name) .attr('draggable', 'false') From 3a295bcbad373da57963b9dbc49bb295d70ffa50 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 10 May 2025 10:00:18 +0300 Subject: [PATCH 14/23] fix select record tree item --- client/src/views/modals/select-category-tree-records.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/views/modals/select-category-tree-records.js b/client/src/views/modals/select-category-tree-records.js index 609a3e37d7..d4c773d20b 100644 --- a/client/src/views/modals/select-category-tree-records.js +++ b/client/src/views/modals/select-category-tree-records.js @@ -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(); From 88565e2e19d6068573403671a7f580472a38d438 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 10 May 2025 17:45:46 +0300 Subject: [PATCH 15/23] address field metadata independent --- client/src/views/fields/address.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/src/views/fields/address.js b/client/src/views/fields/address.js index 24cdb9cbea..0330b1d90e 100644 --- a/client/src/views/fields/address.js +++ b/client/src/views/fields/address.js @@ -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 = []; From 4cf6e4f9d39c45d9cb98049182fbad154a845367 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 12:32:29 +0300 Subject: [PATCH 16/23] email template text search only name --- .../Espo/Resources/metadata/entityDefs/EmailTemplate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json b/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json index c04b6a0f6b..18a0357fd4 100644 --- a/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json +++ b/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json @@ -88,7 +88,7 @@ "collection": { "orderBy": "createdAt", "order": "desc", - "textFilterFields": ["name", "subject"] + "textFilterFields": ["name"] }, "optimisticConcurrencyControl": true } From d704d089a3d78ad29d33bf86c6bc7e1f96dc435c Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 12:41:30 +0300 Subject: [PATCH 17/23] lang fix --- .../Espo/Modules/Crm/Resources/i18n/en_US/MassEmail.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/MassEmail.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/MassEmail.json index 7a8d9e9d1d..397dcedf25 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/MassEmail.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/MassEmail.json @@ -52,7 +52,7 @@ "storeSentEmails": "Emails will be stored in CRM." }, "presetFilters": { - "actual": "Actual", + "actual": "Active", "complete": "Complete" } } From 205091dc6ff30edaf050cbdd664a378211e35c48 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 12:41:45 +0300 Subject: [PATCH 18/23] change email template order --- application/Espo/Resources/i18n/en_US/EmailTemplate.json | 2 +- .../Espo/Resources/metadata/entityDefs/EmailTemplate.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/Espo/Resources/i18n/en_US/EmailTemplate.json b/application/Espo/Resources/i18n/en_US/EmailTemplate.json index 23547f7f63..763437ff76 100644 --- a/application/Espo/Resources/i18n/en_US/EmailTemplate.json +++ b/application/Espo/Resources/i18n/en_US/EmailTemplate.json @@ -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", diff --git a/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json b/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json index 18a0357fd4..21546c40d2 100644 --- a/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json +++ b/application/Espo/Resources/metadata/entityDefs/EmailTemplate.json @@ -86,8 +86,8 @@ } }, "collection": { - "orderBy": "createdAt", - "order": "desc", + "orderBy": "name", + "order": "asc", "textFilterFields": ["name"] }, "optimisticConcurrencyControl": true From 1ba4937f328f4b9e2fb579d037089114718ded2d Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 13:44:51 +0300 Subject: [PATCH 19/23] phone foreign disable numeric search --- client/src/views/fields/foreign-phone.js | 6 ++++++ client/src/views/fields/phone.js | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/src/views/fields/foreign-phone.js b/client/src/views/fields/foreign-phone.js index 4cfb10fbd1..4e7e4d0718 100644 --- a/client/src/views/fields/foreign-phone.js +++ b/client/src/views/fields/foreign-phone.js @@ -32,6 +32,12 @@ class ForeignPhoneFieldView extends PhoneFieldView { type = 'foreign' readOnly = true + + setup() { + super.setup(); + + this.isNumeric = false; + } } export default ForeignPhoneFieldView; diff --git a/client/src/views/fields/phone.js b/client/src/views/fields/phone.js index 75dc6f5b42..867e51cd7f 100644 --- a/client/src/views/fields/phone.js +++ b/client/src/views/fields/phone.js @@ -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, ''); } From eae84b9d8f98dcc96e19fe64ad4a451f0a1345af Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 13:45:02 +0300 Subject: [PATCH 20/23] foreign-field params helper --- .../src/helpers/misc/foreign-field-params.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 client/src/helpers/misc/foreign-field-params.js diff --git a/client/src/helpers/misc/foreign-field-params.js b/client/src/helpers/misc/foreign-field-params.js new file mode 100644 index 0000000000..68e4c98f3b --- /dev/null +++ b/client/src/helpers/misc/foreign-field-params.js @@ -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 . + * + * 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}`); + } +} From 400347740d8a0f789f0f3d904737c38c6d9e5d94 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 13:45:47 +0300 Subject: [PATCH 21/23] comment --- client/src/views/fields/foreign-phone.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/views/fields/foreign-phone.js b/client/src/views/fields/foreign-phone.js index 4e7e4d0718..4cdaea3af3 100644 --- a/client/src/views/fields/foreign-phone.js +++ b/client/src/views/fields/foreign-phone.js @@ -36,6 +36,7 @@ class ForeignPhoneFieldView extends PhoneFieldView { setup() { super.setup(); + // Numeric search does not work for foreign. this.isNumeric = false; } } From 1ee853890c5bc0d564b1a49ca58614b1d72e4b90 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sun, 11 May 2025 19:00:35 +0300 Subject: [PATCH 22/23] pgsql TZ fix --- .../QueryComposer/PostgresqlQueryComposer.php | 7 ++ .../Espo/ORM/PostgresqlQueryComposerTest.php | 69 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php b/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php index 3760e52263..9a1d71cf5b 100644 --- a/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php @@ -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'"; } diff --git a/tests/unit/Espo/ORM/PostgresqlQueryComposerTest.php b/tests/unit/Espo/ORM/PostgresqlQueryComposerTest.php index f44833b021..6f9bf7f2e9 100644 --- a/tests/unit/Espo/ORM/PostgresqlQueryComposerTest.php +++ b/tests/unit/Espo/ORM/PostgresqlQueryComposerTest.php @@ -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); + } } From 3d5991495be5dc6dadfdb004f2aba9f40d6e7586 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 12 May 2025 10:13:54 +0300 Subject: [PATCH 23/23] 9.1.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2d343654d..7c9836b3cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 1879c74695..07e9a219ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "espocrm", - "version": "9.1.0", + "version": "9.1.1", "description": "Open-source CRM.", "repository": { "type": "git",