diff --git a/client/src/view.js b/client/src/view.js index 25b3440ffe..fa5d065409 100644 --- a/client/src/view.js +++ b/client/src/view.js @@ -31,13 +31,15 @@ define('view', [], function () { /** * A base view. * - * @class Espo.View + * @class + * @name Class * @extends Bull.View + * @memberOf module:view */ - return Bull.View.extend(/** @lends Espo.View.prototype */{ + return Bull.View.extend(/** @lends module:view.Class.prototype */{ /** - * @callback Espo.View~actionHandlerCallback + * @callback module:view.Class~actionHandlerCallback * @param {jQuery.Event} e A DOM event. */ @@ -46,7 +48,7 @@ define('view', [], function () { * * @name model * @type {Espo.Model|null} - * @memberof Espo.View.prototype + * @memberOf module:view.Class.prototype * @public */ @@ -55,7 +57,7 @@ define('view', [], function () { * * @name collection * @type {Espo.Collection|null} - * @memberof Espo.View.prototype + * @memberOf module:view.Class.prototype * @public */ @@ -64,7 +66,7 @@ define('view', [], function () { * * @deprecated Use the `events` property. * @param {string} action - * @param {Espo.View~actionHandlerCallback} handler + * @param {module:view.Class~actionHandlerCallback} handler */ addActionHandler: function (action, handler) { let fullAction = 'click button[data-action=\"'+action+'\"]'; diff --git a/client/src/views/detail.js b/client/src/views/detail.js index 243cdfdc03..fd679c5db0 100644 --- a/client/src/views/detail.js +++ b/client/src/views/detail.js @@ -28,22 +28,58 @@ define('views/detail', ['views/main'], function (Dep) { - return Dep.extend({ + /** + * @class + * @name Class + * @extends module:views/main.Class + * @memberOf module:views/detail + */ + return Dep.extend(/** @lends module:views/detail.Class.prototype */{ + /** + * @inheritDoc + */ template: 'detail', - scope: null, - + /** + * @inheritDoc + */ name: 'Detail', - optionsToPass: ['attributes', 'returnUrl', 'returnDispatchParams', 'rootUrl'], + /** + * @inheritDoc + */ + optionsToPass: [ + 'attributes', + 'returnUrl', + 'returnDispatchParams', + 'rootUrl', + ], + /** + * A header view name. + * + * @type {string} + */ headerView: 'views/header', + /** + * A record view name. + * + * @type {string} + */ recordView: 'views/record/detail', + /** + * A root breadcrumb item not to be a link. + * + * @type {boolean} + */ rootLinkDisabled: false, + /** + * Add an un-follow button. + */ addUnfollowButtonToMenu: function () { this.removeMenuItem('follow', true); @@ -55,6 +91,9 @@ define('views/detail', ['views/main'], function (Dep) { }, true); }, + /** + * Add a follow button. + */ addFollowButtonToMenu: function () { this.removeMenuItem('unfollow', true); @@ -67,6 +106,9 @@ define('views/detail', ['views/main'], function (Dep) { }, true); }, + /** + * @inheritDoc + */ setup: function () { Dep.prototype.setup.call(this); @@ -91,6 +133,9 @@ define('views/detail', ['views/main'], function (Dep) { this.getHelper().processSetupHandlers(this, 'detail'); }, + /** + * Set up a page title. + */ setupPageTitle: function () { this.listenTo(this.model, 'after:save', () => { this.updatePageTitle(); @@ -103,6 +148,9 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * Set up a header. + */ setupHeader: function () { this.createView('header', this.headerView, { model: this.model, @@ -120,6 +168,9 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * Set up a record. + */ setupRecord: function () { var o = { model: this.model, @@ -142,11 +193,19 @@ define('views/detail', ['views/main'], function (Dep) { return this.createView('record', this.getRecordViewName(), o); }, + /** + * Get a record view name. + * + * @returns {string} + */ getRecordViewName: function () { return this.getMetadata() .get('clientDefs.' + this.scope + '.recordViews.detail') || this.recordView; }, + /** + * Control follow/unfollow buttons visibility.. + */ handleFollowButton: function () { if (this.model.get('isFollowed')) { this.addUnfollowButtonToMenu(); @@ -159,6 +218,9 @@ define('views/detail', ['views/main'], function (Dep) { } }, + /** + * Action 'follow'. + */ actionFollow: function () { this.disableMenuItem('follow'); @@ -174,6 +236,9 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * Action 'unfollow'. + */ actionUnfollow: function () { this.disableMenuItem('unfollow'); @@ -189,6 +254,9 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * @inheritDoc + */ getHeader: function () { var name = Handlebars.Utils.escapeExpression(this.model.get('name')); @@ -221,6 +289,9 @@ define('views/detail', ['views/main'], function (Dep) { ]); }, + /** + * @inheritDoc + */ updatePageTitle: function () { if (this.model.has('name')) { this.setPageTitle(this.model.get('name')); @@ -230,27 +301,120 @@ define('views/detail', ['views/main'], function (Dep) { } }, + /** + * Update a relationship panel (fetch data). + * + * @param {string} name A relationship name. + */ updateRelationshipPanel: function (name) { var bottom = this.getView('record').getView('bottom'); if (bottom) { var rel = bottom.getView(name); + if (rel) { rel.collection.fetch(); } } }, + /** + * When a related record created, attributes will be copied from a current entity. + * + * Example: + * ``` + * { + * 'linkName': { + * 'attributeNameOfCurrentEntity': 'attributeNameOfCreatedRelatedEntity', + * } + * } + * ``` + * + * @type {Object} + */ relatedAttributeMap: {}, + /** + * When a related record created, use a function to obtain some attributes for a created entity. + * + * Example: + * ``` + * { + * 'linkName': function () { + * return { + * 'someAttribute': this.model.get('attribute1') + ' ' + + * this.model.get('attribute2') + * }; + * }, + * } + * ``` + * + * @type {Object} + */ relatedAttributeFunctions: {}, + /** + * When selecting a related record, field filters can be automatically applied. + * + * Example: + * ``` + * { + * 'linkName': { + * 'field1': function () { + * return { + * attribute: 'field1', + * type: 'equals', + * value: this.model.get('someField'), + * data: {},// Additional filter data specific for a field type. + * }; + * }, + * }, + * } + * ``` + * + * @type {Object} + */ selectRelatedFilters: {}, + /** + * When selecting a related record, a primary filter can be automatically applied. + * + * Example: + * ``` + * { + * 'linkName1': 'primaryFilterName', + * 'linkName2': function () { + * return 'primaryFilterName'; + * }, + * } + * ``` + * + * @type {Object} + */ selectPrimaryFilterNames: {}, + /** + * When selecting a related record, bool filters can be automatically applied. + * + * Example: + * ``` + * { + * 'linkName1': ['onlyMy', 'followed'], + * 'linkName2': function () { + * return ['someBoolFilterName]; + * }, + * } + * ``` + * + * @type {Object} + */ selectBoolFilterLists: [], + /** + * Action 'createRelated'. + * + * @param {Object} data + */ actionCreateRelated: function (data) { data = data || {}; @@ -267,9 +431,10 @@ define('views/detail', ['views/main'], function (Dep) { attributes = _.extend(this.relatedAttributeFunctions[link].call(this), attributes); } - Object.keys(this.relatedAttributeMap[link] || {}).forEach((attr) => { - attributes[this.relatedAttributeMap[link][attr]] = this.model.get(attr); - }); + Object.keys(this.relatedAttributeMap[link] || {}) + .forEach(attr => { + attributes[this.relatedAttributeMap[link][attr]] = this.model.get(attr); + }); this.notify('Loading...'); @@ -302,6 +467,11 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * Action 'selectRelated'. + * + * @param {Object} data + */ actionSelectRelated: function (data) { var link = data.link; @@ -437,6 +607,9 @@ define('views/detail', ['views/main'], function (Dep) { }); }, + /** + * Action 'duplicate'. + */ actionDuplicate: function () { Espo.Ui.notify(this.translate('pleaseWait', 'messages')); diff --git a/client/src/views/edit.js b/client/src/views/edit.js index 3a36ae817d..e22ab2110a 100644 --- a/client/src/views/edit.js +++ b/client/src/views/edit.js @@ -28,16 +28,37 @@ define('views/edit', ['views/main'], function (Dep) { - return Dep.extend({ + /** + * @class + * @name Class + * @extends module:views/main.Class + * @memberOf module:views/edit + */ + return Dep.extend(/** @lends module:views/edit.Class.prototype */{ + /** + * @inheritDoc + */ template: 'edit', + /** + * @inheritDoc + */ scope: null, + /** + * @inheritDoc + */ name: 'Edit', + /** + * @inheritDoc + */ menu: null, + /** + * @inheritDoc + */ optionsToPass: [ 'returnUrl', 'returnDispatchParams', @@ -47,12 +68,30 @@ define('views/edit', ['views/main'], function (Dep) { 'returnAfterCreate', ], + /** + * A header view name. + * + * @type {string} + */ headerView: 'views/header', + /** + * A record view name. + * + * @type {string} + */ recordView: 'views/record/edit', + /** + * A root breadcrumb item not to be a link. + * + * @type {boolean} + */ rootLinkDisabled: false, + /** + * @inheritDoc + */ setup: function () { this.headerView = this.options.headerView || this.headerView; this.recordView = this.options.recordView || this.recordView; @@ -63,6 +102,9 @@ define('views/edit', ['views/main'], function (Dep) { this.getHelper().processSetupHandlers(this, 'edit'); }, + /** + * Set up a header. + */ setupHeader: function () { this.createView('header', this.headerView, { model: this.model, @@ -71,6 +113,9 @@ define('views/edit', ['views/main'], function (Dep) { }); }, + /** + * Set up a record. + */ setupRecord: function () { var o = { model: this.model, @@ -89,10 +134,18 @@ define('views/edit', ['views/main'], function (Dep) { return this.createView('record', this.getRecordViewName(), o); }, + /** + * Get a record view name. + * + * @returns {string} + */ getRecordViewName: function () { return this.getMetadata().get('clientDefs.' + this.scope + '.recordViews.edit') || this.recordView; }, + /** + * @inheritDoc + */ getHeader: function () { var headerIconHtml = this.getHeaderIconHtml(); @@ -133,6 +186,9 @@ define('views/edit', ['views/main'], function (Dep) { return this.buildHeaderHtml(arr); }, + /** + * @inheritDoc + */ updatePageTitle: function () { var title; diff --git a/client/src/views/main.js b/client/src/views/main.js index ca9b3dea63..6f8f3fa85f 100644 --- a/client/src/views/main.js +++ b/client/src/views/main.js @@ -28,20 +28,49 @@ define('views/main', ['view'], function (Dep) { - return Dep.extend({ + /** + * A base main view. The detail, edit, list views to be extended from. + * + * @class + * @name Class + * @extends module:view.Class + * @memberOf module:views/main + */ + return Dep.extend(/** @lends module:views/main.Class.prototype */{ + /** + * A scope name. + * + * @type {string} scope + */ scope: null, + /** + * A name. + * + * @type {string} name + */ name: null, + /** + * Top-right menu definitions. + * + * @type {Object} menu + */ menu: null, + /** + * @inheritDoc + */ events: { 'click .action': function (e) { Espo.Utils.handleAction(this, e); }, }, + /** + * @inheritDoc + */ init: function () { this.scope = this.options.scope || this.scope; this.menu = {}; @@ -92,10 +121,17 @@ define('views/main', ['view'], function (Dep) { this.updateLastUrl(); }, + /** + * Update a last history URL. + */ updateLastUrl: function () { this.lastUrl = this.getRouter().getCurrentUrl(); }, + /** + * @internal + * @returns {Object} + */ getMenu: function () { if (this.menuDisabled) { return {}; @@ -141,8 +177,21 @@ define('views/main', ['view'], function (Dep) { return menu; }, - getHeader: function () {}, + /** + * Get a header HTML. To be overridden. + * + * @returns {string} HTML. + */ + getHeader: function () { + return ''; + }, + /** + * Build a header HTML. To be called from the #getHeader method. + * + * @param {string[]} arr A breadcrumb path. Like: Account > Name > edit. + * @returns {string} HTML + */ buildHeaderHtml: function (arr) { var a = []; @@ -154,16 +203,29 @@ define('views/main', ['view'], function (Dep) { a.join('') + ''; }, + + /** + * Get an icon HTML. + * + * @returns {string} HTML + */ getHeaderIconHtml: function () { return this.getHelper().getScopeColorIconHtml(this.scope); }, + /** + * Action 'showModal'. + * + * @todo Revise. To be removed? + * + * @param {Object} data + */ actionShowModal: function (data) { var view = data.view; if (!view) { return; - }; + } this.createView('modal', view, { model: this.model, @@ -183,6 +245,14 @@ define('views/main', ['view'], function (Dep) { }); }, + /** + * Add a menu item. + * + * @param {'buttons'|'dropdown'|'actions'} type A type. + * @param {Object} item Item definitions. + * @param {boolean} [toBeginning=false] To beginning. + * @param {boolean} [doNotReRender=false] Skip re-render. + */ addMenuItem: function (type, item, toBeginning, doNotReRender) { if (item) { item.name = item.name || item.action; @@ -197,8 +267,6 @@ define('views/main', ['view'], function (Dep) { if (data.name === name) { index = i; - - return; } }); @@ -229,6 +297,12 @@ define('views/main', ['view'], function (Dep) { } }, + /** + * Remove a menu item. + * + * @param {string} name An item name. + * @param {boolean} doNotReRender Skip re-render. + */ removeMenuItem: function (name, doNotReRender) { var index = -1; var type = false; @@ -264,11 +338,14 @@ define('views/main', ['view'], function (Dep) { if (doNotReRender && this.isRendered()) { this.$el.find('.header .header-buttons [data-name="'+name+'"]').remove(); - - return; } }, + /** + * Disable a menu item. + * + * @param {string} name A name. + */ disableMenuItem: function (name) { this.$el .find('.header .header-buttons [data-name="'+name+'"]') @@ -276,6 +353,11 @@ define('views/main', ['view'], function (Dep) { .attr('disabled'); }, + /** + * Enable a menu item. + * + * @param {string} name A name. + */ enableMenuItem: function (name) { this.$el .find('.header .header-buttons [data-name="'+name+'"]') @@ -283,6 +365,12 @@ define('views/main', ['view'], function (Dep) { .removeAttr('disabled'); }, + /** + * Action 'navigateToRoot'. + * + * @param {Object} data + * @param {jQuery.Event} e + */ actionNavigateToRoot: function (data, e) { e.stopPropagation(); @@ -298,6 +386,11 @@ define('views/main', ['view'], function (Dep) { }); }, + /** + * Hide a menu item. + * + * @param {string} name A name. + */ hideHeaderActionItem: function (name) { ['actions', 'dropdown', 'buttons'].forEach((t) => { (this.menu[t] || []).forEach((item, i) => { @@ -319,6 +412,11 @@ define('views/main', ['view'], function (Dep) { this.controlMenuDropdownVisibility(); }, + /** + * Show a hidden menu item. + * + * @param {string} name A name. + */ showHeaderActionItem: function (name) { ['actions', 'dropdown', 'buttons'].forEach((t) => { (this.menu[t] || []).forEach((item, i) => { @@ -340,6 +438,12 @@ define('views/main', ['view'], function (Dep) { this.controlMenuDropdownVisibility(); }, + /** + * Whether a menu has any non-hidden dropdown items. + * + * @private + * @returns {boolean} + */ hasMenuVisibleDropdownItems: function () { var hasItems = false; @@ -352,6 +456,9 @@ define('views/main', ['view'], function (Dep) { return hasItems; }, + /** + * @private + */ controlMenuDropdownVisibility: function () { var $d = this.$el.find('.page-header .dropdown-group');