From 2863654f8feb0bc5acbfafeba0477b2fdb3092f0 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 21 Feb 2025 11:50:46 +0200 Subject: [PATCH] edit modal helper ref --- client/src/helpers/record-modal.js | 185 ++++++++++++++++-- client/src/helpers/record/create-related.js | 80 ++++---- client/src/ui.js | 9 + .../email/fields/from-address-varchar.js | 2 +- client/src/views/fields/link-multiple.js | 2 +- client/src/views/fields/link-parent.js | 2 +- client/src/views/fields/link.js | 2 +- client/src/views/global-search/name-field.js | 2 +- client/src/views/list-related.js | 65 +----- client/src/views/modals/edit.js | 6 +- client/src/views/record/list.js | 83 ++++---- .../src/views/record/panels/relationship.js | 2 +- 12 files changed, 268 insertions(+), 172 deletions(-) diff --git a/client/src/helpers/record-modal.js b/client/src/helpers/record-modal.js index f766226ed2..4f01f35a03 100644 --- a/client/src/helpers/record-modal.js +++ b/client/src/helpers/record-modal.js @@ -29,9 +29,10 @@ import {inject} from 'di'; import Metadata from 'metadata'; import AclManager from 'acl-manager'; +import Router from 'router'; /** - * A record-modal helper. + * A record-modal helper. Use to render the quick view and quick edit modals. */ class RecordModalHelper { @@ -50,22 +51,34 @@ class RecordModalHelper { acl /** - * @param {module:view} view + * @private + * @type {Router} + */ + @inject(Router) + router + + /** + * Show the 'detail' modal. + * + * @param {import('view').default} view * @param {{ * id: string, - * scope: string, - * model?: module:model, + * entityType: string, + * model?: import('model').default, * editDisabled?: boolean, * rootUrl?: string, + * afterSave?: function(import('model').default), + * afterDestroy?: function(import('model').default), * }} params - * @return {Promise} + * @return {Promise} */ - showDetail(view, params) { + async showDetail(view, params) { const id = params.id; - const scope = params.scope; + // noinspection JSUnresolvedReference + const entityType = params.entityType || params.scope; const model = params.model; - if (!id || !scope) { + if (!id || !entityType) { console.error("Bad data."); return Promise.reject(); @@ -75,27 +88,163 @@ class RecordModalHelper { return Promise.reject(); } - const viewName = this.metadata.get(['clientDefs', scope, 'modalViews', 'detail']) || + const viewName = this.metadata.get(`clientDefs.${entityType}.modalViews.detail`) || 'views/modals/detail'; - Espo.Ui.notify(' ... '); + Espo.Ui.notifyWait() const options = { - scope: scope, + entityType: entityType, model: model, id: id, quickEditDisabled: params.editDisabled, rootUrl: params.rootUrl, }; - return view.createView('modal', viewName, options, modalView => { - modalView.render() - .then(() => Espo.Ui.notify(false)); + Espo.Ui.notifyWait(); - view.listenToOnce(modalView, 'remove', () => { - view.clearView('modal'); - }); - }); + const modalView = /** @type {import('views/modals/detail').default} */ + await view.createView('modal', viewName, options); + + // @todo Revise. + view.listenToOnce(modalView, 'remove', () => view.clearView('modal')); + + if (params.afterSave) { + modalView.listenTo(modalView, 'after:save', model => params.afterSave(model)); + } + + if (params.afterDestroy) { + modalView.listenToOnce(modalView, 'after:destroy', model => params.afterDestroy(model)); + } + + await modalView.render(); + + Espo.Ui.notify(); + + return modalView; + } + + /** + * Show the 'edit' modal. + * + * @param {import('view').default} view + * @param {{ + * entityType: string, + * id?: string, + * model?: import('model').default, + * rootUrl?: string, + * noFullForm?: boolean, + * returnUrl?: string, + * afterSave?: function(import('model').default), + * returnDispatchParams?: { + * controller: string, + * action: string|null, + * options: {isReturn?: boolean} & Record, + * }, + * }} params + * @return {Promise} + * @since 9.1.0 + */ + async showEdit(view, params) { + const id = params.id; + const entityType = params.entityType; + const model = params.model; + + const viewName = this.metadata.get(`clientDefs.${entityType}.modalViews.edit`) || + 'views/modals/edit'; + + /** @type {module:views/modals/detail~options} */ + const options = { + entityType: entityType, + id: id, + model: model, + fullFormDisabled: params.noFullForm, + returnUrl: params.returnUrl || this.router.getCurrentUrl(), + returnDispatchParams: params.returnDispatchParams, + }; + + if (params.rootUrl) { + options.rootUrl = params.rootUrl; + } + + Espo.Ui.notifyWait(); + + const modalView = /** @type {import('views/modals/edit').default} */ + await view.createView('modal', viewName, options) + + // @todo Revise. + modalView.listenToOnce(modalView, 'remove', () => view.clearView('modal')); + + if (params.afterSave) { + modalView.listenTo(modalView, 'after:save', model => params.afterSave(model)); + } + + await modalView.render(); + + Espo.Ui.notify(); + + return modalView; + } + + /** + * Show the 'create' modal. + * + * @param {import('view').default} view + * @param {{ + * entityType: string, + * rootUrl?: string, + * noFullForm?: boolean, + * returnUrl?: string, + * relate?: model:model~setRelateItem | model:model~setRelateItem[], + * attributes?: Record., + * afterSave?: function(import('model').default), + * returnDispatchParams?: { + * controller: string, + * action: string|null, + * options: {isReturn?: boolean} & Record, + * }, + * }} params + * @return {Promise} + * @since 9.1.0 + */ + async showCreate(view, params) { + const entityType = params.entityType; + + const viewName = this.metadata.get(`clientDefs.${entityType}.modalViews.edit`) || + 'views/modals/edit'; + + /** @type {module:views/modals/edit~options} */ + const options = { + entityType: entityType, + fullFormDisabled: params.noFullForm, + returnUrl: params.returnUrl || this.router.getCurrentUrl(), + returnDispatchParams: params.returnDispatchParams, + relate: params.relate, + attributes: params.attributes, + }; + + if (params.rootUrl) { + options.rootUrl = params.rootUrl; + } + + Espo.Ui.notifyWait(); + + const modalView = /** @type {import('views/modals/edit').default} */ + await view.createView('modal', viewName, options) + + + // @todo Revise. + modalView.listenToOnce(modalView, 'remove', () => view.clearView('modal')); + + if (params.afterSave) { + modalView.listenTo(modalView, 'after:save', model => params.afterSave(model)); + } + + await modalView.render(); + + Espo.Ui.notify(); + + return modalView; } } diff --git a/client/src/helpers/record/create-related.js b/client/src/helpers/record/create-related.js index 54f1c92b72..037a8d6e1f 100644 --- a/client/src/helpers/record/create-related.js +++ b/client/src/helpers/record/create-related.js @@ -28,6 +28,7 @@ import {inject} from 'di'; import Metadata from 'metadata'; +import RecordModal from 'helpers/record-modal'; /** * @internal @@ -52,65 +53,52 @@ class CreateRelatedHelper { /** * @param {import('model').default} model * @param {string} link - * @param {Record} options + * @param {{ + * afterSave: function(import('model').default), + * }} [options] */ - process(model, link, options = {}) { + async process(model, link, options = {}) { const scope = model.defs['links'][link].entity; const foreignLink = model.defs['links'][link].foreign; + /** @type {Record} */ + const panelDefs = this.metadata.get(`clientDefs.${model.entityType}.relationshipPanels.${link}`) || {}; + + const attributeMap = panelDefs.createAttributeMap || {}; + const handler = panelDefs.createHandler; + let attributes = {}; - const attributeMap = this.metadata - .get(['clientDefs', model.entityType, 'relationshipPanels', link, 'createAttributeMap']) || {}; + Object.keys(attributeMap).forEach(attr => attributes[attributeMap[attr]] = model.get(attr)); - Object.keys(attributeMap) - .forEach(attr => attributes[attributeMap[attr]] = model.get(attr)); + if (handler) { + const Handler = await Espo.loader.requirePromise(handler); + /** @type {{getAttributes: function(import('model').default): Promise}} */ + const handlerObj = new Handler(this.view.getHelper()); - Espo.Ui.notify(' ... '); + const additionalAttributes = await handlerObj.getAttributes(model); - const handler = this.metadata - .get(['clientDefs', model.entityType, 'relationshipPanels', link, 'createHandler']); - - new Promise(resolve => { - if (!handler) { - resolve({}); - - return; - } - - Espo.loader.requirePromise(handler) - .then(Handler => new Handler(this.view.getHelper())) - .then(handler => { - handler.getAttributes(model) - .then(attributes => resolve(attributes)); - }); - }).then(additionalAttributes => { attributes = {...attributes, ...additionalAttributes}; + } - const viewName = this.metadata.get(['clientDefs', scope, 'modalViews', 'edit']) || 'views/modals/edit'; + const helper = new RecordModal(); - this.view.createView('quickCreate', viewName, { - scope: scope, - relate: { - model: model, - link: foreignLink, - }, - attributes: attributes, - }, view => { - view.render(); - view.notify(false); + await helper.showCreate(this.view, { + entityType: scope, + relate: { + model: model, + link: foreignLink, + }, + attributes: attributes, + afterSave: m => { + if (options.afterSave) { + options.afterSave(m); + } - this.view.listenToOnce(view, 'after:save', () => { - if (options.fromSelectRelated) { - setTimeout(() => this.view.clearView('dialogSelectRelated'), 25); - } - - model.trigger(`update-related:${link}`); - - model.trigger('after:relate'); - model.trigger(`after:relate:${link}`); - }); - }); + model.trigger(`update-related:${link}`); + model.trigger('after:relate'); + model.trigger(`after:relate:${link}`); + }, }); } } diff --git a/client/src/ui.js b/client/src/ui.js index be30a55970..80b0837f40 100644 --- a/client/src/ui.js +++ b/client/src/ui.js @@ -1190,6 +1190,15 @@ Espo.Ui = { * @property {boolean} [suppress] Suppress other warning alerts while this is displayed. */ + /** + * Show the spinner. + * + * @since 9.1.0 + */ + notifyWait: function () { + Espo.Ui.notify(' ... '); + }, + /** * Show a notify-message. * diff --git a/client/src/views/email/fields/from-address-varchar.js b/client/src/views/email/fields/from-address-varchar.js index 06b09b36dc..1ecaffb823 100644 --- a/client/src/views/email/fields/from-address-varchar.js +++ b/client/src/views/email/fields/from-address-varchar.js @@ -587,7 +587,7 @@ class EmailFromAddressVarchar extends BaseFieldView { helper.showDetail(this, { id: data.id, - scope: data.scope, + entityType: data.scope, }); } } diff --git a/client/src/views/fields/link-multiple.js b/client/src/views/fields/link-multiple.js index 2f2df8f032..37a9ea2691 100644 --- a/client/src/views/fields/link-multiple.js +++ b/client/src/views/fields/link-multiple.js @@ -1060,7 +1060,7 @@ class LinkMultipleFieldView extends BaseFieldView { helper.showDetail(this, { id: id, - scope: entityType, + entityType: entityType, }); } diff --git a/client/src/views/fields/link-parent.js b/client/src/views/fields/link-parent.js index 94106b7b87..6ca6cd0820 100644 --- a/client/src/views/fields/link-parent.js +++ b/client/src/views/fields/link-parent.js @@ -732,7 +732,7 @@ class LinkParentFieldView extends BaseFieldView { helper.showDetail(this, { id: id, - scope: entityType, + entityType: entityType, }); } } diff --git a/client/src/views/fields/link.js b/client/src/views/fields/link.js index 32287ef164..bed4eda391 100644 --- a/client/src/views/fields/link.js +++ b/client/src/views/fields/link.js @@ -1098,7 +1098,7 @@ class LinkFieldView extends BaseFieldView { helper.showDetail(this, { id: id, - scope: entityType, + entityType: entityType, }); } diff --git a/client/src/views/global-search/name-field.js b/client/src/views/global-search/name-field.js index 360d4f63ef..b88276231d 100644 --- a/client/src/views/global-search/name-field.js +++ b/client/src/views/global-search/name-field.js @@ -68,7 +68,7 @@ class GlobalSearchNameFieldView extends BaseFieldView { helper.showDetail(this, { id: this.model.id, - scope: this.model.attributes._scope, + entityType: this.model.attributes._scope, }); } } diff --git a/client/src/views/list-related.js b/client/src/views/list-related.js index 24bb31f215..62dd1b296d 100644 --- a/client/src/views/list-related.js +++ b/client/src/views/list-related.js @@ -30,6 +30,8 @@ import MainView from 'views/main'; import SearchManager from 'search-manager'; +import RecordModal from 'helpers/record-modal'; +import CreateRelatedHelper from 'helpers/record/create-related'; /** * A list-related view. @@ -632,65 +634,16 @@ class ListRelatedView extends MainView { * * @protected */ - actionQuickCreate() { + async actionQuickCreate() { const link = this.link; - const foreignScope = this.foreignScope; - const foreignLink = this.model.getLinkParam(link, 'foreign'); - let attributes = {}; + const helper = new CreateRelatedHelper(this); - const attributeMap = this.getMetadata() - .get(['clientDefs', this.scope, 'relationshipPanels', link, 'createAttributeMap']) || {}; - - Object.keys(attributeMap) - .forEach(attr => { - attributes[attributeMap[attr]] = this.model.get(attr); - }); - - Espo.Ui.notify(' ... '); - - const handler = this.getMetadata() - .get(['clientDefs', this.scope, 'relationshipPanels', link, 'createHandler']); - - (new Promise(resolve => { - if (!handler) { - resolve({}); - - return; - } - - Espo.loader.requirePromise(handler) - .then(Handler => new Handler(this.getHelper())) - .then(handler => { - handler.getAttributes(this.model) - .then(attributes => resolve(attributes)); - }); - })) - .then(additionalAttributes => { - attributes = {...attributes, ...additionalAttributes}; - - const viewName = this.getMetadata() - .get(['clientDefs', foreignScope, 'modalViews', 'edit']) || 'views/modals/edit'; - - this.createView('quickCreate', viewName, { - scope: foreignScope, - relate: { - model: this.model, - link: foreignLink, - }, - attributes: attributes, - }, view => { - view.render(); - view.notify(false); - - this.listenToOnce(view, 'after:save', () => { - this.collection.fetch(); - - this.model.trigger('after:relate'); - this.model.trigger('after:relate:' + link); - }); - }); - }); + return helper.process(this.model, link, { + afterSave: () => { + this.collection.fetch() + }, + }); } // noinspection JSUnusedGlobalSymbols diff --git a/client/src/views/modals/edit.js b/client/src/views/modals/edit.js index f58413d9fd..c7a62a2f18 100644 --- a/client/src/views/modals/edit.js +++ b/client/src/views/modals/edit.js @@ -139,7 +139,11 @@ class EditModalView extends ModalView { * @property {boolean} [focusForCreate] Focus for create. * @property {string} [rootUrl] A root URL. * @property {string} [returnUrl] A return URL. - * @property {Record} [returnDispatchParams] Return dispatch params. + * @property {{ + * controller: string, + * action: string|null, + * options: {isReturn?: boolean} & Record, + * }} [returnDispatchParams] Return dispatch params. * @property {string} [fullFormUrl] A full-form URL. As of v9.0. */ diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index 063a90dfc7..aaa1da8f81 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -3274,27 +3274,38 @@ class ListRecordView extends View { return; } - const view = await (new RecordModal()).showDetail(this, { + const rootUrl = this.options.keepCurrentRootUrl ? this.getRouter().getCurrentUrl() : undefined; + + const helper = new RecordModal(); + + await helper.showDetail(this, { id: id, - scope: scope, + entityType: scope, model: model, - rootUrl: this.options.keepCurrentRootUrl ? this.getRouter().getCurrentUrl() : null, + rootUrl: rootUrl, editDisabled: this.quickEditDisabled, - }) + afterSave: m => { + if (!model) { + return; + } - if (!model) { - return; - } + this.trigger('after:save', m); + }, + afterDestroy: m => { + if (!model) { + return; + } - this.listenTo(view, 'after:save', model => this.trigger('after:save', model)); - this.listenTo(view, 'after:destroy', model => this.removeRecordFromList(model.id)); + this.removeRecordFromList(m.id); + }, + }); } // noinspection JSUnusedGlobalSymbols /** * @param {Object.} data */ - actionQuickEdit(data) { + async actionQuickEdit(data) { data = data || {}; const id = data.id; @@ -3327,43 +3338,18 @@ class ListRecordView extends View { return; } - const viewName = this.getMetadata().get(['clientDefs', scope, 'modalViews', 'edit']) || - 'views/modals/edit'; - if (!this.quickEditDisabled) { - Espo.Ui.notify(' ... '); + const helper = new RecordModal(); - const options = { - scope: scope, + const rootUrl = this.options.keepCurrentRootUrl ? this.getRouter().getCurrentUrl() : undefined; + + await helper.showEdit(this, { + entityType: scope, id: id, model: model, - fullFormDisabled: data.noFullForm, - returnUrl: this.getRouter().getCurrentUrl(), - returnDispatchParams: { - controller: scope, - action: null, - options: { - isReturn: true, - }, - }, - }; - - if (this.options.keepCurrentRootUrl) { - options.rootUrl = this.getRouter().getCurrentUrl(); - } - - this.createView('modal', viewName, options, (view) => { - view.once('after:render', () => { - Espo.Ui.notify(false); - }); - - view.render(); - - this.listenToOnce(view, 'remove', () => { - this.clearView('modal'); - }); - - this.listenToOnce(view, 'after:save', (m) => { + noFullForm: data.noFullForm, + rootUrl: rootUrl, + afterSave: m => { const model = this.collection.get(m.id); if (model) { @@ -3371,7 +3357,14 @@ class ListRecordView extends View { } this.trigger('after:save', m); - }); + }, + returnDispatchParams: { + controller: scope, + action: null, + options: { + isReturn: true, + }, + }, }); return; @@ -3394,7 +3387,7 @@ class ListRecordView extends View { options.rootUrl = this.getRouter().getCurrentUrl(); } - this.getRouter().navigate('#' + scope + '/edit/' + id, {trigger: false}); + this.getRouter().navigate(`#${scope}/edit/${id}`, {trigger: false}); this.getRouter().dispatch(scope, 'edit', options); } diff --git a/client/src/views/record/panels/relationship.js b/client/src/views/record/panels/relationship.js index 184cc3f0db..76bcd8fe12 100644 --- a/client/src/views/record/panels/relationship.js +++ b/client/src/views/record/panels/relationship.js @@ -744,7 +744,7 @@ class RelationshipPanelView extends BottomPanelView { helper .showDetail(this, { - scope: scope, + entityType: scope, id: id, model: model, })