diff --git a/application/Espo/Core/Portal/Utils/ThemeManager.php b/application/Espo/Core/Portal/Utils/ThemeManager.php index 72fee9807d..f76d70244e 100644 --- a/application/Espo/Core/Portal/Utils/ThemeManager.php +++ b/application/Espo/Core/Portal/Utils/ThemeManager.php @@ -31,6 +31,7 @@ namespace Espo\Core\Portal\Utils; use Espo\Core\Utils\Config; use Espo\Core\Utils\Metadata; +use Espo\Core\Utils\Theme\MetadataProvider; use Espo\Entities\Portal; use Espo\Core\Utils\ThemeManager as BaseThemeManager; @@ -38,9 +39,13 @@ class ThemeManager extends BaseThemeManager { private Portal $portal; - public function __construct(Config $config, Metadata $metadata, Portal $portal) - { - parent::__construct($config, $metadata); + public function __construct( + Config $config, + Metadata $metadata, + MetadataProvider $metadataProvider, + Portal $portal, + ) { + parent::__construct($config, $metadata, $metadataProvider); $this->portal = $portal; } diff --git a/application/Espo/Core/Utils/Client/ActionRenderer.php b/application/Espo/Core/Utils/Client/ActionRenderer.php index ccb6d3ca43..e4fa666e90 100644 --- a/application/Espo/Core/Utils/Client/ActionRenderer.php +++ b/application/Espo/Core/Utils/Client/ActionRenderer.php @@ -55,6 +55,7 @@ class ActionRenderer initAuth: $params->initAuth(), scripts: $params->getScripts(), pageTitle: $params->getPageTitle(), + theme: $params->getTheme(), ); $securityParams = new SecurityParams( @@ -76,6 +77,7 @@ class ActionRenderer bool $initAuth, array $scripts, ?string $pageTitle, + ?string $theme, ): string { $encodedData = Json::encode($data); @@ -96,6 +98,7 @@ class ActionRenderer runScript: $script, scripts: $scripts, pageTitle: $pageTitle, + theme: $theme, ); return $this->clientManager->render($params); diff --git a/application/Espo/Core/Utils/Client/ActionRenderer/Params.php b/application/Espo/Core/Utils/Client/ActionRenderer/Params.php index d5b82f5bdc..448ce58bbf 100644 --- a/application/Espo/Core/Utils/Client/ActionRenderer/Params.php +++ b/application/Espo/Core/Utils/Client/ActionRenderer/Params.php @@ -44,6 +44,7 @@ class Params /** @var Script[] */ private array $scripts = []; private ?string $pageTitle = null; + private ?string $theme = null; /** * @param ?array $data @@ -118,6 +119,17 @@ class Params return $obj; } + /** + * @since 9.1.0 + */ + public function withTheme(?string $theme): self + { + $obj = clone $this; + $obj->theme = $theme; + + return $obj; + } + public function getController(): string { return $this->controller; @@ -166,4 +178,12 @@ class Params { return $this->pageTitle; } + + /** + * @since 9.1.0 + */ + public function getTheme(): ?string + { + return $this->theme; + } } diff --git a/application/Espo/Core/Utils/Client/RenderParams.php b/application/Espo/Core/Utils/Client/RenderParams.php index 9ec7d98fd3..70f893ede4 100644 --- a/application/Espo/Core/Utils/Client/RenderParams.php +++ b/application/Espo/Core/Utils/Client/RenderParams.php @@ -35,10 +35,12 @@ readonly class RenderParams * @param ?string $runScript A JS run-script. * @param Script[] $scripts Scripts to include on the page. * @param ?string $pageTitle A page title. Since 9.1.0. + * @param ?string $theme A page theme name. */ public function __construct( public ?string $runScript = null, public array $scripts = [], public ?string $pageTitle = null, + public ?string $theme = null, ) {} } diff --git a/application/Espo/Core/Utils/ClientManager.php b/application/Espo/Core/Utils/ClientManager.php index b606dade65..5bac61ac72 100644 --- a/application/Espo/Core/Utils/ClientManager.php +++ b/application/Espo/Core/Utils/ClientManager.php @@ -40,6 +40,7 @@ use Espo\Core\Utils\Config\ApplicationConfig; use Espo\Core\Utils\Config\SystemConfig; use Espo\Core\Utils\File\Manager as FileManager; +use Espo\Core\Utils\Theme\MetadataProvider as ThemeMetadataProvider; use Slim\Psr7\Response as Psr7Response; use Slim\ResponseEmitter; @@ -70,6 +71,7 @@ class ClientManager private LoaderParamsProvider $loaderParamsProvider, private SystemConfig $systemConfig, private ApplicationConfig $applicationConfig, + private ThemeMetadataProvider $themeMetadataProvider, ) { $this->nonce = Util::generateKey(); } @@ -168,6 +170,7 @@ class ClientManager runScript: $params->runScript, additionalScripts: $params->scripts, pageTitle: $params->pageTitle, + theme: $params->theme, ); } @@ -181,6 +184,7 @@ class ClientManager array $vars = [], array $additionalScripts = [], ?string $pageTitle = null, + ?string $theme = null, ): string { $runScript ??= $this->runScript; @@ -232,6 +236,10 @@ class ClientManager $this->module->getInternalList() ); + $stylesheet = $theme ? + $this->themeMetadataProvider->getStylesheet($theme) : + $this->themeManager->getStylesheet(); + $data = [ 'applicationId' => $this->applicationId, 'apiUrl' => $this->apiUrl, @@ -239,7 +247,8 @@ class ClientManager 'cacheTimestamp' => $cacheTimestamp, 'appTimestamp' => $appTimestamp, 'loaderCacheTimestamp' => Json::encode($loaderCacheTimestamp), - 'stylesheet' => $this->themeManager->getStylesheet(), + 'stylesheet' => $stylesheet, + 'theme' => Json::encode($theme), 'runScript' => $runScript, 'basePath' => $this->basePath, 'useCache' => $useCache ? 'true' : 'false', diff --git a/application/Espo/Core/Utils/Theme/MetadataProvider.php b/application/Espo/Core/Utils/Theme/MetadataProvider.php new file mode 100644 index 0000000000..cb74a0d1d3 --- /dev/null +++ b/application/Espo/Core/Utils/Theme/MetadataProvider.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\Core\Utils\Theme; + +use Espo\Core\Utils\Metadata; + +/** + * @internal + * @since 9.1.0 + */ +class MetadataProvider +{ + private string $defaultStylesheet = 'client/css/espo/espo.css'; + + public function __construct( + private Metadata $metadata, + ) {} + + public function getStylesheet(string $theme): string + { + return $this->metadata->get("themes.$theme.stylesheet") ?? $this->defaultStylesheet; + } + + public function isDark(string $theme): bool + { + return (bool) $this->metadata->get("themes.$theme.isDark"); + } +} diff --git a/application/Espo/Core/Utils/ThemeManager.php b/application/Espo/Core/Utils/ThemeManager.php index b0efe7ddbc..2baf4b9b6e 100644 --- a/application/Espo/Core/Utils/ThemeManager.php +++ b/application/Espo/Core/Utils/ThemeManager.php @@ -29,15 +29,18 @@ namespace Espo\Core\Utils; +use Espo\Core\Utils\Theme\MetadataProvider; + class ThemeManager { private string $defaultName = 'Espo'; - private string $defaultStylesheet = 'client/css/espo/espo.css'; + private string $defaultLogoSrc = 'client/img/logo.svg'; public function __construct( private Config $config, - private Metadata $metadata + private Metadata $metadata, + private MetadataProvider $metadataProvider, ) {} public function getName(): string @@ -47,7 +50,7 @@ class ThemeManager public function getStylesheet(): string { - return $this->metadata->get(['themes', $this->getName(), 'stylesheet']) ?? $this->defaultStylesheet; + return $this->metadataProvider->getStylesheet($this->getName()); } public function getLogoSrc(): string @@ -57,6 +60,6 @@ class ThemeManager public function isDark(): bool { - return (bool) $this->metadata->get("themes.{$this->getName()}.isDark"); + return $this->metadataProvider->isDark($this->getName()); } } diff --git a/application/Espo/Entities/LeadCapture.php b/application/Espo/Entities/LeadCapture.php index d7c71dcf5f..f035f7504c 100644 --- a/application/Espo/Entities/LeadCapture.php +++ b/application/Espo/Entities/LeadCapture.php @@ -221,8 +221,19 @@ class LeadCapture extends Entity return (bool) $this->get('formEnabled'); } + /** + * @since 9.1.0 + */ public function getFormTitle(): ?string { return $this->get('formTitle'); } + + /** + * @since 9.1.0 + */ + public function getFormTheme(): ?string + { + return $this->get('formTheme'); + } } diff --git a/application/Espo/EntryPoints/LeadCaptureForm.php b/application/Espo/EntryPoints/LeadCaptureForm.php index 7c14a3ed3b..772ba04301 100644 --- a/application/Espo/EntryPoints/LeadCaptureForm.php +++ b/application/Espo/EntryPoints/LeadCaptureForm.php @@ -73,7 +73,8 @@ class LeadCaptureForm implements EntryPoint $params = $params ->withFrameAncestors($leadCapture->getFormFrameAncestors()) - ->withPageTitle($leadCapture->getFormTitle()); + ->withPageTitle($leadCapture->getFormTitle()) + ->withTheme($leadCapture->getFormTheme()); if ($captchaScript) { $params = $params->withScripts([new Script(source: $captchaScript)]); diff --git a/application/Espo/Resources/i18n/en_US/LeadCapture.json b/application/Espo/Resources/i18n/en_US/LeadCapture.json index 94d61d8afb..2857087892 100644 --- a/application/Espo/Resources/i18n/en_US/LeadCapture.json +++ b/application/Espo/Resources/i18n/en_US/LeadCapture.json @@ -29,6 +29,7 @@ "formEnabled": "Web Form", "formUrl": "Form URL", "formTitle": "Form Title", + "formTheme": "Form Theme", "formSuccessText": "Text to display after form submission", "formText": "Text to display on form", "formSuccessRedirectUrl": "URL to redirect to after form submission", diff --git a/application/Espo/Resources/layouts/LeadCapture/detail.json b/application/Espo/Resources/layouts/LeadCapture/detail.json index c6d416bc47..0404ef3d66 100644 --- a/application/Espo/Resources/layouts/LeadCapture/detail.json +++ b/application/Espo/Resources/layouts/LeadCapture/detail.json @@ -18,7 +18,8 @@ [{"name": "formEnabled"}, {"name": "formTitle"}], [{"name": "formText"}, {"name": "formSuccessText"}], [{"name": "formSuccessRedirectUrl"}, {"name": "formLanguage"}], - [{"name": "formFrameAncestors"}, {"name": "formCaptcha"}] + [{"name": "formFrameAncestors"}, {"name": "formCaptcha"}], + [{"name": "formTheme"}, false] ] }, { diff --git a/application/Espo/Resources/metadata/clientDefs/LeadCapture.json b/application/Espo/Resources/metadata/clientDefs/LeadCapture.json index 1906648618..0422368825 100644 --- a/application/Espo/Resources/metadata/clientDefs/LeadCapture.json +++ b/application/Espo/Resources/metadata/clientDefs/LeadCapture.json @@ -164,6 +164,16 @@ ] } }, + "formTheme": { + "visible": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "formEnabled" + } + ] + } + }, "formText": { "visible": { "conditionGroup": [ diff --git a/application/Espo/Resources/metadata/entityDefs/LeadCapture.json b/application/Espo/Resources/metadata/entityDefs/LeadCapture.json index 938c366e2b..51570e4da1 100644 --- a/application/Espo/Resources/metadata/entityDefs/LeadCapture.json +++ b/application/Espo/Resources/metadata/entityDefs/LeadCapture.json @@ -136,6 +136,12 @@ "type": "varchar", "maxLength": 80 }, + "formTheme": { + "type": "enum", + "maxLength": 64, + "view": "views/lead-capture/fields/form-theme", + "translation": "Global.themes" + }, "formText": { "type": "text", "tooltip": "optInConfirmationSuccessMessage" diff --git a/application/Espo/Tools/LeadCapture/FormService.php b/application/Espo/Tools/LeadCapture/FormService.php index a597cdaab7..739e2eb8ac 100644 --- a/application/Espo/Tools/LeadCapture/FormService.php +++ b/application/Espo/Tools/LeadCapture/FormService.php @@ -36,6 +36,7 @@ use Espo\Core\Utils\Config; use Espo\Core\Utils\DataCache; use Espo\Core\Utils\Language; use Espo\Core\Utils\Metadata; +use Espo\Core\Utils\Theme\MetadataProvider as ThemeMetadataProvider; use Espo\Core\Utils\ThemeManager; use Espo\Entities\Integration; use Espo\Entities\LeadCapture; @@ -60,6 +61,7 @@ class FormService private DataCache $dataCache, private ThemeManager $themeManager, private Config\SystemConfig $systemConfig, + private ThemeMetadataProvider $themeMetadataProvider, ) {} /** @@ -75,7 +77,6 @@ class FormService $data = $this->getDataInternal($leadCapture); $data['captchaKey'] = $captchaKey; - $data['isDark'] = $this->themeManager->isDark(); return [$leadCapture, $data, $captchaScript]; } @@ -413,6 +414,7 @@ class FormService 'regExpPatterns' => $this->metadata->get("app.regExpPatterns"), ], ], + 'isDark' => $this->isDark($leadCapture), 'detailLayout' => $detailLayout, 'language' => $languageData, 'successText' => $successText, @@ -465,4 +467,13 @@ class FormService return 'https://www.google.com/recaptcha/api.js?render=' . $siteKey; } + + private function isDark(LeadCapture $leadCapture): bool + { + if (!$leadCapture->getFormTheme()) { + return $this->themeManager->isDark(); + } + + return $this->themeMetadataProvider->isDark($leadCapture->getFormTheme()); + } } diff --git a/client/src/app.js b/client/src/app.js index ac1d217ffe..76480a2f4a 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -116,6 +116,11 @@ class App { */ this.internalModuleList = options.internalModuleList || []; + /** + * @private + */ + this.themeName = options.theme || null; + /** * A list of bundled modules. * @@ -433,7 +438,7 @@ class App { this.fieldManager.acl = this.acl; - this.themeManager = new ThemeManager(this.settings, this.preferences, this.metadata); + this.themeManager = new ThemeManager(this.settings, this.preferences, this.metadata, this.themeName); this.modelFactory = new ModelFactory(this.metadata); this.collectionFactory = new CollectionFactory(this.modelFactory, this.settings, this.metadata); @@ -1561,6 +1566,7 @@ class App { * @property {string} [bundledModuleList] A list of bundled modules. * @property {Number|null} [cacheTimestamp] A cache timestamp. * @property {Number|null} [appTimestamp] An application timestamp. + * @property {string|null} [theme] A theme name. */ Object.assign(App.prototype, Events); diff --git a/client/src/theme-manager.js b/client/src/theme-manager.js index 644b1e48d9..977325dc36 100644 --- a/client/src/theme-manager.js +++ b/client/src/theme-manager.js @@ -37,7 +37,7 @@ class ThemeManager { * @param {module:models/settings} config A config. * @param {module:models/preferences} preferences Preferences. * @param {module:metadata} metadata Metadata. - * @param {?string} [name] A name. If not set, then will be obtained from config and preferences. + * @param {string|null} [name] A name. If not set, then will be obtained from config and preferences. */ constructor(config, preferences, metadata, name) { /** @@ -60,7 +60,13 @@ class ThemeManager { /** * @private - * @type {?string} + * @type {boolean} + */ + this.useConfig = !name; + + /** + * @private + * @type {string|null} */ this.name = name || null; } @@ -220,11 +226,15 @@ class ThemeManager { let values = null; - if (!this.config.get('userThemesDisabled') && this.preferences.get('theme')) { + if ( + this.useConfig && + !this.config.get('userThemesDisabled') && + this.preferences.get('theme') + ) { values = this.preferences.get('themeParams'); } - if (!values) { + if (!values && this.useConfig) { values = this.config.get('themeParams'); } diff --git a/client/src/views/lead-capture/fields/form-theme.js b/client/src/views/lead-capture/fields/form-theme.js new file mode 100644 index 0000000000..4b3c0b252b --- /dev/null +++ b/client/src/views/lead-capture/fields/form-theme.js @@ -0,0 +1,46 @@ +/************************************************************************ + * 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 EnumFieldView from 'views/fields/enum'; + +export default class FormThemeFieldView extends EnumFieldView { + + setupOptions() { + const list = Object.keys(this.getMetadata().get('themes') || {}) + .sort((v1, v2) => { + if (v2 === 'EspoRtl') { + return -1; + } + + return this.translate(v1, 'theme') + .localeCompare(this.translate(v2, 'theme')); + }); + + this.params.options = ['', ...list]; + } +} diff --git a/html/main.html b/html/main.html index 866326c552..59acb0113d 100644 --- a/html/main.html +++ b/html/main.html @@ -31,6 +31,7 @@ ajaxTimeout: {{ajaxTimeout}}, internalModuleList: {{internalModuleList}}, bundledModuleList: {{bundledModuleList}}, + theme: {{theme}}, }, app => { loadedApp = app;