client page theme, lead form theme

This commit is contained in:
Yuri Kuznetsov
2025-02-11 15:35:00 +02:00
parent fe2f15c576
commit e0c84e007c
18 changed files with 217 additions and 16 deletions
@@ -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;
}
@@ -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);
@@ -44,6 +44,7 @@ class Params
/** @var Script[] */
private array $scripts = [];
private ?string $pageTitle = null;
private ?string $theme = null;
/**
* @param ?array<string, mixed> $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;
}
}
@@ -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,
) {}
}
+10 -1
View File
@@ -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',
@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
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");
}
}
+7 -4
View File
@@ -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());
}
}
+11
View File
@@ -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');
}
}
@@ -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)]);
@@ -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",
@@ -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]
]
},
{
@@ -164,6 +164,16 @@
]
}
},
"formTheme": {
"visible": {
"conditionGroup": [
{
"type": "isTrue",
"attribute": "formEnabled"
}
]
}
},
"formText": {
"visible": {
"conditionGroup": [
@@ -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"
@@ -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());
}
}
+7 -1
View File
@@ -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);
+14 -4
View File
@@ -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');
}
@@ -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 <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import 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];
}
}
+1
View File
@@ -31,6 +31,7 @@
ajaxTimeout: {{ajaxTimeout}},
internalModuleList: {{internalModuleList}},
bundledModuleList: {{bundledModuleList}},
theme: {{theme}},
}, app => {
loadedApp = app;