From 22cbfb669cd99cc93c1f150b864e8c2e0ee7c72a Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 10:26:34 +0200 Subject: [PATCH 01/20] fix image resize when width to height is large --- application/Espo/EntryPoints/Image.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/application/Espo/EntryPoints/Image.php b/application/Espo/EntryPoints/Image.php index 8f1d0af583..7eb12269b9 100644 --- a/application/Espo/EntryPoints/Image.php +++ b/application/Espo/EntryPoints/Image.php @@ -267,8 +267,12 @@ class Image implements EntryPoint } } - if ($targetWidth < 1 || $targetHeight < 1) { - throw new RuntimeException("No width or height."); + if ($targetWidth < 1) { + $targetWidth = 1; + } + + if ($targetHeight < 1) { + $targetHeight = 1; } $targetImage = imagecreatetruecolor($targetWidth, $targetHeight); From 2ef02e8144ceb911234cccf1de4ecbe91d2cf005 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 11:40:11 +0200 Subject: [PATCH 02/20] fix warning --- application/Espo/Tools/Notification/RecordService.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/application/Espo/Tools/Notification/RecordService.php b/application/Espo/Tools/Notification/RecordService.php index d670183cc3..769036a39e 100644 --- a/application/Espo/Tools/Notification/RecordService.php +++ b/application/Espo/Tools/Notification/RecordService.php @@ -472,12 +472,15 @@ class RecordService private function prepareSetFields(Notification $entity): void { - if ($entity->getRelated() && $entity->getData()?->relatedName) { - $entity->set('relatedName', $entity->getData()->relatedName); + $relatedName = $entity->getData()->relatedName ?? null; + $createdByName = $entity->getData()->createdByName ?? null; + + if ($entity->getRelated() && $relatedName !== null) { + $entity->set('relatedName', $relatedName); } - if ($entity->getCreatedBy() && $entity->getData()?->createdByName) { - $entity->set('createdByName', $entity->getData()->createdByName); + if ($entity->getCreatedBy() && $createdByName !== null) { + $entity->set('createdByName', $createdByName); } } } From 767a77a66c29dc43ea8f78692afde1d93dbb90aa Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 12:43:52 +0200 Subject: [PATCH 03/20] PDF font mapping --- .../Tools/Pdf/Dompdf/DompdfInitializer.php | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php index 0a3f860ead..9c40e10f53 100644 --- a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php +++ b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php @@ -32,6 +32,7 @@ namespace Espo\Tools\Pdf\Dompdf; use Dompdf\Dompdf; use Dompdf\Options; use Espo\Core\Utils\Config; +use Espo\Core\Utils\Metadata; use Espo\Tools\Pdf\Params; use Espo\Tools\Pdf\Template; @@ -41,8 +42,21 @@ class DompdfInitializer private const PT = 2.83465; + /** @var array */ + private array $standardFontMapping = [ + 'courier' => 'DejaVu Sans Mono', + 'fixed' => 'DejaVu Sans Mono', + 'helvetica' => 'DejaVu Sans', + 'monospace' => 'DejaVu Sans Mono', + 'sans-serif' => 'DejaVu Sans', + 'serif' => 'DejaVu Serif', + 'times' => 'DejaVu Serif', + 'times-roman' => 'DejaVu Serif', + ]; + public function __construct( private Config $config, + private Metadata $metadata, ) {} public function initialize(Template $template, Params $params): Dompdf @@ -54,9 +68,7 @@ class DompdfInitializer $pdf = new Dompdf($options); - if ($params->isPdfA()) { - $this->mapFonts($pdf); - } + $this->mapFonts($pdf, $params->isPdfA()); $size = $template->getPageFormat() === Template::PAGE_FORMAT_CUSTOM ? [0.0, 0.0, $template->getPageWidth() * self::PT, $template->getPageHeight() * self::PT] : @@ -79,18 +91,30 @@ class DompdfInitializer $this->defaultFontFace; } - private function mapFonts(Dompdf $pdf): void + private function mapFonts(Dompdf $pdf, bool $isPdfA): void { - // Fonts are included in PDF/A. Map standard fonts to open source analogues. + // When fonts are included in PDF/A, we need to map standard fonts to open source analogues. + // Also need to support popular fonts specified in CSS styles. $fontMetrics = $pdf->getFontMetrics(); - $fontMetrics->setFontFamily('courier', $fontMetrics->getFamily('DejaVu Sans Mono')); - $fontMetrics->setFontFamily('fixed', $fontMetrics->getFamily('DejaVu Sans Mono')); - $fontMetrics->setFontFamily('helvetica', $fontMetrics->getFamily('DejaVu Sans')); - $fontMetrics->setFontFamily('monospace', $fontMetrics->getFamily('DejaVu Sans Mono')); - $fontMetrics->setFontFamily('sans-serif', $fontMetrics->getFamily('DejaVu Sans')); - $fontMetrics->setFontFamily('serif', $fontMetrics->getFamily('DejaVu Serif')); - $fontMetrics->setFontFamily('times', $fontMetrics->getFamily('DejaVu Serif')); - $fontMetrics->setFontFamily('times-roman', $fontMetrics->getFamily('DejaVu Serif')); + if ($isPdfA) { + foreach ($this->standardFontMapping as $key => $value) { + $fontMetrics->setFontFamily($key, $fontMetrics->getFamily($value)); + } + + return; + } + + /** @var string[] $fontList */ + $fontList = $this->metadata->get('app.pdfEngines.Dompdf.fontFaceList') ?? []; + $fontList = array_map(fn ($it) => strtolower($it), $fontList); + + foreach ($this->standardFontMapping as $key => $value) { + if (in_array(strtolower($key), $fontList)) { + continue; + } + + $fontMetrics->setFontFamily($key, $fontMetrics->getFamily($value)); + } } } From 418770bce50da3e539b53f528ed98d0ea7523a8c Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 12:50:18 +0200 Subject: [PATCH 04/20] pdf font field sorted list --- application/Espo/Resources/metadata/entityDefs/Template.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/Espo/Resources/metadata/entityDefs/Template.json b/application/Espo/Resources/metadata/entityDefs/Template.json index b09102c588..4f74381390 100644 --- a/application/Espo/Resources/metadata/entityDefs/Template.json +++ b/application/Espo/Resources/metadata/entityDefs/Template.json @@ -119,7 +119,8 @@ }, "fontFace": { "type": "enum", - "view": "views/template/fields/font-face" + "view": "views/template/fields/font-face", + "isSorted": true }, "title": { "type": "varchar" From efb2ce9ba30996b7611b2effb58694d1e50d3d24 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 12:54:01 +0200 Subject: [PATCH 05/20] schema --- schema/metadata/entityDefs.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json index c5d0eb7b52..132863863d 100644 --- a/schema/metadata/entityDefs.json +++ b/schema/metadata/entityDefs.json @@ -433,6 +433,27 @@ } } }, + { + "if": { + "properties": { + "type": { + "anyOf": [ + {"const": "enum"}, + {"const": "multiEnum"}, + {"const": "checklist"} + ] + } + } + }, + "then": { + "properties": { + "isSorted": { + "type": "boolean", + "description": "Sort the dropdown options alphabetically." + } + } + } + }, { "if": { "properties": { From 23592e9b66822a7dc019f5270cf4e0fc943ad052 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 17:12:41 +0200 Subject: [PATCH 06/20] open api spec link --- .../Espo/Resources/i18n/en_US/ApiUser.json | 3 +- .../metadata/clientDefs/ApiUser.json | 17 ++++++- .../handlers/api-user/open-api-spec-action.js | 46 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 client/src/handlers/api-user/open-api-spec-action.js diff --git a/application/Espo/Resources/i18n/en_US/ApiUser.json b/application/Espo/Resources/i18n/en_US/ApiUser.json index 58070d2ed1..bc26e0c4a5 100644 --- a/application/Espo/Resources/i18n/en_US/ApiUser.json +++ b/application/Espo/Resources/i18n/en_US/ApiUser.json @@ -1,5 +1,6 @@ { "labels": { - "Create ApiUser": "Create API User" + "Create ApiUser": "Create API User", + "OpenAPI spec": "OpenAPI spec" } } diff --git a/application/Espo/Resources/metadata/clientDefs/ApiUser.json b/application/Espo/Resources/metadata/clientDefs/ApiUser.json index 7f274b0a31..2c8b6c50b6 100644 --- a/application/Espo/Resources/metadata/clientDefs/ApiUser.json +++ b/application/Espo/Resources/metadata/clientDefs/ApiUser.json @@ -30,5 +30,18 @@ }, "filterList": [ ], - "boolFilterList": [] -} \ No newline at end of file + "boolFilterList": [], + "menu": { + "list": { + "dropdown": [ + { + "name": "openApiSpec", + "label": "OpenAPI spec", + "link": "api/v1/OpenApi", + "handler": "handlers/api-user/open-api-spec-action", + "actionFunction": "process" + } + ] + } + } +} diff --git a/client/src/handlers/api-user/open-api-spec-action.js b/client/src/handlers/api-user/open-api-spec-action.js new file mode 100644 index 0000000000..2cad0b69da --- /dev/null +++ b/client/src/handlers/api-user/open-api-spec-action.js @@ -0,0 +1,46 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2026 EspoCRM, Inc. + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * 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 ActionHandler from 'action-handler'; +import {inject} from 'di'; +import Router from 'router'; + +// noinspection JSUnusedGlobalSymbols +export default class extends ActionHandler { + + /** + * @type {Router} + * @private + */ + @inject(Router) + router + + process() { + window.open('api/v1/OpenApi', '_blank'); + } +} From 616ee676222f0bb698507389b9954dad1757b5db Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 20:29:21 +0200 Subject: [PATCH 07/20] pdf: disable js --- application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php index 9c40e10f53..ca45f51c6f 100644 --- a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php +++ b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php @@ -63,8 +63,10 @@ class DompdfInitializer { $options = new Options(); - $options->setIsPdfAEnabled($params->isPdfA()); - $options->setDefaultFont($this->getFontFace($template)); + $options + ->setIsPdfAEnabled($params->isPdfA()) + ->setDefaultFont($this->getFontFace($template)) + ->setIsJavascriptEnabled(false); $pdf = new Dompdf($options); From 3f846e861fa32b354078663ec864b9930ffb0eb2 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 6 Feb 2026 20:49:45 +0200 Subject: [PATCH 08/20] dompdf fontdir --- application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php | 6 ++++++ schema/metadata/app/pdfEngines.json | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php index ca45f51c6f..e6bb7930e7 100644 --- a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php +++ b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php @@ -68,6 +68,12 @@ class DompdfInitializer ->setDefaultFont($this->getFontFace($template)) ->setIsJavascriptEnabled(false); + $fontDir = $this->metadata->get('app.pdfEngines.Dompdf.additionalParams.fontDir'); + + if ($fontDir) { + $options->setFontDir($fontDir); + } + $pdf = new Dompdf($options); $this->mapFonts($pdf, $params->isPdfA()); diff --git a/schema/metadata/app/pdfEngines.json b/schema/metadata/app/pdfEngines.json index c64c0d4451..d433c65cd3 100644 --- a/schema/metadata/app/pdfEngines.json +++ b/schema/metadata/app/pdfEngines.json @@ -33,6 +33,11 @@ } ] } + }, + "additionalParams": { + "type": "object", + "additionalProperties": true, + "description": "Additional engine-wise parameters." } } } From 7728f4765b76b177e277e4ee91b9a93ee26a5f97 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sat, 7 Feb 2026 20:32:19 +0200 Subject: [PATCH 09/20] reset count disabled to default --- application/Espo/Tools/EntityManager/EntityManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/application/Espo/Tools/EntityManager/EntityManager.php b/application/Espo/Tools/EntityManager/EntityManager.php index eb5fb4ae79..61b3be0362 100644 --- a/application/Espo/Tools/EntityManager/EntityManager.php +++ b/application/Espo/Tools/EntityManager/EntityManager.php @@ -724,6 +724,7 @@ class EntityManager 'collection.order', 'collection.textFilterFields', 'collection.fullTextSearch', + 'collection.countDisabled', ]); foreach ($this->getAdditionalParamLocationMap($name) as $it) { From 19e5c483fab302585ac1c5b9a8c3be9f78427e13 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sun, 8 Feb 2026 10:19:02 +0200 Subject: [PATCH 10/20] link field icon helper --- client/src/helpers/field/link-icon.js | 103 ++++++++++++++++++++++++++ frontend/less/espo/custom.less | 49 ++++++++---- 2 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 client/src/helpers/field/link-icon.js diff --git a/client/src/helpers/field/link-icon.js b/client/src/helpers/field/link-icon.js new file mode 100644 index 0000000000..564c019fc2 --- /dev/null +++ b/client/src/helpers/field/link-icon.js @@ -0,0 +1,103 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2026 EspoCRM, Inc. + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * 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. + ************************************************************************/ + +/** + * @since 9.3.1 + */ +export default class LinkFieldIconHelper { + + /** + * @param {import('views/fields/link').default} view + * @param {{ + * iconClass: string, + * getColor: function(): string, + * }} options + */ + constructor(view, options) { + this.view = view; + this.options = options; + + view.listenTo(view, 'after:render', () => { + if (view.isEditMode()) { + this.control(); + } + }); + + view.addHandler('keydown', `input[data-name="${view.nameName}"]`, (/** KeyboardEvent */e, target) => { + if (e.code === 'Enter') { + return; + } + + target.classList.add('being-typed'); + }); + + view.addHandler('change', `input[data-name="${view.nameName}"]`, (e, target) => { + setTimeout(() => target.classList.remove('being-typed'), 200); + }); + + view.addHandler('blur', `input[data-name="${view.nameName}"]`, (e, target) => { + target.classList.remove('being-typed'); + }); + + view.on('change', () => { + if (!view.isEditMode()) { + return; + } + + const span = view.element.querySelector('span.icon-in-input'); + + if (span) { + span.parentNode.removeChild(span); + } + + setTimeout(() => this.control(), 0); + }); + } + + /** + * @private + */ + control() { + const view = this.view; + + const nameElement = view.element.querySelector(`input[data-name="${view.nameName}"]`); + nameElement.classList.remove('being-typed'); + + const icon = document.createElement('span'); + icon.className = 'icon-in-input ' + this.options.iconClass; + icon.style.color = this.options.getColor(); + + const input = view.element.querySelector('.input-group > input'); + + if (!input) { + return; + } + + input.after(icon); + } +} diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index b2f8a0c692..a9a1eee884 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -2364,24 +2364,41 @@ td.cell { } } -div:has(.avatar-in-input):not(:has(.being-typed)) { - > .input-group > input { - padding-left: var(--33px); +div:has(.avatar-in-input), +div:has(.icon-in-input) { + &:not(:has(.being-typed)) { + > .input-group > input { + padding-left: var(--33px); + } + + &:has(.color-icon) { + > .input-group > input { + padding-left: var(--21px); + } + } + + > .icon-in-input, + > .avatar-in-input { + display: inline-block; + position: absolute; + z-index: 3; + user-select: none; + pointer-events: none; + } + + > .avatar-in-input { + top: var(--9px); + left: var(--10px); + } + + > .icon-in-input { + top: var(--11px); + left: var(--10px); + } } - > .avatar-in-input { - display: inline-block; - position: absolute; - top: var(--9px); - left: var(--10px); - z-index: 3; - user-select: none; - pointer-events: none; - } -} - -div:has(.avatar-in-input) { - > .avatar-in-input { + > .avatar-in-input, + > .icon-in-input { display: none; } } From d0d7c8b36eb8f16e1e790113562b98ac4bcefd2b Mon Sep 17 00:00:00 2001 From: Yurii Date: Sun, 8 Feb 2026 11:01:59 +0200 Subject: [PATCH 11/20] link icon helper dynamic icon --- client/src/helpers/field/link-icon.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/helpers/field/link-icon.js b/client/src/helpers/field/link-icon.js index 564c019fc2..fdabf45c13 100644 --- a/client/src/helpers/field/link-icon.js +++ b/client/src/helpers/field/link-icon.js @@ -35,6 +35,7 @@ export default class LinkFieldIconHelper { * @param {import('views/fields/link').default} view * @param {{ * iconClass: string, + * getIconClass: function(): string|null, * getColor: function(): string, * }} options */ @@ -92,6 +93,14 @@ export default class LinkFieldIconHelper { icon.className = 'icon-in-input ' + this.options.iconClass; icon.style.color = this.options.getColor(); + const iconClass = this.options.getIconClass(); + + if (!iconClass) { + return; + } + + icon.className += ' ' + iconClass; + const input = view.element.querySelector('.input-group > input'); if (!input) { From e20c89d4cec1118add02a6bf05a8b36e21a44ab6 Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 9 Feb 2026 11:50:06 +0200 Subject: [PATCH 12/20] detail modal update source collection event --- client/src/views/modals/detail.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/client/src/views/modals/detail.js b/client/src/views/modals/detail.js index 5b1515649f..330c2a5775 100644 --- a/client/src/views/modals/detail.js +++ b/client/src/views/modals/detail.js @@ -223,8 +223,7 @@ class DetailModalView extends ModalView { return; } - this.model = this.sourceModel.clone(); - this.model.collection = this.sourceModel.collection.clone(); + this.cloneSourceModel(); this.setupAfterModelCreated(); @@ -259,6 +258,23 @@ class DetailModalView extends ModalView { } } + /** + * @private + */ + cloneSourceModel() { + this.model = this.sourceModel.clone(); + + const sourceCollection = this.sourceModel.collection; + + if (!sourceCollection) { + return; + } + + this.model.collection = sourceCollection.clone(); + + this.listenTo(this.model.collection, 'update-source', () => sourceCollection.fetch()) + } + /** * Additional setup with the model ready. * @@ -502,7 +518,7 @@ class DetailModalView extends ModalView { if (nextButtonEnabled) { this.enableButton('next'); } else { - this.disableButton('next'); + this.disableButton('next'); } } @@ -512,6 +528,7 @@ class DetailModalView extends ModalView { } const previousModel = this.model; + const previousCollection = this.model.collection; this.sourceModel = this.model.collection.at(indexOfRecord); @@ -524,12 +541,15 @@ class DetailModalView extends ModalView { this.id = this.sourceModel.id; this.scope = this.sourceModel.entityType; - this.model = this.sourceModel.clone(); - this.model.collection = this.sourceModel.collection.clone(); + this.cloneSourceModel(); this.stopListening(previousModel, 'change'); this.stopListening(previousModel, 'sync'); + if (previousCollection) { + this.stopListening(previousCollection, 'update-source'); + } + this.listenTo(this.model, 'change', () => { this.sourceModel.set(this.model.getClonedAttributes()); }); From 95700564bb0072755e5d60b2566f41b44bfc2864 Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 9 Feb 2026 18:08:39 +0200 Subject: [PATCH 13/20] dompdf use cache dir --- .../Tools/Pdf/Dompdf/DompdfInitializer.php | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php index e6bb7930e7..6f7b0e8c6e 100644 --- a/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php +++ b/application/Espo/Tools/Pdf/Dompdf/DompdfInitializer.php @@ -30,8 +30,10 @@ namespace Espo\Tools\Pdf\Dompdf; use Dompdf\Dompdf; +use Dompdf\FontMetrics; use Dompdf\Options; use Espo\Core\Utils\Config; +use Espo\Core\Utils\File\Manager as FileManager; use Espo\Core\Utils\Metadata; use Espo\Tools\Pdf\Params; use Espo\Tools\Pdf\Template; @@ -39,6 +41,8 @@ use Espo\Tools\Pdf\Template; class DompdfInitializer { private string $defaultFontFace = 'DejaVu Sans'; + private string $cacheDir = 'data/cache/application/dompdf'; + private string $pdfaCacheDir = 'data/cache/application/pdfa-dompdf'; private const PT = 2.83465; @@ -57,6 +61,7 @@ class DompdfInitializer public function __construct( private Config $config, private Metadata $metadata, + private FileManager $fileManager, ) {} public function initialize(Template $template, Params $params): Dompdf @@ -68,15 +73,18 @@ class DompdfInitializer ->setDefaultFont($this->getFontFace($template)) ->setIsJavascriptEnabled(false); - $fontDir = $this->metadata->get('app.pdfEngines.Dompdf.additionalParams.fontDir'); + $dir = $params->isPdfA() ? $this->pdfaCacheDir : $this->cacheDir; - if ($fontDir) { - $options->setFontDir($fontDir); + $options->setFontDir($dir); + $options->setFontCache($dir); + + if (!$this->fileManager->isDir($dir)) { + $this->fileManager->mkdir($dir); } $pdf = new Dompdf($options); - $this->mapFonts($pdf, $params->isPdfA()); + $this->mapFonts($pdf, $params->isPdfA(), $dir); $size = $template->getPageFormat() === Template::PAGE_FORMAT_CUSTOM ? [0.0, 0.0, $template->getPageWidth() * self::PT, $template->getPageHeight() * self::PT] : @@ -99,8 +107,14 @@ class DompdfInitializer $this->defaultFontFace; } - private function mapFonts(Dompdf $pdf, bool $isPdfA): void + private function mapFonts(Dompdf $pdf, bool $isPdfA, string $dir): void { + $file = $dir . '/' . FontMetrics::USER_FONTS_FILE; + + if ($this->fileManager->exists($file)) { + return; + } + // When fonts are included in PDF/A, we need to map standard fonts to open source analogues. // Also need to support popular fonts specified in CSS styles. $fontMetrics = $pdf->getFontMetrics(); From cc8fa5a6b687f8c56b288d120a86db95376988e2 Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 9 Feb 2026 19:05:19 +0200 Subject: [PATCH 14/20] load columns only field in list --- .../Espo/Core/FieldProcessing/LinkMultiple/ListLoader.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/Espo/Core/FieldProcessing/LinkMultiple/ListLoader.php b/application/Espo/Core/FieldProcessing/LinkMultiple/ListLoader.php index 126a4c86cf..bab9a048d8 100644 --- a/application/Espo/Core/FieldProcessing/LinkMultiple/ListLoader.php +++ b/application/Espo/Core/FieldProcessing/LinkMultiple/ListLoader.php @@ -65,7 +65,11 @@ class ListLoader implements LoaderInterface foreach ($this->getFieldList($entityType) as $field) { if ( !in_array($field . 'Ids', $select) && - !in_array($field . 'Names', $select) + !in_array($field . 'Names', $select) && + ( + !$entity->hasAttribute($field . 'Columns') || + !in_array($field . 'Columns', $select) + ) ) { continue; } From b86aca9bff54410f0390e8b80f9a1a1129e6ddfb Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 9 Feb 2026 19:41:19 +0200 Subject: [PATCH 15/20] workking time exception ux impr --- .../Calendars/OnlyIfNoUsers.php | 55 +++++++++++++++++++ .../Calendars/RequiredIfNoUsers.php | 52 ++++++++++++++++++ .../Users/OnlyIfNoCalendars.php | 55 +++++++++++++++++++ .../Espo/Entities/WorkingTimeRange.php | 9 +++ .../layouts/WorkingTimeRange/detail.json | 6 +- .../layouts/WorkingTimeRange/detailSmall.json | 6 +- .../metadata/entityDefs/WorkingTimeRange.json | 12 +++- .../metadata/logicDefs/WorkingTimeRange.json | 32 ++++++++++- 8 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/OnlyIfNoUsers.php create mode 100644 application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/RequiredIfNoUsers.php create mode 100644 application/Espo/Classes/FieldValidators/WorkingTimeRange/Users/OnlyIfNoCalendars.php diff --git a/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/OnlyIfNoUsers.php b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/OnlyIfNoUsers.php new file mode 100644 index 0000000000..746fa83a86 --- /dev/null +++ b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/OnlyIfNoUsers.php @@ -0,0 +1,55 @@ +. + * + * 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. + ************************************************************************/ + +namespace Espo\Classes\FieldValidators\WorkingTimeRange\Calendars; + +use Espo\Core\FieldValidation\Validator; +use Espo\Core\FieldValidation\Validator\Data; +use Espo\Core\FieldValidation\Validator\Failure; +use Espo\Entities\WorkingTimeRange; +use Espo\ORM\Entity; + +/** + * @implements Validator + */ +class OnlyIfNoUsers implements Validator +{ + public function validate(Entity $entity, string $field, Data $data): ?Failure + { + if ($entity->getCalendars()->getCount() === 0) { + return null; + } + + if ($entity->getUsers()->getCount() === 0) { + return null; + } + + return Failure::create(); + } +} diff --git a/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/RequiredIfNoUsers.php b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/RequiredIfNoUsers.php new file mode 100644 index 0000000000..7730b09450 --- /dev/null +++ b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Calendars/RequiredIfNoUsers.php @@ -0,0 +1,52 @@ +. + * + * 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. + ************************************************************************/ + +namespace Espo\Classes\FieldValidators\WorkingTimeRange\Calendars; + +use Espo\Core\FieldValidation\Validator; +use Espo\Core\FieldValidation\Validator\Data; +use Espo\Core\FieldValidation\Validator\Failure; +use Espo\Entities\WorkingTimeRange; +use Espo\ORM\Entity; + +/** + * @implements Validator + */ +class RequiredIfNoUsers implements Validator +{ + + public function validate(Entity $entity, string $field, Data $data): ?Failure + { + if ($entity->getCalendars()->getCount() !== 0 || $entity->getUsers()->getCount() !== 0) { + return null; + } + + return Failure::create(); + } +} diff --git a/application/Espo/Classes/FieldValidators/WorkingTimeRange/Users/OnlyIfNoCalendars.php b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Users/OnlyIfNoCalendars.php new file mode 100644 index 0000000000..24c82a5a3c --- /dev/null +++ b/application/Espo/Classes/FieldValidators/WorkingTimeRange/Users/OnlyIfNoCalendars.php @@ -0,0 +1,55 @@ +. + * + * 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. + ************************************************************************/ + +namespace Espo\Classes\FieldValidators\WorkingTimeRange\Users; + +use Espo\Core\FieldValidation\Validator; +use Espo\Core\FieldValidation\Validator\Data; +use Espo\Core\FieldValidation\Validator\Failure; +use Espo\Entities\WorkingTimeRange; +use Espo\ORM\Entity; + +/** + * @implements Validator + */ +class OnlyIfNoCalendars implements Validator +{ + public function validate(Entity $entity, string $field, Data $data): ?Failure + { + if ($entity->getUsers()->getCount() === 0) { + return null; + } + + if ($entity->getCalendars()->getCount() === 0) { + return null; + } + + return Failure::create(); + } +} diff --git a/application/Espo/Entities/WorkingTimeRange.php b/application/Espo/Entities/WorkingTimeRange.php index 282842520c..08ba264e75 100644 --- a/application/Espo/Entities/WorkingTimeRange.php +++ b/application/Espo/Entities/WorkingTimeRange.php @@ -128,4 +128,13 @@ class WorkingTimeRange extends Entity /** @var LinkMultiple */ return $this->getValueObject('users'); } + + /** + * @since 9.3.1 + */ + public function getCalendars(): LinkMultiple + { + /** @var LinkMultiple */ + return $this->getValueObject('calendars'); + } } diff --git a/application/Espo/Resources/layouts/WorkingTimeRange/detail.json b/application/Espo/Resources/layouts/WorkingTimeRange/detail.json index ccfc2151eb..67a59492b4 100644 --- a/application/Espo/Resources/layouts/WorkingTimeRange/detail.json +++ b/application/Espo/Resources/layouts/WorkingTimeRange/detail.json @@ -13,9 +13,13 @@ {"name": "timeRanges"}, false ], + [ + {"name": "users"}, + false + ], [ {"name": "calendars"}, - {"name": "users"} + false ], [ {"name": "description"} diff --git a/application/Espo/Resources/layouts/WorkingTimeRange/detailSmall.json b/application/Espo/Resources/layouts/WorkingTimeRange/detailSmall.json index ccfc2151eb..67a59492b4 100644 --- a/application/Espo/Resources/layouts/WorkingTimeRange/detailSmall.json +++ b/application/Espo/Resources/layouts/WorkingTimeRange/detailSmall.json @@ -13,9 +13,13 @@ {"name": "timeRanges"}, false ], + [ + {"name": "users"}, + false + ], [ {"name": "calendars"}, - {"name": "users"} + false ], [ {"name": "description"} diff --git a/application/Espo/Resources/metadata/entityDefs/WorkingTimeRange.json b/application/Espo/Resources/metadata/entityDefs/WorkingTimeRange.json index 3a89ba0bf3..80d9090ffe 100644 --- a/application/Espo/Resources/metadata/entityDefs/WorkingTimeRange.json +++ b/application/Espo/Resources/metadata/entityDefs/WorkingTimeRange.json @@ -34,12 +34,20 @@ }, "calendars": { "type": "linkMultiple", - "tooltip": true + "tooltip": true, + "validatorClassNameList": [ + "Espo\\Classes\\FieldValidators\\WorkingTimeRange\\Calendars\\OnlyIfNoUsers", + "Espo\\Classes\\FieldValidators\\WorkingTimeRange\\Calendars\\RequiredIfNoUsers" + ], + "autocompleteOnEmpty": true }, "users": { "type": "linkMultiple", "view": "views/working-time-range/fields/users", - "tooltip": true + "tooltip": true, + "validatorClassNameList": [ + "Espo\\Classes\\FieldValidators\\WorkingTimeRange\\Users\\OnlyIfNoCalendars" + ] }, "createdAt": { "type": "datetime", diff --git a/application/Espo/Resources/metadata/logicDefs/WorkingTimeRange.json b/application/Espo/Resources/metadata/logicDefs/WorkingTimeRange.json index 605b9f7262..3684ab3489 100644 --- a/application/Espo/Resources/metadata/logicDefs/WorkingTimeRange.json +++ b/application/Espo/Resources/metadata/logicDefs/WorkingTimeRange.json @@ -11,7 +11,7 @@ ] } }, - "users": { + "calendars": { "visible": { "conditionGroup": [ { @@ -19,8 +19,36 @@ "value": [ { "type": "isNotEmpty", - "attribute": "id" + "attribute": "calendarsIds" }, + { + "type": "isEmpty", + "attribute": "usersIds" + } + ] + } + ] + } + }, + "users": { + "required": { + "conditionGroup": [ + { + "type": "or", + "value": [ + { + "type": "isEmpty", + "attribute": "calendarsIds" + } + ] + } + ] + }, + "visible": { + "conditionGroup": [ + { + "type": "or", + "value": [ { "type": "isNotEmpty", "attribute": "usersIds" From ba8c4028132125bac06a39d9abffcb5cbb248126 Mon Sep 17 00:00:00 2001 From: Yurii Date: Mon, 9 Feb 2026 22:30:04 +0200 Subject: [PATCH 16/20] load capture: load additional fields on generate --- application/Espo/Tools/LeadCapture/Service.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/Espo/Tools/LeadCapture/Service.php b/application/Espo/Tools/LeadCapture/Service.php index af9380b903..9cb79dc89c 100644 --- a/application/Espo/Tools/LeadCapture/Service.php +++ b/application/Espo/Tools/LeadCapture/Service.php @@ -84,6 +84,7 @@ class Service $this->entityManager->saveEntity($entity); + $service->loadAdditionalFields($entity); $service->prepareEntityForOutput($entity); return $entity; @@ -108,6 +109,7 @@ class Service $this->entityManager->saveEntity($entity); + $service->loadAdditionalFields($entity); $service->prepareEntityForOutput($entity); return $entity; From 5ca1cce14a00ad2af77a2cb5f4dec523c13fc386 Mon Sep 17 00:00:00 2001 From: Yurii Date: Thu, 12 Feb 2026 09:45:47 +0200 Subject: [PATCH 17/20] record dashlet layout field translaction fix --- .../src/views/dashlets/fields/records/expanded-layout.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/views/dashlets/fields/records/expanded-layout.js b/client/src/views/dashlets/fields/records/expanded-layout.js index 110a432721..efa04889ae 100644 --- a/client/src/views/dashlets/fields/records/expanded-layout.js +++ b/client/src/views/dashlets/fields/records/expanded-layout.js @@ -108,8 +108,14 @@ class ExpandedLayoutDashletFieldView extends BaseFieldView { this.addActionHandler('editItem', (event, target) => this.editItem(target.dataset.name)); - this.targetEntityType = this.model.get('entityType') || + this.targetEntityType = this.model.attributes.entityType ?? this.getMetadata().get(['dashlets', this.dataObject.dashletName, 'entityType']); + + this.listenTo(this.model, 'change:entityType', () => { + if (this.model.attributes.entityType) { + this.targetEntityType = this.model.attributes.entityType; + } + }); } /** From 72f0f8e0a763926f85de91211967fe4d40443473 Mon Sep 17 00:00:00 2001 From: Yurii Date: Thu, 12 Feb 2026 13:03:08 +0200 Subject: [PATCH 18/20] fix search layout ignoring fields --- client/src/views/admin/layouts/filters.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/views/admin/layouts/filters.js b/client/src/views/admin/layouts/filters.js index b5f4d356cc..dc10d17a77 100644 --- a/client/src/views/admin/layouts/filters.js +++ b/client/src/views/admin/layouts/filters.js @@ -118,6 +118,16 @@ class LayoutFiltersView extends LayoutRowsView { return false; } + /** @type {string[]|null} */ + const layoutList = model.getFieldParam(name, 'layoutAvailabilityList'); + + if ( + layoutList && + !layoutList.includes(this.type) + ) { + return false; + } + return !model.getFieldParam(name, 'disabled') && !model.getFieldParam(name, 'utility') && !model.getFieldParam(name, 'layoutFiltersDisabled'); From fbab0a980304640fe841c0f0f19f958b1e0da6f7 Mon Sep 17 00:00:00 2001 From: Yurii Date: Thu, 12 Feb 2026 13:43:22 +0200 Subject: [PATCH 19/20] createDisabled check --- client/src/views/record/detail.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/views/record/detail.js b/client/src/views/record/detail.js index d875d84712..77448345d5 100644 --- a/client/src/views/record/detail.js +++ b/client/src/views/record/detail.js @@ -846,7 +846,8 @@ class DetailRecordView extends BaseRecordView { if (this.duplicateAction) { if ( this.getAcl().check(this.entityType, 'create') && - !this.getMetadata().get(['clientDefs', this.scope, 'duplicateDisabled']) + !this.getMetadata().get(['clientDefs', this.scope, 'duplicateDisabled']) && + !this.getMetadata().get(['clientDefs', this.scope, 'createDisabled']) ) { this.addDropdownItem({ label: 'Duplicate', From 9e1e3396d0a677ebacd963063614d10b3c6590e9 Mon Sep 17 00:00:00 2001 From: Yurii Date: Fri, 13 Feb 2026 13:33:18 +0200 Subject: [PATCH 20/20] increase precision --- .../Espo/Resources/metadata/entityDefs/CurrencyRecordRate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Espo/Resources/metadata/entityDefs/CurrencyRecordRate.json b/application/Espo/Resources/metadata/entityDefs/CurrencyRecordRate.json index 4dc01c39a0..594963e90b 100644 --- a/application/Espo/Resources/metadata/entityDefs/CurrencyRecordRate.json +++ b/application/Espo/Resources/metadata/entityDefs/CurrencyRecordRate.json @@ -23,7 +23,7 @@ "type": "decimal", "decimalPlaces": 6, "min": 0.0001, - "precision": 15, + "precision": 20, "scale": 8, "required": true, "audited": true,