personal email folder mapping
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 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 <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\Classes\FieldValidators\EmailAccount\FolderMap;
|
||||
|
||||
use Espo\Core\FieldValidation\Validator;
|
||||
use Espo\Core\FieldValidation\Validator\Data;
|
||||
use Espo\Core\FieldValidation\Validator\Failure;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\EmailFolder;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @implements Validator<EmailAccount>
|
||||
*/
|
||||
class Valid implements Validator
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
public function validate(Entity $entity, string $field, Data $data): ?Failure
|
||||
{
|
||||
$map = $entity->get('folderMap');
|
||||
|
||||
if ($map === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$map instanceof stdClass) {
|
||||
return Failure::create();
|
||||
}
|
||||
|
||||
foreach (get_object_vars($map) as $folderId) {
|
||||
if ($folderId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_string($folderId)) {
|
||||
return Failure::create();
|
||||
}
|
||||
|
||||
$folderEntry = $this->entityManager->getRepositoryByClass(EmailFolder::class)->getById($folderId);
|
||||
|
||||
if (!$folderEntry) {
|
||||
return Failure::create();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 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 <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\Classes\RecordHooks\EmailAccount;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Error\Body;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\Hook\SaveHook;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\EmailFolder;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @implements SaveHook<EmailAccount>
|
||||
*/
|
||||
class BeforeSave implements SaveHook
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
) {}
|
||||
|
||||
public function process(Entity $entity): void
|
||||
{
|
||||
$this->checkFolderMap($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkFolderMap(EmailAccount $entity): void
|
||||
{
|
||||
if (!$entity->isAttributeChanged('folderMap')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$map = $entity->get('folderMap');
|
||||
|
||||
if ($map === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$map instanceof stdClass) {
|
||||
throw new RuntimeException("Bad folderMap.");
|
||||
}
|
||||
|
||||
foreach (get_object_vars($map) as $folderId) {
|
||||
if ($folderId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_string($folderId)) {
|
||||
throw new RuntimeException("Bad folderMap item.");
|
||||
}
|
||||
|
||||
$folderEntry = $this->entityManager->getRepositoryByClass(EmailFolder::class)->getById($folderId);
|
||||
|
||||
if (!$folderEntry) {
|
||||
throw Forbidden::createWithBody('noFolder',
|
||||
Body::create()->withMessageTranslation('noFolder', EmailAccount::ENTITY_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityRead($folderEntry)) {
|
||||
throw new Forbidden("No access to mapped folder $folderId.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,4 +164,12 @@ interface Account
|
||||
* Get the last connection time;
|
||||
*/
|
||||
public function getConnectedAt(): ?DateTime;
|
||||
|
||||
|
||||
/**
|
||||
* Get an email folder mapped to the email folder.
|
||||
*
|
||||
* @since 9.3.0
|
||||
*/
|
||||
public function getMappedEmailFolder(string $folder): ?Link;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class Fetcher
|
||||
Account $account,
|
||||
string $folderOriginal,
|
||||
Storage $storage,
|
||||
Collection $filterList
|
||||
Collection $filterList,
|
||||
): void {
|
||||
|
||||
$fetchData = $account->getFetchData();
|
||||
@@ -132,11 +132,11 @@ class Fetcher
|
||||
$previousLastUniqueId = $lastUniqueId;
|
||||
|
||||
$idList = $this->getIdList(
|
||||
$account,
|
||||
$storage,
|
||||
$lastUniqueId,
|
||||
$lastDate,
|
||||
$forceByDate
|
||||
account: $account,
|
||||
storage: $storage,
|
||||
lastUID: $lastUniqueId,
|
||||
lastDate: $lastDate,
|
||||
forceByDate: $forceByDate,
|
||||
);
|
||||
|
||||
if (count($idList) === 1 && $lastUniqueId) {
|
||||
@@ -162,7 +162,13 @@ class Fetcher
|
||||
}
|
||||
}
|
||||
|
||||
$email = $this->fetchEmail($account, $storage, $id, $filterList);
|
||||
$email = $this->fetchEmail(
|
||||
account: $account,
|
||||
storage: $storage,
|
||||
id: $id,
|
||||
filterList: $filterList,
|
||||
mappedEmailFolderId: $account->getMappedEmailFolder($folderOriginal)?->getId(),
|
||||
);
|
||||
|
||||
$isLast = $counter === count($idList) - 1;
|
||||
$isLastInPortion = $counter === $portionLimit - 1;
|
||||
@@ -254,7 +260,8 @@ class Fetcher
|
||||
Account $account,
|
||||
Storage $storage,
|
||||
int $id,
|
||||
Collection $filterList
|
||||
Collection $filterList,
|
||||
?string $mappedEmailFolderId,
|
||||
): ?Email {
|
||||
|
||||
$teamIdList = $account->getTeams()->getIdList();
|
||||
@@ -265,11 +272,7 @@ class Fetcher
|
||||
|
||||
$fetchOnlyHeader = $this->checkFetchOnlyHeader($storage, $id);
|
||||
|
||||
$folderData = [];
|
||||
|
||||
if ($userId && $account->getEmailFolder()) {
|
||||
$folderData[$userId] = $account->getEmailFolder()->getId();
|
||||
}
|
||||
$folderData = $this->prepareFolderData($userId, $mappedEmailFolderId, $account);
|
||||
|
||||
$flags = null;
|
||||
|
||||
@@ -456,4 +459,24 @@ class Fetcher
|
||||
array_diff($flags, [Flag::RECENT])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function prepareFolderData(?string $userId, ?string $mappedEmailFolderId, Account $account): array
|
||||
{
|
||||
if (!$userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$folderData = [];
|
||||
|
||||
if ($mappedEmailFolderId) {
|
||||
$folderData[$userId] = $mappedEmailFolderId;
|
||||
} else if ($account->getEmailFolder()) {
|
||||
$folderData[$userId] = $account->getEmailFolder()->getId();
|
||||
}
|
||||
|
||||
return $folderData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,4 +381,9 @@ class Account implements AccountInterface
|
||||
/** @var DateTime */
|
||||
return $this->entity->getValueObject('connectedAt');
|
||||
}
|
||||
|
||||
public function getMappedEmailFolder(string $folder): ?Link
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,11 @@ class Account implements AccountInterface
|
||||
return $this->entity->getEmailFolder();
|
||||
}
|
||||
|
||||
public function getMappedEmailFolder(string $folder): ?Link
|
||||
{
|
||||
return $this->entity->getMappedEmailFolder($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -117,6 +117,20 @@ class EmailAccount extends Entity
|
||||
return $this->getValueObject('emailFolder');
|
||||
}
|
||||
|
||||
public function getMappedEmailFolder(string $folder): ?Link
|
||||
{
|
||||
/** @var stdClass $map */
|
||||
$map = $this->get('folderMap') ?? (object) [];
|
||||
|
||||
$folderId = $map->$folder ?? null;
|
||||
|
||||
if (!$folderId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Link::create($folderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"smtpSecurity": "SMTP Security",
|
||||
"smtpAuthMechanism": "SMTP Auth Mechanism",
|
||||
"smtpUsername": "SMTP Username",
|
||||
"smtpPassword": "SMTP Password"
|
||||
"smtpPassword": "SMTP Password",
|
||||
"folderMap": "Folder Mapping"
|
||||
},
|
||||
"links": {
|
||||
"filters": "Filters",
|
||||
@@ -60,6 +61,7 @@
|
||||
"active": "Active"
|
||||
},
|
||||
"messages": {
|
||||
"noFolder": "Mapped folder does not exist.",
|
||||
"couldNotConnectToImap": "Could not connect to IMAP server",
|
||||
"connectionIsOk": "Connection is Ok",
|
||||
"imapNotConnected": "Could not connect to [IMAP account](#EmailAccount/view/{id})."
|
||||
@@ -67,7 +69,7 @@
|
||||
"tooltips": {
|
||||
"useSmtp": "The ability to send emails.",
|
||||
"emailAddress": "The user record (assigned user) should have the same email address to be able to use this email account for sending.",
|
||||
"monitoredFolders": "Multiple folders should be separated by comma.\n\nYou can add a 'Sent' folder to sync emails sent from an external email client.",
|
||||
"monitoredFolders": "Select IMAP folders to import. Optionally, map them to Espo folders.\n\nYou can add the 'Sent' folder to sync emails sent from an external email client.",
|
||||
"storeSentEmails": "Sent emails will be stored on the IMAP server. Email Address field should match the address emails will be sent from."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,21 @@
|
||||
"view": "views/email-account/fields/folders",
|
||||
"displayAsList": true,
|
||||
"noEmptyString": true,
|
||||
"duplicateIgnore": true
|
||||
"duplicateIgnore": true,
|
||||
"tooltip": true,
|
||||
"fullNameAdditionalAttributeList": ["folderMap"]
|
||||
},
|
||||
"sentFolder": {
|
||||
"type": "varchar",
|
||||
"view": "views/email-account/fields/folder",
|
||||
"duplicateIgnore": true
|
||||
},
|
||||
"folderMap": {
|
||||
"type": "jsonObject",
|
||||
"validatorClassNameList": [
|
||||
"Espo\\Classes\\FieldValidators\\EmailAccount\\FolderMap\\Valid"
|
||||
]
|
||||
},
|
||||
"storeSentEmails": {
|
||||
"type": "bool",
|
||||
"tooltip": true
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"Espo\\Classes\\Record\\InboundEmail\\PasswordsInputFilter"
|
||||
],
|
||||
"beforeCreateHookClassNameList": [
|
||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeCreate"
|
||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeCreate",
|
||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave"
|
||||
],
|
||||
"beforeUpdateHookClassNameList": [
|
||||
"Espo\\Classes\\RecordHooks\\EmailAccount\\BeforeSave"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,14 +28,108 @@
|
||||
|
||||
import ArrayFieldView from 'views/fields/array';
|
||||
|
||||
export default class extends ArrayFieldView {
|
||||
export default class EmailAccountFoldersFieldView extends ArrayFieldView {
|
||||
|
||||
// language=Handlebars
|
||||
detailTemplateContent = `
|
||||
{{#unless isEmpty}}
|
||||
{{#each itemDataList}}
|
||||
<div class="multi-enum-item-container">
|
||||
<span>{{value}}</span>
|
||||
{{~#if folderLabel}}
|
||||
<span class="text-muted small"><span> · </span>{{folderLabel}}</span>
|
||||
{{/if~}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{~#if valueIsSet~}}
|
||||
<span class="none-value">{{translate 'None'}}</span>{{else}}<span class="loading-value"></span>
|
||||
{{~/if~}}
|
||||
{{/unless}}
|
||||
|
||||
`
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
noFolderMap = false
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {string}
|
||||
*/
|
||||
getFoldersUrl = 'EmailAccount/action/getFolders'
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {{id: string, name: string}[]}
|
||||
*/
|
||||
folderDataList
|
||||
|
||||
getAttributeList() {
|
||||
return [
|
||||
...super.getAttributeList(),
|
||||
'folderMap',
|
||||
];
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
...super.data(),
|
||||
itemDataList: this.getItemDataList(),
|
||||
};
|
||||
}
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
if (!this.noFolderMap) {
|
||||
this.loadEmailFolders();
|
||||
|
||||
this.listenTo(this.model, 'change:assignedUserId', () => this.loadEmailFolders());
|
||||
}
|
||||
|
||||
this.addHandler('change', 'select[data-role="folderId"]', () => {
|
||||
this.trigger('change');
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async loadEmailFolders() {
|
||||
this.folderDataList = await this.fetchEmailFolders();
|
||||
|
||||
await this.whenRendered();
|
||||
await this.reRender();
|
||||
}
|
||||
|
||||
setupOptions() {
|
||||
this.params.options = ['INBOX'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {Record[]}
|
||||
*/
|
||||
getItemDataList() {
|
||||
/** @type {string[]} */
|
||||
const values = this.model.attributes[this.name] ?? [];
|
||||
|
||||
return values.map(value => {
|
||||
const folderId = this.getItemMappedFolderId(value);
|
||||
|
||||
const item = this.folderDataList?.find(it => it.id === folderId);
|
||||
|
||||
return {
|
||||
value: value,
|
||||
folderLabel: item?.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fetchFolders() {
|
||||
return new Promise(resolve => {
|
||||
const data = {
|
||||
@@ -69,6 +163,108 @@ export default class extends ArrayFieldView {
|
||||
});
|
||||
}
|
||||
|
||||
afterRender() {
|
||||
super.afterRender();
|
||||
|
||||
if (this.isDetailMode()) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
getItemHtml(value) {
|
||||
const html = super.getItemHtml(value);
|
||||
|
||||
if (this.noFolderMap) {
|
||||
return html;
|
||||
}
|
||||
|
||||
let folderDataList = this.folderDataList;
|
||||
|
||||
const folderId = this.getItemMappedFolderId(value);
|
||||
|
||||
if (!folderDataList && folderId) {
|
||||
folderDataList = [{id: folderId, name: folderId}];
|
||||
}
|
||||
|
||||
if (!folderDataList) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
const item = div.querySelector('.list-group-item');
|
||||
|
||||
const group = document.createElement('div');
|
||||
group.classList.add('item-input-container');
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.className = 'form-control native-select input-sm';
|
||||
select.dataset.role = 'folderId';
|
||||
|
||||
select.append(
|
||||
(() => {
|
||||
return document.createElement('option');
|
||||
})()
|
||||
);
|
||||
|
||||
for (const item of folderDataList) {
|
||||
const option = document.createElement('option');
|
||||
|
||||
option.value = item.id;
|
||||
option.text = item.name;
|
||||
|
||||
if (folderId && item.id === folderId) {
|
||||
option.selected = true;
|
||||
option.setAttribute('selected', 'selected');
|
||||
}
|
||||
|
||||
select.append(option)
|
||||
}
|
||||
|
||||
group.append(select);
|
||||
item.append(group);
|
||||
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @return Promise.<{id: string, name: string}[]>
|
||||
*/
|
||||
async fetchEmailFolders() {
|
||||
if (!this.model.attributes.assignedUserId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collection = await this.getCollectionFactory().create('EmailFolder');
|
||||
|
||||
collection.data.select = ['id', 'name'].join(',');
|
||||
collection.where = [
|
||||
{
|
||||
attribute: 'assignedUserId',
|
||||
type: 'equals',
|
||||
value: this.model.attributes.assignedUserId,
|
||||
}
|
||||
];
|
||||
|
||||
await collection.fetch();
|
||||
|
||||
return collection.models.map(m => ({id: m.id, name: m.attributes.name}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string|null} folder
|
||||
*/
|
||||
getItemMappedFolderId(folder) {
|
||||
const map = /** @type {Record.<string, string>} */
|
||||
this.model.attributes.folderMap ?? {};
|
||||
|
||||
return map[folder] ?? null;
|
||||
}
|
||||
|
||||
actionAddItem() {
|
||||
Espo.Ui.notifyWait();
|
||||
|
||||
@@ -96,4 +292,42 @@ export default class extends ArrayFieldView {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetch() {
|
||||
const data = super.fetch();
|
||||
|
||||
if (this.noFolderMap) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const map = {};
|
||||
|
||||
/** @type {string[]} */
|
||||
const folders = data[this.name] ?? [];
|
||||
|
||||
/** @type {HTMLElement[]} */
|
||||
const items = Array.from(this.element?.querySelectorAll('.list-group-item') ?? []);
|
||||
|
||||
for (const folder of folders) {
|
||||
const item = items.find(div => div.dataset.value === folder);
|
||||
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @type {HTMLSelectElement|null} */
|
||||
const select = item.querySelector('select[data-role="folderId"]');
|
||||
|
||||
if (!select) {
|
||||
continue;
|
||||
}
|
||||
|
||||
map[folder] = select.value !== '' ? select.value : null;
|
||||
}
|
||||
|
||||
return {
|
||||
...super.fetch(),
|
||||
folderMap: map,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,25 @@
|
||||
|
||||
import FoldersView from 'views/email-account/fields/folders';
|
||||
|
||||
export default class extends FoldersView {
|
||||
export default class InboundEmailGroupFolderFieldView extends FoldersView {
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
getFoldersUrl = 'InboundEmail/action/getFolders'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
noFolderMap = true
|
||||
|
||||
/*async fetchEmailFolders() {
|
||||
if (!this.model.attributes.assignedUserId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collection = await this.getCollectionFactory().create('GroupEmailFolder');
|
||||
|
||||
collection.data.select = ['id', 'name'].join(',');
|
||||
|
||||
await collection.fetch();
|
||||
|
||||
return collection.models.map(m => ({id: m.id, name: m.attributes.name}));
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -615,6 +615,12 @@ select.form-control.native-select {
|
||||
&:hover {
|
||||
background-color: var(--dropdown-link-hover-bg);
|
||||
}
|
||||
|
||||
&:empty {
|
||||
&::checkmark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::picker-icon {
|
||||
@@ -648,6 +654,14 @@ select.form-control.native-select {
|
||||
&:has(option.text-primary:checked) {
|
||||
color: var(--state-primary-text);
|
||||
}
|
||||
|
||||
&.input-sm {
|
||||
line-height: var(--20px);
|
||||
|
||||
&::picker-icon {
|
||||
top: var(--minus-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -702,6 +716,19 @@ select.form-control.native-select {
|
||||
}
|
||||
}
|
||||
|
||||
&:has(> .item-input-container) {
|
||||
> span.text {
|
||||
width: calc(50%);
|
||||
}
|
||||
|
||||
> .item-input-container {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
width: calc(50% - var(--40px));
|
||||
padding-right: var(--5px);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(> .item-button):has(> .drag-handle) {
|
||||
> span.text {
|
||||
width: calc(100% - var(--36px) - var(--18px));
|
||||
|
||||
Reference in New Issue
Block a user