Compare commits

...

42 Commits
9.0.4 ... 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
55 changed files with 439 additions and 98 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

@@ -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"
]

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

@@ -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

@@ -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();

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()) {
@@ -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

@@ -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";

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 {

4
package-lock.json generated
View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "9.0.4",
"version": "9.0.8",
"description": "Open-source CRM.",
"repository": {
"type": "git",