mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-11 07:27:01 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dcbff1d08 | ||
|
|
61c5ad9802 | ||
|
|
6b58d30eec | ||
|
|
84f7fc562c | ||
|
|
b431f40f9f | ||
|
|
85bb013a0e | ||
|
|
aeeb779ab0 | ||
|
|
ed1f07d872 | ||
|
|
97938b8fcd | ||
|
|
54d73fa073 | ||
|
|
48ce79811f | ||
|
|
c939deb589 | ||
|
|
91740192d2 | ||
|
|
bd900d0b48 | ||
|
|
0ae0365ee5 | ||
|
|
cbcc560bd3 | ||
|
|
398743fe63 | ||
|
|
92f6759591 | ||
|
|
368c2fb866 | ||
|
|
985c6fb64b | ||
|
|
37d2d8cf4f | ||
|
|
5cfbdb21e9 | ||
|
|
da14a41387 | ||
|
|
01a40e311d | ||
|
|
8477416063 | ||
|
|
c2d7bc818e | ||
|
|
aca48f024d | ||
|
|
3f0140c716 | ||
|
|
3366a27575 | ||
|
|
a93ba33e92 | ||
|
|
09dea0be01 | ||
|
|
40157bcb8c | ||
|
|
e0e80c5a56 | ||
|
|
1954efd7e0 | ||
|
|
cb34377363 | ||
|
|
2e9437572d | ||
|
|
0e639ef6a8 | ||
|
|
5c7467e4bf | ||
|
|
b9b71b2015 | ||
|
|
87875c1a7f | ||
|
|
e802ceea86 | ||
|
|
4dfce57bd1 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,4 +63,5 @@ class FieldType
|
||||
public const WYSIWYG = 'wysiwyg';
|
||||
public const JSON_ARRAY = 'jsonArray';
|
||||
public const JSON_OBJECT = 'jsonObject';
|
||||
public const PASSWORD = 'password';
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) [];
|
||||
|
||||
@@ -318,5 +318,9 @@ return [
|
||||
'emailScheduledBatchCount' => 50,
|
||||
'emailAddressMaxCount' => 10,
|
||||
'phoneNumberMaxCount' => 10,
|
||||
'iframeSandboxExcludeDomainList' => [
|
||||
'youtube.com',
|
||||
'google.com',
|
||||
],
|
||||
'isInstalled' => false,
|
||||
];
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
["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"
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -631,10 +631,6 @@ class ActivitiesPanelView extends RelationshipPanelView {
|
||||
});
|
||||
}
|
||||
|
||||
actionRefresh() {
|
||||
this.collection.fetch();
|
||||
}
|
||||
|
||||
actionSetHeld(data) {
|
||||
const id = data.id;
|
||||
|
||||
|
||||
@@ -194,10 +194,6 @@ export default class TasksRelationshipPanelView extends RelationshipPanelView {
|
||||
});
|
||||
}
|
||||
|
||||
actionRefresh() {
|
||||
this.collection.fetch();
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
actionComplete(data) {
|
||||
const id = data.id;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('; ');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -167,6 +167,7 @@ export default class DynamicLogicConditionsStringItemBaseView extends View {
|
||||
model: this.model,
|
||||
name: this.field,
|
||||
selector: `[data-view-key="${key}"]`,
|
||||
readOnly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
|
||||
model: model,
|
||||
name: this.field,
|
||||
selector: `[data-view-key="${key}"]`,
|
||||
readOnly: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
|
||||
params: {
|
||||
options: this.getMetadata().get(['entityDefs', this.scope, 'fields', this.field, 'options']) || []
|
||||
},
|
||||
readOnly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export default class extends DynamicLogicConditionsStringItemBaseView {
|
||||
model: this.model,
|
||||
name: this.field,
|
||||
selector: `[data-view-key="${key}"]`,
|
||||
readOnly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1407,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();
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
@@ -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') || {};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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]();
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -4199,6 +4208,22 @@ body > .autocomplete-suggestions.text-search-suggestions {
|
||||
}
|
||||
}
|
||||
|
||||
.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";
|
||||
|
||||
|
||||
@@ -205,6 +205,10 @@ ul.dropdown-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> li.divider:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> li.divider + li.divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.0.4",
|
||||
"version": "9.0.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "espocrm",
|
||||
"version": "9.0.4",
|
||||
"version": "9.0.8",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "espocrm",
|
||||
"version": "9.0.4",
|
||||
"version": "9.0.8",
|
||||
"description": "Open-source CRM.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user