Compare commits

...

49 Commits
9.0.3 ... 9.0.8

Author SHA1 Message Date
Yuri Kuznetsov
3dcbff1d08 9.0.8 2025-04-15 09:53:20 +03:00
Yuri Kuznetsov
61c5ad9802 email show body plain impr 2025-04-09 21:54:03 +03:00
Yuri Kuznetsov
6b58d30eec improve html sanitize 2025-04-09 21:19:39 +03:00
Yuri Kuznetsov
84f7fc562c CSP form-action self 2025-04-09 21:11:27 +03:00
Yuri Kuznetsov
b431f40f9f update ubuntu 2025-04-08 19:43:01 +03:00
Yuri Kuznetsov
85bb013a0e 9.0.7 2025-04-08 19:30:16 +03:00
Yuri Kuznetsov
aeeb779ab0 fix upload button 2025-04-08 19:22:22 +03:00
Yuri Kuznetsov
ed1f07d872 comment 2025-04-08 19:02:10 +03:00
Yuri Kuznetsov
97938b8fcd comment 2025-04-08 17:46:36 +03:00
Yuri Kuznetsov
54d73fa073 show action button rerender if no button 2025-04-08 17:28:52 +03:00
Yuri Kuznetsov
48ce79811f fix domain 2025-03-21 10:58:24 +02:00
Yuri Kuznetsov
c939deb589 iframeSandboxExcludeDomainList 2025-03-21 10:46:26 +02:00
Yuri Kuznetsov
91740192d2 order disabled api key 2025-03-19 14:30:49 +02:00
Yuri Kuznetsov
bd900d0b48 disable order 2025-03-19 14:29:22 +02:00
Yuri Kuznetsov
0ae0365ee5 9.0.6 2025-03-19 11:08:06 +02:00
Yuri Kuznetsov
cbcc560bd3 sanitize navbar color 2025-03-19 09:33:35 +02:00
Yuri Kuznetsov
398743fe63 remove icon class 2025-03-14 12:43:08 +02:00
Yuri Kuznetsov
92f6759591 fix task reminders handler 2025-03-14 12:42:08 +02:00
Yuri Kuznetsov
368c2fb866 9.0.5 2025-03-13 17:08:24 +02:00
Yuri Kuznetsov
985c6fb64b fix free busy service 2025-03-13 17:08:16 +02:00
Yuri Kuznetsov
37d2d8cf4f navbar dropdown active bg 2025-03-12 15:51:50 +02:00
Yuri Kuznetsov
5cfbdb21e9 style fix 2025-03-11 19:01:15 +02:00
Yuri Kuznetsov
da14a41387 duration focus after update fix 2025-03-06 14:12:50 +02:00
Yuri Kuznetsov
01a40e311d link parent getSelectFilters for autocomplete 2025-02-27 17:46:11 +02:00
Yuri Kuznetsov
8477416063 sandbox=allow-scripts 2025-02-26 21:03:29 +02:00
Yuri Kuznetsov
c2d7bc818e do no show last dropdown divider 2025-02-19 11:41:35 +02:00
Yuri Kuznetsov
aca48f024d fix dynamic logic string ui 2025-02-18 09:23:59 +02:00
Yuri Kuznetsov
3f0140c716 dashlet auto refresh no notify 2025-02-17 14:53:02 +02:00
Yuri Kuznetsov
3366a27575 metadata additional fields not customizable fix 2025-02-17 11:08:24 +02:00
Yuri Kuznetsov
a93ba33e92 cleanup 2025-02-17 10:42:00 +02:00
Yuri Kuznetsov
09dea0be01 refresh panel and dashlet notify 2025-02-16 14:39:32 +02:00
Yuri Kuznetsov
40157bcb8c strip html in notification message 2025-02-16 14:08:56 +02:00
Yuri Kuznetsov
e0e80c5a56 trigger change on control+enter save 2025-02-15 16:55:44 +02:00
Yuri Kuznetsov
1954efd7e0 tab search improve 2025-02-14 22:52:33 +02:00
Yuri Kuznetsov
cb34377363 formula field style impr 2025-02-14 16:18:13 +02:00
Yuri Kuznetsov
2e9437572d fix list tree where 2025-02-14 13:15:26 +02:00
Yuri Kuznetsov
0e639ef6a8 fix docs 2025-02-14 12:03:57 +02:00
Yuri Kuznetsov
5c7467e4bf select related create fix 2025-02-14 12:00:35 +02:00
Yuri Kuznetsov
b9b71b2015 calendar month title fix 2025-02-13 21:21:20 +02:00
Yuri Kuznetsov
87875c1a7f task reminder re-appearing fix 2025-02-11 13:08:32 +02:00
Yuri Kuznetsov
e802ceea86 markdown: disable header ids 2025-02-10 16:59:13 +02:00
Yuri Kuznetsov
4dfce57bd1 css fix 2025-02-10 10:25:44 +02:00
Yuri Kuznetsov
bfd1ff5fa7 9.0.4 2025-02-09 22:51:15 +02:00
Yuri Kuznetsov
2322b2d1da fix empty group tab causing js error 2025-02-09 22:51:04 +02:00
Yuri Kuznetsov
88b0479366 field: fromView set option 2025-02-08 09:35:11 +02:00
Yuri Kuznetsov
cf4045faec disable code ligatures 2025-02-07 22:42:51 +02:00
Yuri Kuznetsov
bfc5e8054d cleanup 2025-02-07 15:26:58 +02:00
Yuri Kuznetsov
62a290c197 frontend portal acl: use accountLink and contactLink 2025-02-07 15:21:14 +02:00
Yuri Kuznetsov
ed249366f9 bundles changes 2025-02-06 20:47:08 +02:00
59 changed files with 629 additions and 139 deletions

View File

@@ -23,7 +23,7 @@ on:
jobs:
test:
name: Test on PHP ${{ matrix.php-versions }}
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
matrix:
php-versions: ['8.2', '8.3', '8.4']

View File

@@ -63,15 +63,12 @@ class RecordTree extends Record
return (object) $this->actionListTree($request->getRouteParams(), $request->getParsedBody(), $request);
}
$where = $request->getQueryParams()['where'] ?? null;
$selectParams = $this->fetchSearchParamsFromRequest($request);
$parentId = $request->getQueryParam('parentId');
$maxDepth = $request->getQueryParam('maxDepth');
$onlyNotEmpty = (bool) $request->getQueryParam('onlyNotEmpty');
if ($where !== null && !is_array($where)) {
throw new BadRequest();
}
if ($maxDepth !== null) {
$maxDepth = (int) $maxDepth;
}
@@ -79,7 +76,7 @@ class RecordTree extends Record
$collection = $this->getRecordTreeService()->getTree(
$parentId,
[
'where' => $where,
'where' => $selectParams->getWhere(),
'onlyNotEmpty' => $onlyNotEmpty,
],
$maxDepth

View File

@@ -63,4 +63,5 @@ class FieldType
public const WYSIWYG = 'wysiwyg';
public const JSON_ARRAY = 'jsonArray';
public const JSON_OBJECT = 'jsonObject';
public const PASSWORD = 'password';
}

View File

@@ -76,6 +76,10 @@ class Applier
if ($this->metadataProvider->isFieldOrderDisabled($this->entityType, $orderBy)) {
throw new Forbidden("Order by the field '$orderBy' is disabled.");
}
if ($this->metadataProvider->getFieldType($this->entityType, $orderBy) === FieldType::PASSWORD) {
throw new Forbidden("Order by field '$orderBy' is not allowed.");
}
}
if ($orderBy === null) {

View File

@@ -112,6 +112,10 @@ class ClientManager
$string .= ' ' . $src;
}
if (!$this->config->get('clientCspFormActionDisabled')) {
$string .= "; form-action 'self'";
}
// Checking the parameter for bc.
if (!$this->config->get('clientXFrameOptionsHeaderDisabled')) {
$string .= '; frame-ancestors';

View File

@@ -58,40 +58,6 @@
}
]
}
},
"reminders": {
"visible": {
"conditionGroup": [
{
"type": "and",
"value": [
{
"type": "or",
"value": [
{
"type": "isNotEmpty",
"attribute": "dateEnd"
},
{
"type": "isNotEmpty",
"attribute": "dateEndDate"
}
]
},
{
"type": "notEquals",
"attribute": "status",
"value": "Completed"
},
{
"type": "notEquals",
"attribute": "status",
"value": "Canceled"
}
]
}
]
}
}
}
},

View File

@@ -89,7 +89,8 @@
"Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\Valid",
"Espo\\Modules\\Crm\\Classes\\FieldValidators\\Event\\Reminders\\MaxCount"
],
"duplicateIgnore": true
"duplicateIgnore": true,
"dynamicLogicDisabled": true
},
"description": {
"type": "text",

View File

@@ -708,7 +708,7 @@ class Service
private function fetchWorkingRangeListForTeams(array $teamIdList, FetchParams $fetchParams): array
{
$teamList = iterator_to_array(
$this->entityManager
$this->entityManager
->getRDBRepositoryByClass(Team::class)
->where([Attribute::ID => $teamIdList])
->find()
@@ -794,6 +794,13 @@ class Service
->withFrom($params->from)
->withTo($params->to);
if ($fetchParams->getScopeList() === null) {
$fetchParams = $fetchParams->withScopeList(
$this->config->get('busyRangesEntityList') ??
[Meeting::ENTITY_TYPE, Call::ENTITY_TYPE]
);
}
$eventList = $this->fetchInternal($user, $fetchParams->withSkipAcl(), !$params->accessCheck);
$ignoreHash = (object) [];

View File

@@ -318,5 +318,9 @@ return [
'emailScheduledBatchCount' => 50,
'emailAddressMaxCount' => 10,
'phoneNumberMaxCount' => 10,
'iframeSandboxExcludeDomainList' => [
'youtube.com',
'google.com',
],
'isInstalled' => false,
];

View File

@@ -36,15 +36,20 @@
["selectDefs"],
["pdfDefs"],
["notificationDefs", "__ANY__", "assignmentNotificatorClassName"],
["authenticationMethods", "__ANY__", "implementationClassName"]
["authenticationMethods", "__ANY__", "implementationClassName"],
["aclDefs", "__ANY__", "accessCheckerClassName"],
["aclDefs", "__ANY__", "portalAccessCheckerClassName"],
["aclDefs", "__ANY__", "ownershipCheckerClassName"],
["aclDefs", "__ANY__", "portalOwnershipCheckerClassName"],
["aclDefs", "__ANY__", "assignmentCheckerClassName"],
["aclDefs", "__ANY__", "linkCheckerClassNameMap"]
],
"frontendNonAdminHiddenPathList": [
["recordDefs"],
["aclDefs"]
["recordDefs"]
],
"additionalBuilderClassNameList": [
"Espo\\Core\\Utils\\Metadata\\AdditionalBuilder\\FilterFields",
"Espo\\Core\\Utils\\Metadata\\AdditionalBuilder\\Fields",
"Espo\\Core\\Utils\\Metadata\\AdditionalBuilder\\FilterFields",
"Espo\\Core\\Utils\\Metadata\\AdditionalBuilder\\DeleteIdField",
"Espo\\Core\\Utils\\Metadata\\AdditionalBuilder\\StreamUpdatedAtField"
]

View File

@@ -45,7 +45,8 @@
"directAccessDisabled": true,
"fieldManagerParamList": [
"tooltipText"
]
],
"orderDisabled": true
},
"passwordConfirm": {
"type": "password",
@@ -81,7 +82,8 @@
"tooltipText"
],
"copyToClipboard": true,
"dynamicLogicVisibleDisabled": true
"dynamicLogicVisibleDisabled": true,
"orderDisabled": true
},
"secretKey": {
"type": "varchar",

View File

@@ -64,7 +64,7 @@ class RecordTree extends Record
protected $categoryField = null;
/**
* @param array<string, mixed> $params
* @param array{where?: ?WhereItem, onlyNotEmpty?: bool} $params
* @return ?Collection<Entity>
* @throws Forbidden
* @throws BadRequest
@@ -84,7 +84,7 @@ class RecordTree extends Record
}
/**
* @param array<string, mixed> $params
* @param array{where?: ?WhereItem, onlyNotEmpty?: bool} $params
* @return ?Collection<Entity>
* @throws BadRequest
* @throws Forbidden
@@ -104,7 +104,11 @@ class RecordTree extends Record
return null;
}
$searchParams = SearchParams::fromRaw($params);
$searchParams = SearchParams::create();
if (isset($params['where'])) {
$searchParams = $searchParams->withWhere($params['where']);
}
$selectBuilder = $this->selectBuilderFactory
->create()

View File

@@ -39,9 +39,7 @@ use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\Utils\Metadata\Helper as MetadataHelper;
use Espo\Core\Utils\Util;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\ORM\Type\AttributeType;
use Espo\Tools\EntityManager\NameUtil;
use RuntimeException;
use stdClass;

View File

@@ -53,7 +53,11 @@ class RemindersHandler {
this.listenTo(this.model, 'change', () => {
if (
!this.model.hasChanged('assignedUserId') &&
!this.model.hasChanged('assignedUsersIds')
!this.model.hasChanged('assignedUsersIds') &&
!this.model.hasChanged('assignedUsersIds') &&
!this.model.hasChanged('dateEnd') &&
!this.model.hasChanged('dateEndDate') &&
!this.model.hasChanged('status')
) {
return;
}
@@ -63,6 +67,12 @@ class RemindersHandler {
}
control() {
if (!this.model.attributes.dateEnd && !this.model.attributes.dateEndDate) {
this.view.hideField('reminders');
return;
}
/** @type {string[]} */
const assignedUsersIds = this.model.attributes.assignedUsersIds || [];

View File

@@ -453,7 +453,7 @@ class CalendarView extends View {
start + this.rangeSeparator + end :
start;
} else {
title = moment(view.currentStart).format(format);
title = this.dateToMoment(view.currentStart).format(format);
}
if (this.options.userId && this.options.userName) {

View File

@@ -176,12 +176,33 @@ class ActivitiesDashletView extends BaseDashletView {
}
actionRefresh() {
this.collection.fetch({
this.refreshInternal();
}
autoRefresh() {
this.refreshInternal({skipNotify: true});
}
/**
* @private
* @param {{skipNotify?: boolean}} [options]
* @return {Promise<void>}
*/
async refreshInternal(options = {}) {
if (!options.skipNotify) {
Espo.Ui.notify(' ... ');
}
await this.collection.fetch({
previousTotal: this.collection.total,
previousDataList: this.collection.models.map(model => {
return Espo.Utils.cloneDeep(model.attributes);
}),
});
if (!options.skipNotify) {
Espo.Ui.notify();
}
}
// noinspection JSUnusedGlobalSymbols

View File

@@ -631,10 +631,6 @@ class ActivitiesPanelView extends RelationshipPanelView {
});
}
actionRefresh() {
this.collection.fetch();
}
actionSetHeld(data) {
const id = data.id;

View File

@@ -194,10 +194,6 @@ export default class TasksRelationshipPanelView extends RelationshipPanelView {
});
}
actionRefresh() {
this.collection.fetch();
}
// noinspection JSUnusedGlobalSymbols
actionComplete(data) {
const id = data.id;

View File

@@ -12,7 +12,11 @@
</div>
<div class="message-container text-danger" style="height: 20px; margin-bottom: 10px; margin-top: 10px;"></div>
<div class="buttons-container">
<button class="btn btn-primary disabled" data-action="upload">{{translate 'Upload' scope='Admin'}}</button>
<button
class="btn btn-primary disabled"
data-action="upload"
disabled="disabled"
>{{translate 'Upload' scope='Admin'}}</button>
</div>
</div>
</div>

View File

@@ -1,16 +1,15 @@
<div class="row">
<div class="{{#if hasSide}}col-md-10 col-sm-10 col-xs-12{{else}}col-md-12{{/if}}">
<div class="formula-edit-container">
<div>
<div id="{{containerId}}">{{value}}</div>
</div>
{{#if hasSide}}
<div class="col-md-2 col-sm-2 col-xs-12">
<div>
<div class="button-container">
<div class="btn-group pull-right">
{{#if hasCheckSyntax}}
<button
type="button"
class="btn btn-default btn-sm btn-icon"
class="btn btn-text btn-sm btn-icon"
data-action="checkSyntax"
title="{{translate 'Check Syntax' scope='Formula'}}"
><span class="far fa-circle"></span></button>
@@ -18,7 +17,7 @@
{{#if hasInsert}}
<button
type="button"
class="btn btn-default btn-sm dropdown-toggle btn-icon"
class="btn btn-text btn-sm dropdown-toggle btn-icon"
data-toggle="dropdown"
><span class="fas fa-plus"></span></button>
<ul class="dropdown-menu pull-right">

View File

@@ -158,7 +158,7 @@
<div class="advanced-filters-apply-container{{#unless toShowApplyFiltersButton}} hidden{{/unless}}">
<a role="button" tabindex="0" class="btn btn-default btn-sm" data-action="applyFilters">
<span class="fas fa-search"></span>
<span class="fas fa-search fa-sm"></span>
<span class="text-apply{{#if toShowResetFiltersText}} hidden{{/if}}">{{translate 'Apply'}}</span>
<span class="text-reset{{#unless toShowResetFiltersText}} hidden{{/unless}}">{{translate 'Reset'}}</span>
</a>

View File

@@ -29,6 +29,8 @@
/** @module acl-portal */
import Acl from 'acl';
import {inject} from 'di';
import Metadata from 'metadata';
/**
* Internal class for portal access checking. Can be extended to customize access checking
@@ -36,6 +38,13 @@ import Acl from 'acl';
*/
class AclPortal extends Acl {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/** @inheritDoc */
checkScope(data, action, precise, entityAccessData) {
entityAccessData = entityAccessData || {};
@@ -171,20 +180,81 @@ class AclPortal extends Acl {
/**
* Check if a user in an account of a model.
*
* @param {module:model} model A model.
* @param {import('model').default} model A model.
* @returns {boolean|null} True if in an account, null if not clear.
*/
checkInAccount(model) {
const accountIdList = this.getUser().getLinkMultipleIdList('accounts');
const accountsIds = this.getUser().getLinkMultipleIdList('accounts');
if (!accountsIds.length) {
return false;
}
const link = this.metadata.get(`aclDefs.${model.entityType}.accountLink`);
if (link) {
const linkType = model.getLinkType(link);
if (linkType === 'belongsTo' || linkType === 'hasOne') {
const idAttribute = link + 'Id';
if (!model.has(idAttribute)) {
return null;
}
const id = model.get(idAttribute);
if (!id) {
return false;
}
return accountsIds.includes(id);
}
if (linkType === 'belongsToParent') {
const idAttribute = link + 'Id';
const typeAttribute = link + 'Type';
if (!model.has(idAttribute) || !model.has(typeAttribute)) {
return null;
}
const id = model.get(idAttribute);
if (model.get(typeAttribute) !== 'Account' || !id) {
return false;
}
return accountsIds.includes(id);
}
if (linkType === 'hasMany') {
if (!model.hasField(link) || model.getFieldType(link) !== 'linkMultiple') {
return true;
}
if (!model.has(link + 'Ids')) {
return null;
}
const ids = model.getLinkMultipleIdList(link);
for (const id of ids) {
if (accountsIds.includes(id)) {
return true;
}
}
return false;
}
if (!accountIdList.length) {
return false;
}
if (
model.hasField('account') &&
model.get('accountId') &&
accountIdList.includes(model.get('accountId'))
accountsIds.includes(model.get('accountId'))
) {
return true;
}
@@ -197,7 +267,7 @@ class AclPortal extends Acl {
}
(model.getLinkMultipleIdList('accounts')).forEach(id => {
if (accountIdList.includes(id)) {
if (accountsIds.includes(id)) {
result = true;
}
});
@@ -207,7 +277,7 @@ class AclPortal extends Acl {
model.hasField('parent') &&
model.hasLink('parent') &&
model.get('parentType') === 'Account' &&
accountIdList.includes(model.get('parentId'))
accountsIds.includes(model.get('parentId'))
) {
return true;
}
@@ -234,6 +304,53 @@ class AclPortal extends Acl {
return false;
}
const link = this.metadata.get(`aclDefs.${model.entityType}.contactLink`);
if (link) {
const linkType = model.getLinkType(link);
if (linkType === 'belongsTo' || linkType === 'hasOne') {
const idAttribute = link + 'Id';
if (!model.has(idAttribute)) {
return null;
}
return model.get(idAttribute) === contactId;
}
if (linkType === 'belongsToParent') {
const idAttribute = link + 'Id';
const typeAttribute = link + 'Type';
if (!model.has(idAttribute) || !model.has(typeAttribute)) {
return null;
}
if (model.get(typeAttribute) !== 'Contact') {
return false;
}
return model.get(idAttribute) === contactId;
}
if (linkType === 'hasMany') {
if (!model.hasField(link) || model.getFieldType(link) !== 'linkMultiple') {
return true;
}
if (!model.has(link + 'Ids')) {
return null;
}
const ids = model.getLinkMultipleIdList(link);
return ids.includes(contactId);
}
return false;
}
if (model.hasField('contact')) {
if (model.get('contactId')) {
if (contactId === model.get('contactId')) {

View File

@@ -58,6 +58,8 @@ class SelectRelatedHelper {
* primaryFilterName?: string,
* boolFilterList?: string[]|string,
* viewKey?: string,
* hasCreate?: boolean,
* onCreate?: function(): void,
* }} options
*/
process(model, link, options = {}) {
@@ -159,10 +161,14 @@ class SelectRelatedHelper {
const orderBy = filters.orderBy || panelDefs.selectOrderBy;
const orderDirection = filters.orderBy ? filters.order : panelDefs.selectOrderDirection;
const createButton = options.hasCreate === true && options.onCreate !== undefined;
/** @type {import('views/modals/select-records').default} */
let modalView;
this.view.createView('dialogSelectRelated', viewName, {
scope: scope,
multiple: true,
triggerCreateEvent: true,
filters: advanced,
massRelateEnabled: massRelateEnabled,
primaryFilterName: primaryFilterName,
@@ -171,7 +177,17 @@ class SelectRelatedHelper {
layoutName: panelDefs.selectLayout,
orderBy: orderBy,
orderDirection: orderDirection,
createButton: createButton,
onCreate: () => {
modalView.close();
if (options.onCreate) {
options.onCreate();
}
},
}, view => {
modalView = view;
view.render();
Espo.Ui.notify(false);

View File

@@ -41,6 +41,7 @@ import Selectize from 'lib!selectize';
* searched items.
* @property {'value'|'text'|'$order'|'$score'} [sortBy='$order'] Item sorting.
* @property {'asc'|'desc'} [sortDirection='asc'] Sort direction.
* @property {function()} [onFocus] On-focus callback.
*/
/**
@@ -153,6 +154,12 @@ const Select = {
},
};
if (options.onFocus) {
selectizeOptions.onFocus = function () {
options.onFocus();
};
}
if (!options.matchAnyWord) {
/** @this Selectize */
selectizeOptions.score = function (search) {

View File

@@ -61,6 +61,7 @@ class ViewHelper {
marked.setOptions({
breaks: true,
tables: false,
headerIds: false,
});
DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
@@ -76,6 +77,22 @@ class ViewHelper {
if (node instanceof HTMLOListElement && node.start && node.start > 99) {
node.removeAttribute('start');
}
if (node instanceof HTMLFormElement) {
if (node.action) {
node.removeAttribute('action');
}
if (node.hasAttribute('method')) {
node.removeAttribute('method');
}
}
if (node instanceof HTMLButtonElement) {
if (node.type === 'submit') {
node.type = 'button';
}
}
});
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
@@ -92,6 +109,29 @@ class ViewHelper {
}
}
});
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
if (data.attrName === 'style') {
const style = data.attrValue
.split(';')
.map(s => s.trim())
.filter(rule => {
const [property, value] = rule.split(':')
.map(s => s.trim().toLowerCase());
if (
property === 'position' &&
['absolute', 'fixed', 'sticky'].includes(value)
) {
return false;
}
return true;
});
data.attrValue = style.join('; ');
}
});
}
/**

View File

@@ -167,6 +167,7 @@ export default class DynamicLogicConditionsStringItemBaseView extends View {
model: this.model,
name: this.field,
selector: `[data-view-key="${key}"]`,
readOnly: true,
});
}
}

View File

@@ -72,6 +72,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
model: model,
name: this.field,
selector: `[data-view-key="${key}"]`,
readOnly: true,
});
});
}

View File

@@ -44,6 +44,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
params: {
options: this.getMetadata().get(['entityDefs', this.scope, 'fields', this.field, 'options']) || []
},
readOnly: true,
});
}
}

View File

@@ -37,12 +37,15 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
const viewName = 'views/fields/link';
const foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'fields', this.field, 'entity']) ||
this.getMetadata().get(['entityDefs', this.scope, 'links', this.field, 'entity'])
this.createView('value', viewName, {
model: this.model,
name: 'link',
selector: `[data-view-key="${key}"]`,
foreignScope: this.getMetadata().get(['entityDefs', this.scope, 'fields', this.field, 'entity']) ||
this.getMetadata().get(['entityDefs', this.scope, 'links', this.field, 'entity'])
readOnly: true,
foreignScope: foreignScope,
});
}
}

View File

@@ -41,6 +41,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
model: this.model,
name: this.field,
selector: `[data-view-key="${key}"]`,
readOnly: true,
});
}
}

View File

@@ -1421,7 +1421,6 @@ export default class extends ModalView {
"fas fa-water",
"fas fa-water-ladder",
"fas fa-wave-square",
"fas fa-web-awesome",
"fas fa-weight-hanging",
"fas fa-weight-scale",
"fas fa-wheat-awn",

View File

@@ -44,6 +44,10 @@ export default class LayoutPanelAttributesView extends ModalView {
shortcutKeys = {
/** @this LayoutPanelAttributesView */
'Control+Enter': function (e) {
if (document.activeElement instanceof HTMLInputElement) {
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
this.actionSave();
e.preventDefault();

View File

@@ -49,6 +49,10 @@ class LinkManagerEditModalView extends ModalView {
},
/** @this LinkManagerEditModalView */
'Control+Enter': function (e) {
if (document.activeElement instanceof HTMLInputElement) {
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
this.save();
e.preventDefault();

View File

@@ -177,16 +177,38 @@ class RecordListDashletView extends BaseDashletView {
}
actionRefresh() {
this.refreshInternal();
}
autoRefresh() {
this.refreshInternal({skipNotify: true});
}
/**
* @private
* @param {{skipNotify?: boolean}} [options]
* @return {Promise<void>}
*/
async refreshInternal(options = {}) {
if (!this.collection) {
return;
}
if (!options.skipNotify) {
Espo.Ui.notify(' ... ');
}
this.collection.where = this.searchManager.getWhere();
this.collection.fetch({
await this.collection.fetch({
previousDataList: this.collection.models.map(model => {
return Espo.Utils.cloneDeep(model.attributes);
}),
});
if (!options.skipNotify) {
Espo.Ui.notify();
}
}
// noinspection JSUnusedGlobalSymbols

View File

@@ -32,7 +32,44 @@ class IframeDashletView extends BaseDashletView {
name = 'Iframe'
templateContent = '<iframe style="margin: 0; border: 0;"></iframe>'
/**
* @private
* @type {boolean}
*/
sandboxDisabled = false
// language=Handlebars
templateContent = `
<iframe
style="margin: 0; border: 0;"
{{#unless viewObject.sandboxDisabled}}
sandbox="allow-scripts"
{{/unless}}
></iframe>
`
setup() {
const url = this.getOption('url');
/** @type {string[]} */
const excludeDomains = this.getConfig().get('iframeSandboxExcludeDomainList') || [];
if (url) {
for (const domain of excludeDomains) {
try {
const urlObject = new URL(url);
if (urlObject.hostname === domain) {
this.sandboxDisabled = true;
break;
}
} catch (e) {
console.warn(`Invalid URL ${url}.`);
}
}
}
}
afterRender() {
const $iframe = this.$el.find('iframe');

View File

@@ -56,7 +56,9 @@ class BaseDashletOptionsModalView extends ModalView {
shortcutKeys = {
/** @this BaseDashletOptionsModalView */
'Control+Enter': 'save',
'Control+Enter': function (e) {
this.handleShortcutKeyCtrlEnter(e);
},
/** @this BaseDashletOptionsModalView */
'Escape': function (e) {
if (this.saveDisabled) {
@@ -235,6 +237,21 @@ class BaseDashletOptionsModalView extends ModalView {
this.getRecordView().showField(name);
}
/**
* @protected
* @param {KeyboardEvent} e
*/
handleShortcutKeyCtrlEnter(e) {
e.preventDefault();
e.stopPropagation();
if (document.activeElement instanceof HTMLInputElement) {
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
this.actionSave();
}
}
export default BaseDashletOptionsModalView;

View File

@@ -35,11 +35,31 @@ class StreamDashletView extends BaseDashletView {
templateContent = '<div class="list-container">{{{list}}}</div>'
actionRefresh() {
this.refreshInternal();
}
autoRefresh() {
this.refreshInternal({skipNotify: true});
}
/**
* @private
* @param {{skipNotify?: boolean}} [options]
* @return {Promise<void>}
*/
async refreshInternal(options = {}) {
if (!this.getRecordView()) {
return;
}
this.getRecordView().showNewRecords();
if (!options.skipNotify) {
Espo.Ui.notify(' ... ');
}
await this.getRecordView().showNewRecords();
if (!options.skipNotify) {
Espo.Ui.notify();
}
}
afterRender() {

View File

@@ -32,7 +32,14 @@ export default class extends ModalView {
backdrop = true
templateContent = `<div class="field" data-name="body-plain">{{{bodyPlain}}}</div>`
templateContent = `
<div class="panel no-side-margin">
<div class="panel-body">
<div class="field" data-name="body-plain">{{{bodyPlain}}}</div>
</div>
</div>
`
setup() {
super.setup();
@@ -52,6 +59,7 @@ export default class extends ModalView {
params: {
readOnly: true,
inlineEditDisabled: true,
displayRawText: true,
},
},
});

View File

@@ -937,7 +937,10 @@ class BaseFieldView extends View {
this.listenTo(this, 'change', () => {
const attributes = this.fetch();
this.model.set(attributes, {ui: true});
this.model.set(attributes, {
ui: true,
fromView: this,
});
});
}
}
@@ -1404,6 +1407,11 @@ class BaseFieldView extends View {
if (key === 'Control+Enter') {
e.stopPropagation();
if (document.activeElement instanceof HTMLInputElement) {
// Fields may need to fetch data first.
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
this.fetchToModel();
this.inlineEditSave();

View File

@@ -38,6 +38,12 @@ class DurationFieldView extends EnumFieldView {
detailTemplate = 'fields/varchar/detail'
editTemplate = 'fields/duration/edit'
/**
* @private
* @type {boolean}
*/
_justFocused = false
data() {
const valueIsSet = this.model.has(this.startField) && this.model.has(this.endField);
@@ -342,6 +348,10 @@ class DurationFieldView extends EnumFieldView {
callback(list);
},
onFocus: () => {
this._justFocused = true;
setTimeout(() => this._justFocused = false, 150);
},
});
}
}
@@ -435,7 +445,7 @@ class DurationFieldView extends EnumFieldView {
updateDuration() {
const seconds = this.seconds;
if (this.isEditMode() && this.$duration && this.$duration.length) {
if (this.isEditMode() && this.$duration && this.$duration.length && !this._justFocused) {
const options = this.getOptions().map(value => {
return {
value: value.toString(),

View File

@@ -508,6 +508,12 @@ class LinkParentFieldView extends BaseFieldView {
url += '&' + $.param({'primaryFilter': primary});
}
const advanced = this.getSelectFilters();
if (advanced && Object.keys(advanced).length) {
url += '&' + $.param({'where': advanced});
}
return url;
}

View File

@@ -171,6 +171,23 @@ class GlobalSearchView extends SiteNavbarItemView {
return false;
})
.sort((a, b) => {
if (
a.lowerLabel.startsWith(lower) &&
!b.lowerLabel.startsWith(lower)
) {
return -1;
}
if (
!a.lowerLabel.startsWith(lower) &&
b.lowerLabel.startsWith(lower)
) {
return 1;
}
return a.lowerLabel.localeCompare(b.lowerLabel);
})
.map(it => ({
value: it.label,
url: it.url,
@@ -331,10 +348,9 @@ class GlobalSearchView extends SiteNavbarItemView {
*/
getTabDataList() {
/** @type {module:views/global-search/global-search~tabData[]}*/
const list = [];
let list = [];
/**
*
* @param {string|TabsHelper~item} item
* @return {module:views/global-search/global-search~tabData}
*/
@@ -351,9 +367,13 @@ class GlobalSearchView extends SiteNavbarItemView {
};
};
/**
* @param {string|TabsHelper~item} item
* @return {boolean}
*/
const checkTab = (item) => {
return (this.tabsHelper.isTabScope(item) || this.tabsHelper.isTabUrl(item)) &&
this.tabsHelper.checkTabAccess(item)
this.tabsHelper.checkTabAccess(item);
}
for (const item of this.tabsHelper.getTabList()) {
@@ -363,7 +383,7 @@ class GlobalSearchView extends SiteNavbarItemView {
continue;
}
if (this.tabsHelper.isTabGroup(item)) {
if (this.tabsHelper.isTabGroup(item) && item.itemList) {
for (const subItem of item.itemList) {
if (checkTab(subItem)) {
list.push(toData(subItem));
@@ -392,6 +412,7 @@ class GlobalSearchView extends SiteNavbarItemView {
.forEach(it => {
it.itemList
.filter(it => it.tabQuickSearch && it.label)
.filter(it => !list.find(subIt => subIt.url === it.url))
.forEach(it => {
const label = this.translate(it.label, 'labels', 'Admin');
@@ -405,6 +426,12 @@ class GlobalSearchView extends SiteNavbarItemView {
});
}
list = list
.filter((it, i) => {
return list.findIndex(subIt => subIt.url === it.url) === i &&
list.findIndex(subIt => subIt.lowerLabel === it.lowerLabel) === i
});
/** @type {Record<string, {tab: boolean}>} */
const scopes = this.getMetadata().get('scopes') || {};

View File

@@ -167,6 +167,9 @@ class MainView extends View {
itemList.forEach(item => {
const viewObject = this;
// @todo Set _reRenderHeaderOnSync to true if `acl` is set `ascScope` is not set?
// Set _reRenderHeaderOnSync in `addMenuItem` method.
if (
(item.initFunction || item.checkVisibilityFunction) &&
(item.handler || item.data && item.data.handler)
@@ -702,8 +705,20 @@ class MainView extends View {
}
const processUi = () => {
this.$headerActionsContainer.find(`li > .action[data-name="${name}"]`).parent().removeClass('hidden');
this.$headerActionsContainer.find(`a.action[data-name="${name}"]`).removeClass('hidden');
const $dropdownItem = this.$headerActionsContainer.find(`li > .action[data-name="${name}"]`).parent();
const $button = this.$headerActionsContainer.find(`a.action[data-name="${name}"]`);
// Item can be available but not rendered as it was skipped by access check in getMenu.
if (item && !$dropdownItem.length && !$button.length) {
if (this.getHeaderView()) {
this.getHeaderView().reRender();
}
return;
}
$dropdownItem.removeClass('hidden');
$button.removeClass('hidden');
this.controlMenuDropdownVisibility();
this.adjustButtons();

View File

@@ -66,6 +66,11 @@ class EditModalView extends ModalView {
e.preventDefault();
e.stopPropagation();
if (document.activeElement instanceof HTMLInputElement) {
// Fields may need to fetch data first.
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
this.actionSave();
},
/** @this EditModalView */

View File

@@ -100,6 +100,7 @@ class SelectRecordsModalView extends ModalView {
* @property {function(): Promise<Record>} [createAttributesProvider] Create-attributes provider.
* @property {Record} [createAttributes] Create-attributes.
* @property {function(import('model').default[])} [onSelect] On record select. As of 9.0.0.
* @property {function()} [onCreate] On create click. As of 9.0.5.
*/
/**
@@ -113,6 +114,11 @@ class SelectRecordsModalView extends ModalView {
/** @private */
this.onSelect = options.onSelect;
}
if (options.onCreate) {
/** @private */
this.onCreate = options.onCreate;
}
}
data() {
@@ -388,6 +394,13 @@ class SelectRecordsModalView extends ModalView {
}
create() {
if (this.onCreate) {
this.onCreate();
return;
}
// @todo Remove in v9.1.0. Kept bc.
if (this.options.triggerCreateEvent) {
this.trigger('create');

View File

@@ -27,7 +27,6 @@
************************************************************************/
import BaseNotificationItemView from 'views/notification/items/base';
import {marked} from 'marked';
import DOMPurify from 'dompurify';
class MessageNotificationItemView extends BaseNotificationItemView {
@@ -45,7 +44,8 @@ class MessageNotificationItemView extends BaseNotificationItemView {
const data = /** @type {Object.<string, *>} */this.model.get('data') || {};
const messageRaw = this.model.get('message') || data.message || '';
const message = marked.parse(messageRaw);
const message = this.getHelper().transformMarkdownText(messageRaw);
this.messageTemplate = DOMPurify.sanitize(message, {}).toString();

View File

@@ -3990,6 +3990,11 @@ class DetailRecordView extends BaseRecordView {
e.preventDefault();
e.stopPropagation();
if (document.activeElement instanceof HTMLInputElement) {
// Fields may need to fetch data first.
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
}
const methodName = 'action' + Espo.Utils.upperCaseFirst(action);
this[methodName]();

View File

@@ -621,8 +621,12 @@ class RelationshipPanelView extends BottomPanelView {
*
* @protected
*/
actionRefresh() {
this.collection.fetch();
async actionRefresh() {
Espo.Ui.notify(' ... ');
await this.collection.fetch()
Espo.Ui.notify();
}
/**
@@ -885,7 +889,10 @@ class RelationshipPanelView extends BottomPanelView {
actionSelectRelated() {
const helper = new SelectRelatedHelper(this);
helper.process(this.model, this.link);
helper.process(this.model, this.link, {
hasCreate: this.defs.create,
onCreate: () => this.actionCreateRelated(),
});
}
/**

View File

@@ -385,8 +385,12 @@ class SidePanelView extends View {
/**
* A `refresh` action.
*/
actionRefresh() {
this.model.fetch();
async actionRefresh() {
Espo.Ui.notify(' ... ');
await this.model.fetch();
Espo.Ui.notify();
}
// noinspection JSUnusedGlobalSymbols

View File

@@ -1286,6 +1286,13 @@ class NavbarSiteView extends View {
color = this.getMetadata().get(['clientDefs', tab, 'color']);
}
if (
color &&
!/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(color)
) {
color = null;
}
if (!params.tabIconsDisabled && !isGroup && !isDivider && !isUrl) {
iconClass = this.getMetadata().get(['clientDefs', tab, 'iconClass'])
}

View File

@@ -852,10 +852,16 @@ class PanelStreamView extends RelationshipPanelView {
return this.getView('list')
}
actionRefresh() {
if (this.getListView()) {
this.getListView().showNewRecords();
async actionRefresh() {
if (!this.getListView()) {
return;
}
Espo.Ui.notify(' ... ');
await this.getListView().showNewRecords();
Espo.Ui.notify();
}
preview() {

View File

@@ -62,7 +62,7 @@ class UserAvatarFieldView extends ImageFieldView {
syncModels();
this.listenTo(this.model, 'change:avatarColor', (m, v, o) => {
if (!o.ui) {
if (o.fromView !== this) {
syncModels();
}
});

View File

@@ -2,11 +2,11 @@
"order": [
"main",
"admin",
"extra",
"crm",
"chart",
"calendar",
"timeline",
"extra"
"timeline"
],
"basePath": "client",
"transpiledPath": "client/lib/transpiled",
@@ -21,35 +21,50 @@
"src/loader.js"
],
"patterns": [
"src/*.js",
"src/ui/**/*.js",
"src/views/*.js",
"src/views/site/*.js",
"src/views/fields/*.js",
"src/views/record/**/*.js",
"src/views/search/**/*.js",
"src/views/modals/*.js",
"src/views/dashlets/**/*.js",
"src/views/global-search/*.js",
"src/views/stream/**/*.js",
"src/views/note/**/*.js",
"src/views/notification/*.js",
"src/controllers/!(admin|inbound-email|layout-set|api-user|role|portal-role).js",
"src/models/*.js",
"src/collections/*.js",
"src/acl/*.js"
"src/**/*.js"
],
"ignorePatterns": [
"src/views/admin/**/*.js",
"src/views/settings/**/*.js",
"src/views/authentication-provider/**/*.js",
"src/views/webhooks/**/*.js",
"src/views/templates/**/*.js",
"src/views/layout-set/**/*.js",
"src/views/role/**/*.js",
"src/views/portal-role/**/*.js",
"src/views/scheduled-job/**/*.js",
"src/views/lead-capture-log-record/**/*.js",
"src/views/inbound-email/**/*.js",
"src/views/extension/**/*.js",
"src/views/api-user/**/*.js",
"src/controllers/admin.js",
"src/controllers/role.js",
"src/controllers/portal-role.js",
"src/views/outbound-email/**/*.js",
"src/views/personal-data/**/*.js",
"src/views/portal/**/*.js",
"src/views/import/**/*.js",
"src/views/import-error/**/*.js",
"src/views/group-email-folder/**/*.js",
"src/views/external-account/**/*.js",
"src/views/email-account/**/*.js"
]
},
"admin": {
"patterns": [
"src/views/admin/**/*.js",
"src/views/settings/**/*.js",
"src/views/api-user/**/*.js",
"src/views/extension/**/*.js",
"src/views/authentication-provider/**/*.js",
"src/views/inbound-email/**/*.js",
"src/views/templates/**/*.js",
"src/views/role/**/*.js",
"src/views/portal-role/**/*.js",
"src/views/portal/**/*.js",
"src/views/scheduled-job/**/*.js",
"src/views/lead-capture/**/*.js",
"src/views/lead-capture-log-record/**/*.js",
"src/views/webhooks/**/*.js",
"src/views/layout-set/**/*.js",
@@ -67,7 +82,8 @@
],
"lookupPatterns": [
"modules/crm/src/**/*.js"
]
],
"noDuplicates": true
},
"chart": {
"patterns": [

View File

@@ -1,11 +1,11 @@
// Code (inline and block)
// Inline and block code styles
code,
kbd,
pre,
samp {
font-family: @font-family-monospace;
font-family: var(--font-family-monospace);
font-variant-ligatures: none;
}
// Inline code

View File

@@ -2731,22 +2731,31 @@ table.table td.cell .html-container {
margin-top: var(--line-height-computed);
margin-bottom: calc(var(--line-height-computed) / 2);
}
h2, h3 {
font-weight: 600;
margin-top: calc(var(--line-height-computed) / 2 * 1.2);
margin-bottom: calc(var(--line-height-computed) / 2);
}
h4 {
font-weight: 500;
}
h4, h5, h6 {
font-weight: normal;
margin-top: calc(var(--line-height-computed) / 2);
margin-bottom: calc(var(--line-height-computed) / 2);
}
h1 {
font-size: calc(var(--font-size-base) * 1.2);
}
h2 {
font-size: calc(var(--font-size-base) * 1.1);
}
h3, h4, h5, h6 {
font-size: var(--font-size-base);
}
@@ -4191,6 +4200,30 @@ body > .autocomplete-suggestions.text-search-suggestions {
margin-top: var(--3px);
}
.ace-tm {
font-variant-ligatures: none;
.ace_content {
font-variant-ligatures: none;
}
}
.formula-edit-container {
&:has(> :last-child:nth-child(2)) {
clear: both;
> div:first-child {
float: left;
width: calc(100% - var(--70px));
}
> div:last-child {
float: right;
width: var(--70px);
}
}
}
@import "misc/kanban.less";
@import "misc/wysiwyg.less";

View File

@@ -205,6 +205,10 @@ ul.dropdown-menu {
display: none;
}
> li.divider:last-child {
display: none;
}
> li.divider + li.divider {
display: none;
}

View File

@@ -73,6 +73,13 @@
background-color: var(--navbar-inverse-link-active-bg);
}
}
li.tab.active {
> a {
background-color: var(--dropdown-link-hover-bg);
color: var(--dropdown-link-hover-color);
}
}
}
li.in-more.dropdown {

14
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "espocrm",
"version": "9.0.3",
"version": "9.0.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "espocrm",
"version": "9.0.3",
"version": "9.0.8",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
@@ -55,7 +55,7 @@
"devDependencies": {
"archiver": "^5.3.0",
"chromedriver": "^127.0.1",
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.0",
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.1",
"fstream": ">=1.0.12",
"glob": "^10.2.6",
"grunt": "^1.5.3",
@@ -3256,8 +3256,8 @@
}
},
"node_modules/espo-frontend-build-tools": {
"version": "0.1.0",
"resolved": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#4590acc6a7849f91f89c9ceab11581fbe099ed46",
"version": "0.1.1",
"resolved": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#7f4723b205cd35c602dc5c5aa81a21ad0a7ce0c1",
"dev": true,
"dependencies": {
"@babel/cli": "^7.25.0",
@@ -9686,9 +9686,9 @@
}
},
"espo-frontend-build-tools": {
"version": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#4590acc6a7849f91f89c9ceab11581fbe099ed46",
"version": "git+ssh://git@github.com/espocrm/frontend-build-tools.git#7f4723b205cd35c602dc5c5aa81a21ad0a7ce0c1",
"dev": true,
"from": "espo-frontend-build-tools@github:espocrm/frontend-build-tools#0.1.0",
"from": "espo-frontend-build-tools@github:espocrm/frontend-build-tools#0.1.1",
"requires": {
"@babel/cli": "^7.25.0",
"@babel/core": "^7.25.0",

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "9.0.3",
"version": "9.0.8",
"description": "Open-source CRM.",
"repository": {
"type": "git",
@@ -19,7 +19,7 @@
"devDependencies": {
"archiver": "^5.3.0",
"chromedriver": "^127.0.1",
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.0",
"espo-frontend-build-tools": "github:espocrm/frontend-build-tools#0.1.1",
"fstream": ">=1.0.12",
"glob": "^10.2.6",
"grunt": "^1.5.3",