/
var
/
www
/
html
/
sugardev25
/
cache
/
javascript
/
base
/
Upload File
HOME
(function(app) { SUGAR.jssource = { "modules":{ "Home":{"fieldTemplates": { "base": { "layoutbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Home.LayoutbuttonField * @alias SUGAR.App.view.fields.BaseHomeLayoutbuttonField * @extends View.Fields.Base.BaseField */ ({ // Layoutbutton FieldTemplate (base) events: { 'click .btn.layout' : 'layoutClicked' }, extendsFrom: 'ButtonField', getFieldElement: function() { return this.$el; }, _render: function() { var buttonField = app.view._getController({type: 'field', name: 'button', platform: app.config.platform}); buttonField.prototype._render.call(this); }, _loadTemplate: function() { app.view.Field.prototype._loadTemplate.call(this); if(this.action !== 'edit' || (this.model.maxColumns <= 1)) { this.template = app.template.empty; } }, format: function(value) { var metadata = this.model.get("metadata"); if(metadata) { var components = this.getComponentsFromMetadata(metadata); return components ? components.length : 1; } return value; }, layoutClicked: function(evt) { var value = $(evt.currentTarget).data('value'); this.setLayout(value); }, /** * Gets component from metadata. * * @param {Object} metadata for all dashboard components * @return {Object} dashboard component */ getComponentsFromMetadata: function(metadata) { var component; // this is a tabbed dashboard if (metadata.tabs) { var tabIndex = this.context.get('activeTab') || 0; component = metadata.tabs[tabIndex].components; } else { component = metadata.components; } return component; }, setLayout: function(value) { var span = 12 / value; if(this.value) { if (value === this.value) { return; } var setComponent = function() { var metadata = this.model.get("metadata"); var components = this.getComponentsFromMetadata(metadata); _.each(components, function(component) { component.width = span; }, this); if (components.length > value) { _.times(components.length - value, function(index) { components[value - 1].rows = components[value - 1].rows.concat(components[value + index].rows); }, this); components.splice(value); } else { _.times(value - components.length, function(index) { components.push({ rows: [], width: span }); }, this); } this.model.set("metadata", app.utils.deepCopy(metadata), {silent: true}); this.model.trigger("change:metadata"); }; if(value !== this.value) { app.alert.show('resize_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_DASHBOARD_LAYOUT_CONFIRM', this.module), onConfirm: _.bind(setComponent, this), onCancel: _.bind(this.render,this) // reverse the toggle done }); } else { setComponent.call(this); } } else { //new data var metadata = { components: [] }; _.times(value, function(index) { metadata.components.push({ rows: [], width: span }); }, this); this.model.set("metadata", app.utils.deepCopy(metadata), {silent: true}); this.model.trigger("change:metadata"); } }, bindDomChange: function() { }, bindDataChange: function() { if (this.model) { this.model.on("change:metadata", this.render, this); if(this.model.isNew()) { //Assign default layout set this.setLayout(1); //clean out model changed attributes not to warn unsaved changes this.model.changed = {}; } } } }) }, "layoutsizer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Home.LayoutsizerField * @alias SUGAR.App.view.fields.BaseHomeLayoutsizerField * @extends View.Fields.Base.BaseField */ ({ // Layoutsizer FieldTemplate (base) spanMin: 2, spanTotal: 12, spanStep: 1, format: function(value) { var metadata = this.model.get("metadata"); return (metadata && metadata.components) ? metadata.components.length - 1 : 0; }, _loadTemplate: function() { app.view.Field.prototype._loadTemplate.call(this); if(this.action !== 'edit') { this.template = app.template.empty; } }, _render: function() { app.view.Field.prototype._render.call(this); if(this.action === 'edit' && this.value > 0) { var self = this, metadata = this.model.get("metadata"); this.$('#layoutwidth').empty().noUiSlider('init', { knobs: this.value, scale: [0,this.spanTotal], step: this.spanStep, connect: false, end: function(type) { if(type !== 'move') { var values = $(this).noUiSlider('value'); self.setValue(values); } } }) .append(function(){ var html = "", segments = (self.spanTotal / self.spanStep) + 1, segmentWidth = $(this).width() / (segments - 1), acum = 0; _.times(segments, function(i){ acum = (segmentWidth * i) - 2; html += "<div class='ticks' style='left:"+acum+"px'></div>"; }, this); return html; }); this.setSliderPosition(metadata); } else { this.$('.noUiSliderEnds').hide(); } }, setSliderPosition: function(metadata) { var divider = 0; _.each(_.pluck(metadata.components, 'width'), function(span, index) { if(index >= this.value) return; divider = divider + parseInt(span, 10); this.$('#layoutwidth').noUiSlider('move', { handle: index, to: divider }); }, this); }, setValue: function(value) { var metadata = this.model.get("metadata"), divider = 0; _.each(metadata.components, function(component, index){ if(index == metadata.components.length - 1) { component.width = this.spanTotal - divider; if(component.width < this.spanMin) { var adjustment = this.spanMin - component.width; for(var i = index - 1; i >= 0; i--) { metadata.components[i].width -= adjustment; if(metadata.components[i].width < this.spanMin) { adjustment = this.spanMin - metadata.components[i].width; metadata.components[i].width = this.spanMin; } else { adjustment = 0; } } component.width = this.spanMin; } } else { component.width = value[index] - divider; if(component.width < this.spanMin) { component.width = this.spanMin; } divider += component.width; } }, this); this.setSliderPosition(metadata); this.model.set("metadata", metadata, {silent: true}); this.model.trigger("change:layout"); }, bindDataChange: function() { if (this.model) { this.model.on("change:metadata", this.render, this); } }, bindDomChange: function() { } }) }, "sugar-dashlet-label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Home.SugarDashletLabelField * @alias SUGAR.App.view.fields.BaseHomeSugarDashletLabelField * @extends View.Fields.Base.LabelField * * Label for trademarked `Sugar Dashlet®` term. */ ({ // Sugar-dashlet-label FieldTemplate (base) extendsFrom: 'LabelField' }) }, "dashboardtitle": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Home.DashboardtitleField * @alias SUGAR.App.view.fields.BaseHomeDashboardtitleField * @extends View.Fields.Base.BaseField */ ({ // Dashboardtitle FieldTemplate (base) events: { 'click .dropdown-toggle': 'toggleClicked', 'click a[data-id]': 'navigateClicked', 'click a[data-action=manager]': 'managerClicked', 'click a[data-type=dashboardtitle]': 'editClicked', }, dashboards: null, hasEditAccess: true, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if (!app.acl.hasAccessToModel('edit', this.model, this.name)) { this.hasEditAccess = false; } }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('input[name=name]').on('keydown', _.bind(function(evt) { this.fieldInput(evt); }, this)); this.$('input[name=name]').on('blur', _.bind(function() { this.fieldBlur(); }, this)); }, /** * Handle the click by dashboard title * @param {Object} evt The jQuery Event Object */ editClicked: function(evt) { if (this.model.get('is_template')) { return; } // Reinitialize hasChanges function to provide opportunity // to collapse the field with changes this.hasChanged = function() { return false; }; this.view.editClicked(evt); this.view.toggleField(this, true); }, /** * Handle input in title field * @param {Object} evt The jQuery Event Object */ fieldInput: function(evt) { this.model.set('name', evt.currentTarget.value, { silent: true }); if (!this.model.dataFetched) { return; } if ($.inArray(evt.keyCode, [13, 27]) != -1) { this.fieldBlur(); } }, /** * Handle blur of title field */ fieldBlur: function() { if (!this.model.dataFetched) { return; } const reloadCollection = () => { this.context.off('record:set:state', reloadCollection); if (this.context.parent) { const collection = this.getCollection(); collection.fetch(); } }; this.context.on('record:set:state', reloadCollection); this.view.saveHandle(); this.view.toggleField(this); }, /** * Return collection * @return {Object} Sugar collection */ getCollection: function() { const contextBro = this.context.parent && _.contains(['multi-line', 'focus'], this.context.parent.get('layout')) ? this.context.parent : this.context.parent.getChildContext({module: 'Home'}); return contextBro.get('collection'); }, toggleClicked: function(evt) { var self = this; if (!_.isEmpty(this.dashboards)) { if (_.isFunction(self.view.adjustDropdownMenu)) { self.view.adjustDropdownMenu(); } return; } let collection = this.getCollection().clone(); var pattern = /^(LBL|TPL|NTC|MSG)_(_|[a-zA-Z0-9])*$/; collection.remove(self.model, {silent: true}); _.each(collection.models, function(model) { if (pattern.test(model.get('name'))) { model.set('name', app.lang.get(model.get('name'), model.get('dashboard_module')) ); } }); self.dashboards = collection; var optionTemplate = app.template.getField(self.type, 'options', self.module); self.$('.dropdown-menu').html(optionTemplate(collection)); if (_.isFunction(self.view.adjustDropdownMenu)) { self.view.adjustDropdownMenu(); } }, /** * Handle the click from the UI * @param {Object} evt The jQuery Event Object */ navigateClicked: function(evt) { var id = $(evt.currentTarget).data('id'); this.navigate(id); }, /** * Navigate the user to the manage dashboards view */ managerClicked: function() { var ctx = this.context && this.context.parent && (_.contains(['multi-line', 'focus'], this.context.parent.get('layout'))) ? this.context.parent : app.controller.context; var dashboardModule = ctx.get('module'); var dashboardLayout = ctx.get('layout'); app.router.navigate('#Dashboards?moduleName=' + dashboardModule + '&viewName=' + dashboardLayout, {trigger: true}); }, /** * Change the Dashboard * @param {string} id The ID of the Dashboard to load * @param {string} [type] (Deprecated) The type of dashboard being loaded, default is undefined */ navigate: function(id, type) { if (!_.isUndefined(type)) { // TODO: Remove the `type` parameter. This is to be done in TY-654 app.logger.warn('The `type` parameter to `View.Fields.Base.Home.DashboardtitleField`' + 'has been deprecated since 7.9.0.0. Please update your code to stop using it.'); } this.view.layout.navigateLayout(id); }, /** * Inspect the dashlet's label and convert i18n string only if it's concerned * * @param {String} i18n string or user typed string * @return {String} Translated string */ format: function(value) { var module = this.context.parent && this.context.parent.get('module') || this.context.get('module'); return app.lang.get(value, module) || value; }, /** * @inheritdoc * * Override template for dashboard title on home page. * Need display it as label so use `f.base.detail` template. */ _loadTemplate: function() { app.view.Field.prototype._loadTemplate.call(this); if (this.context && this.context.get('model') && this.context.get('model').dashboardModule === 'Home' ) { var tpl = (this.tplName === 'detail') ? 'dashboardtitle' : this.tplName; this.template = app.template.getField('dashboardtitle', tpl, this.module) || this.template; } }, /** * Called by record view to set max width of inner record-cell div * to prevent long names from overflowing the outer record-cell container */ setMaxWidth: function(width) { this.$el.css({'max-width': width}); }, /** * Return the width of padding on inner record-cell */ getCellPadding: function() { var padding = 0, $cell = this.$('.dropdown-toggle'); if ($cell.length > 0) { padding = parseInt($cell.css('padding-left'), 10) + parseInt($cell.css('padding-right'), 10); } return padding; } }) }, "favorite": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Dashboards.FavoriteField * @alias SUGAR.App.view.fields.DashboardsBaseFavoriteField * @extends View.Fields.Base.FavoriteField */ ({ // Favorite FieldTemplate (base) // FIXME TY-1463 Remove this file. /** * Check first if the model exists before rendering. * * The dashboards currently reside in the Home module. The Home module does * not have favorites enabled. The dashboards do have favorites enabled. * In order to show the favorite icon on dashboards, we need to bypass * the favoritesEnabled check. * * @override * @private */ _render: function() { // can't favorite something without an id if (!this.model.get('id')) { return null; } return app.view.Field.prototype._render.call(this); } }) }, "hint-dashboardtitle": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Hint-dashboardtitle FieldTemplate (base) extendsFrom: 'HomeDashboardtitleField', hintStateKey: 'hintEnabled', events: { 'click .dropdown-toggle': 'toggleClicked', 'click a[data-id]': 'navigateClicked', 'click a[data-action=manager]': 'managerClicked', 'click a[data-type=hint-dashboardtitle]': 'editClicked', }, /** * @inheritdoc */ initialize: function(options) { options.context.set('forceNew', true); this._super('initialize', [options]); if (this.isRecordView()) { if (this.getHintState()) { var model = this.context.parent.get('model'); setTimeout(function() { app.events.trigger('preview:render', model); }, 0); } app.events.on('preview:close', function() { this.setHintState(false); }, this); } }, /** * Get hint state * * @return {string} */ getHintState: function() { return app.user.lastState.get(this.hintStateKey); }, /** * Set hint state * * @param {string} value */ setHintState: function(value) { app.user.lastState.set(this.hintStateKey, value); }, /** * Check if is record view * * @return {bool} */ isRecordView: function() { var ctxParent = this.context.parent; return ctxParent && ctxParent.get('dataView') === 'record'; }, /** * @inheritdoc */ toggleClicked: function(event) { this._super('toggleClicked', [event]); var isNotAdded = this.$('.dropdown-menu [data-id=\'stage2\']').length < 1; if (this.isRecordView() && isNotAdded) { var template = `<li><a href=\'javascript:void(0);\' data-id=\'stage2\'>${Handlebars.Utils.escapeExpression( app.lang.get('LBL_HINT_PANEL') )}</a></li>`; this.$('.dropdown-menu').prepend(template); } }, /** * @inheritdoc */ navigateClicked: function(evt) { this._super('navigateClicked', [evt]); }, /** * @inheritdoc */ managerClicked: function() { this._super('managerClicked', []); }, /** * @inheritdoc */ editClicked: function(evt) { this._super('editClicked', [evt]); }, /** * @inheritdoc */ navigate: function(id, type) { var isHintState = id === 'stage2'; this.setHintState(isHintState); if (isHintState) { app.events.trigger('preview:render', this.context.parent.get('model')); } else { this._super('navigate', [id, type]); } } }) } }} , "views": { "base": { "about-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.AboutHeaderpaneView * @alias SUGAR.App.view.views.BaseHomeAboutHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // About-headerpane View (base) extendsFrom: 'HeaderpaneView', /** * @override * * Formats the title with the current server info. */ _formatTitle: function(title) { var serverInfo = app.metadata.getServerInfo(); var aboutVars = { product_name: serverInfo.product_name, version: serverInfo.version, custom_version: serverInfo.custom_version, build: serverInfo.build, }; return app.lang.get(title, this.module, aboutVars); } }) }, "webpage": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.WebpageView * @alias SUGAR.App.view.views.BaseHomeWebpageView * @extends View.View */ ({ // Webpage View (base) plugins: ['Dashlet'], /** * @property {Object} _defaultOptions * @property {number} _defaultOptions.limit Default number of rows displayed * in a dashlet. * * @protected */ _defaultOptions: { limit: 10, }, bindDataChange: function(){ if(!this.meta.config) { this.model.on("change", this.render, this); } }, _render: function() { if (!this.meta.config) { this.dashletConfig.view_panel[0].height = this.settings.get('limit') * this.rowHeight; } app.view.View.prototype._render.call(this); }, initDashlet: function(view) { this.viewName = view; var settings = _.extend({}, this._defaultOptions, this.settings.attributes); this.settings.set(settings); }, loadData: function(options) { if (options && options.complete) { options.complete(); } } }) }, "dashboard-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.HomeDashboardHeaderpaneView * @alias SUGAR.App.view.views.BaseHomeDashboardHeaderpaneView * @extends View.Views.Base.HeaderpaneMainView */ ({ // Dashboard-headerpane View (base) extendsFrom: 'DashboardHeaderpaneMainView', className: 'preview-headerbar home-headerpane', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['DashboardFiltersVisibility']); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'button:dashboard_filters:click', this.toggleDashboardFilter); this.listenTo(this.context, 'button:cancel_dashboard_filters:click', this.cancelDashboardFilters); this.listenTo(this.context, 'button:save_dashboard_filters:click', this.saveDashboardFilters); this.listenTo(this.context, 'dashboard-saved-success', this.saveDashboardSuccess); this.listenTo(this.context, 'dashboard-filter-mode-updated', this.filterModeState); this.listenTo(this.context, 'filter-groups-updated', this.enableSaveButton); this.listenTo(this.context, 'filter-field-updated', this.enableSaveButton); this.listenTo(this.context, 'toggle:change-visibility:button', this._toggleDashboardFiltersButton); this.listenTo(this.context, 'dashboard-filters-metadata-loaded', this._handleFiltersButtonVisibility); this.listenTo(this.context, 'dashboard:filter:broken', this.disableSaveButton); this.listenTo(this.model, 'sync', this.modelSync); // we only extend events so that save works _.extend(this.events, this._getHeaderpaneEvents()); }, /** * Initialize properties */ _initProperties: function() { this.hasAccessToReports = app.acl.hasAccess('view', 'Reports'); this.initFiltersVisibilityProperties(); }, /** * @inheritdoc */ _renderHeader: function() { this._super('_renderHeader'); const showFiltersButton = this.$('[data-action="change-visibility"]'); showFiltersButton.off('click', _.bind(this.toggleDashboardFilter, this)); showFiltersButton.on('click', _.bind(this.toggleDashboardFilter, this)); }, /** * Model synced */ modelSync: function() { if (!this.model || this.disposed) { return; } const metadata = this.model ? this.model.get('metadata') : {}; if (!metadata || metadata.tabs) { return; } const showFiltersButton = this.$('[data-action="change-visibility"]'); const originalTitle = app.lang.get('LBL_DASHBOARD_FILTERS', 'Home'); showFiltersButton.removeClass('disabled'); showFiltersButton.attr('data-original-title', originalTitle); showFiltersButton.attr('title', originalTitle); const compatibleDashlets = _.filter(metadata.dashlets, (dashlet) => { return dashlet.view && dashlet.view.filtersDef; }); if (_.isEmpty(compatibleDashlets)) { this._filtersOnScreen = true; this.toggleDashboardFilter(); const incompatibleTitle = app.lang.get('LBL_DASHBOARD_DASHLETS_INCOMPATIBLE', 'Home'); showFiltersButton.addClass('disabled'); showFiltersButton.attr('title', incompatibleTitle); showFiltersButton.attr('data-original-title', incompatibleTitle); } }, /** * Show/hide the button given a set of rules * * @param {Object} filterGroups */ _handleFiltersButtonVisibility: function(filterGroups) { // we execute this code on the next frame given the fact that _renderHeader has to be executed first // _renderHeader is being called when the model has been synced(same as this method) so we // have to make sure this one always executes after _renderHeader // this limitation came from the implementation of the headerpane controller _.debounce(() => { const metadata = this.model ? this.model.get('metadata') : {}; if (metadata.tabs) { return; } this.modelSync(); const showFiltersButton = this.$('[data-action="change-visibility"]'); const dashbordMain = this.layout.getComponent('dashboard-main'); showFiltersButton.toggleClass('!hidden', false); if (!dashbordMain) { showFiltersButton.toggleClass('!hidden', true); return; } const dashletMain = dashbordMain.getComponent('dashlet-main'); if (!dashletMain) { showFiltersButton.toggleClass('!hidden', true); return; } if (this._filtersOnScreen) { const showFiltersButton = this.$('.report-visibility-button'); showFiltersButton.toggleClass('active', true); } this._updateNumberOfFilters(filterGroups); })(); }, /** * Get headerpane events * * @return {Object} */ _getHeaderpaneEvents: function() { const parentDashboardHeaderpaneController = app.view._getController({ type: 'view', name: 'dashboard-headerpane', module: 'Dashboards' }); if (typeof parentDashboardHeaderpaneController !== 'function' || typeof parentDashboardHeaderpaneController.prototype !== 'object') { return {}; } return parentDashboardHeaderpaneController.prototype.events; }, /** * Cancel action */ cancelDashboardFilters: function() { this.context.trigger('dashboard-filters-canceled'); this.context.trigger('dashboard-filter-mode-changed', 'detail'); this.context.trigger('dashboard-filter-mode-updated', 'detail'); this.manageCancelSaveButtons(false); }, /** * Save action */ saveDashboardFilters: function() { this.disableSaveButton(); this.context.trigger('dashboard-filters-save'); }, /** * Save dashboard success */ saveDashboardSuccess: function() { this.manageCancelSaveButtons(false); }, /** * Filter mode ON * * The Cancel/Save button should be visible in the headerpane */ filterModeState: function(state) { const buttonsVisibility = state === 'edit'; this.manageCancelSaveButtons(buttonsVisibility); }, /** * Show/Hide Cancel/Save buttons * * @param {boolean} visibility */ manageCancelSaveButtons: function(visibility) { this.$('[name="cancel_dashboard_filters"]').toggleClass('!hidden', !visibility); this.$('[name="save_dashboard_filters"]').toggleClass('!hidden', !visibility); this.$('.report-visibility-button').toggleClass('!hidden', visibility); }, /** * Filter group save button disabling */ disableSaveButton: function() { this.$('[name="save_dashboard_filters"]').toggleClass('disabled', true); }, /** * Filter group save button enabling * * When a change is made, the save button should be enabled */ enableSaveButton: function() { this.$('[name="save_dashboard_filters"]').toggleClass('disabled', false); }, /** * Show/Hide the filter container */ toggleDashboardFilter: function() { const buttonDisabled = this.$('[data-action="change-visibility"]').hasClass('disabled'); if (buttonDisabled) { return; } this._filtersOnScreen = !this._filtersOnScreen; const showFiltersButton = this.$('.report-visibility-button'); showFiltersButton.toggleClass('active', this._filtersOnScreen); this.storeFilterPanelState(this._filtersOnScreen); this.context.trigger('dashboard-filter-toggled', this._filtersOnScreen); }, /** * Update the displayed number of filters * * @param {Object} filterGroups */ _updateNumberOfFilters: function(filterGroups) { const formattedFiltersNumber = this._getNumberOfFiltersToDisplay(filterGroups); const filtersBadgeEl = this.$('.report-filters-badge'); filtersBadgeEl.toggleClass('!hidden', true); if (formattedFiltersNumber > 0 || _.isString(formattedFiltersNumber)) { filtersBadgeEl.toggleClass('!hidden', false); filtersBadgeEl.text(formattedFiltersNumber); } }, /** * Get a formatted display number * * @param {Object} filterGroups * * @return {string} */ _getNumberOfFiltersToDisplay: function(filterGroups) { const maxNumberOfFiltersDisplayed = 9; const maxNumberOfFiltersLabel = '9+'; let formattedFiltersNumber = _.size(filterGroups); if (formattedFiltersNumber > maxNumberOfFiltersDisplayed) { formattedFiltersNumber = maxNumberOfFiltersLabel; } return formattedFiltersNumber; }, /** * @inheritdoc */ _dispose: function() { const showFiltersButton = this.$('[data-action="change-visibility"]'); showFiltersButton.off('click', _.bind(this.toggleDashboardFilter, this)); this._super('_dispose'); }, }) }, "about-version": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.AboutVersionView * @alias SUGAR.App.view.views.BaseHomeAboutVersionView * @extends View.View */ ({ // About-version View (base) /** * Version info string */ versionInfo: '', /** * Array of the user's products */ products: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); let version = app.metadata.getServerInfo().version; let template = Handlebars.compile(app.lang.get('LBL_ABOUT_VERSION', 'Home')); this.versionInfo = template({ version: version, }); this.products = app.user.getProductNames(); } }) }, "search-dashboard-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.SearchDashboardHeaderpaneView * @alias SUGAR.App.view.views.BaseSearchDashboardHeaderpaneView * @extends View.View */ ({ // Search-dashboard-headerpane View (base) className: 'search-dashboard-headerpane', events: { 'click a[name=collapse_button]' : 'collapseClicked', 'click a[name=expand_button]' : 'expandClicked', 'click a[name=reset_button]' : 'resetClicked' }, /** * Collapses all the dashlets in the dashboard. */ collapseClicked: function() { this.context.trigger('dashboard:collapse:fire', true); }, /** * Expands all the dashlets in the dashboard. */ expandClicked: function() { this.context.trigger('dashboard:collapse:fire', false); }, /** * Triggers 'facets:reset' event to reset the facets applied on the search. */ resetClicked: function() { this.context.parent.trigger('facets:reset', true); } }) }, "helplet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.HelpletView * @alias SUGAR.App.view.views.BaseHomeHelpletView * @extends View.Views.Base.HelpletView */ ({ // Helplet View (base) extendsFrom: 'HelpletView', /** * Console IDs mapped to the console label and support site module name */ _consoleMap: { 'c108bb4a-775a-11e9-b570-f218983a1c3e': { lang: 'LBL_AGENT_WORKBENCH', supportModule: 'ServiceConsole' }, }, /** * Method to fetch the help object from the app.help utility. */ createHelpObject: function() { var helpUrl = {}; // Console check var modelId = app.controller.context.get('modelId'); if (this._consoleMap[modelId]) { helpUrl.plural_module_name = app.lang.get(this._consoleMap[modelId].lang); } this._super('createHelpObject', [helpUrl]); }, /** * Consoles need to link to documentation differently * * @param {Object} params * @return {Object} params * @override */ sanitizeUrlParams: function(params) { var modelId = app.controller.context.get('modelId'); if (this._consoleMap[modelId]) { params.module = this._consoleMap[modelId].supportModule; delete params.route; } return params; } }) }, "dashboard-filters-edit": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.HomeDashboardFiltersEdit * @alias SUGAR.App.view.views.BaseHomeDashboardFiltersEdit * @extends View.View */ ({ // Dashboard-filters-edit View (base) className: 'dashboard-filter-container bg-[--foreground-base] h-full overflow-hidden w-[230px]', plugins: ['DashboardFilters'], events: { 'click [data-panelaction="createNewGroup"]': 'addNewFilterGroup', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Init properties */ _initProperties: function() { this._editMode = true; this.initFiltersProperties(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'dashboard-filter-widget-clicked', this.manageFieldInFilterGroup); this.listenTo(this.context, 'dashboard-filter-group-selected', this.filtersGroupSelected); this.listenTo(this.context, 'dashboard-filter-group-name-changed', this.filterGroupNameChanged); this.listenTo(this.context, 'dashboard-filter-group-removed', this.removeFilterGroup); this.listenTo(this.context, 'dashboard-filter-group-invalid-save', this.invalidGroupSave); }, /** * Dashboard meta data has been loaded, we need to create the views */ manageDashboardFilters: function(filterGroups) { this.handleDashboardFilters(filterGroups); this._toggleAddFilterButton(); }, /** * Toggle loading screen * * @param {boolean} toggle */ toggleLoading: function(toggle) { this.$('.filters-skeleton-loader').toggleClass('hidden', !toggle); this.$('.dashboard-filters-edit-container').toggleClass('hidden', toggle); }, /** * Either add or remove a filter in the group * * @param {Object} data */ manageFieldInFilterGroup: function(data) { if (!this._filterGroups || _.isEmpty(_.keys(this._filterGroups))) { return; } this.context.trigger('dashboard-filters-interaction'); if (data.highlighted) { this._addFieldToFilterGroup(data); } else { this._removeFieldFromFilterGroup(data); } }, /** * Remove a group * * @param {string} groupId */ removeFilterGroup: function(groupId) { if (!_.has(this._filterGroups, groupId) || !_.has(this._filterGroupsView, groupId)) { return; } this.context.trigger('dashboard-filters-interaction'); this._filterGroupsView[groupId].dispose(); delete this._filterGroups[groupId]; delete this._filterGroupsView[groupId]; const newSelectedGroup = _.chain(this._filterGroups).keys().first().value() || false; this._activeFilterGroupId = newSelectedGroup; this.notifyGroupsUpdated(); this._toggleAddFilterButton(); }, /** * Create new empty group from UI */ addNewFilterGroup: function() { this.context.trigger('dashboard-filters-interaction'); this._createNewGroup(); this.notifyGroupsUpdated(); this._toggleAddFilterButton(); }, /** * Triggered when a group name is updated * * @param {string} groupLabel * @param {string} groupId */ filterGroupNameChanged: function(groupLabel, groupId) { if (!_.has(this._filterGroups, groupId)) { return; } this._filterGroups[groupId].label = groupLabel; this.notifyGroupsUpdated(); }, /** * Change active filters group * * @param {string} filtersGroupId */ filtersGroupSelected: function(filtersGroupId) { if (!_.has(this._filterGroups, filtersGroupId) || this._activeFilterGroupId === filtersGroupId) { return; } this._activeFilterGroupId = filtersGroupId; this.notifyGroupsSelected(); }, /** * Show/Hide Add Filter Button */ _toggleAddFilterButton: function() { const showNoFilterContainer = _.isEmpty(this._filterGroups); this.$('[data-container="no-filter-container"]').toggleClass('hidden', !showNoFilterContainer); this.$('[data-container="add-container"]').toggleClass('hidden', !showNoFilterContainer); this.$('.edit-add-group').toggleClass('hidden', showNoFilterContainer); }, /** * Create a new filter group */ _createNewGroup: function() { const groupId = app.utils.generateUUID(); const groupMeta = { label: app.lang.get('LBL_DASHBOARD_FILTER_GROUP'), fieldType: false, fields: [], filterDef: { qualifier_name: 'not_empty', }, fieldsDashlet: [], }; const isSelected = true; this.createGroup(groupId, groupMeta, isSelected); }, /** * Add a filter to the selected group * * @param {Object} data */ _addFieldToFilterGroup: function(data) { let filterGroup = this._filterGroups[this._activeFilterGroupId]; filterGroup.fields = _.without(filterGroup.fields, _.findWhere(filterGroup.fields, { dashletId: data.dashletId, })); filterGroup.fieldType = data.field.type; filterGroup.fields.push({ dashletId: data.dashletId, fieldName: data.field.name, fieldDef: data.field, targetModule: data.targetModule, tableKey: data.filter.table_key, dashletLabel: data.dashletLabel, isRelated: data.isRelated, targetFieldModule: this.targetFieldModule, dashletSpecificData: data.dashletSpecificData, }); this._filterGroups[this._activeFilterGroupId] = filterGroup; const groupView = this._filterGroupsView[this._activeFilterGroupId]; groupView.toggleGroupInvalid(false); this.notifyGroupsUpdated(); }, /** * Remove a filter from the selected group * * @param {Object} data */ _removeFieldFromFilterGroup: function(data) { let filterGroup = this._filterGroups[this._activeFilterGroupId]; filterGroup.fields = _.without(filterGroup.fields, _.findWhere(filterGroup.fields, { fieldName: data.field.name, dashletId: data.dashletId, })); if (_.isEmpty(filterGroup.fields)) { filterGroup.fieldType = false; filterGroup.filterDef = { qualifier_name: 'not_empty', }; } this._filterGroups[this._activeFilterGroupId] = filterGroup; this.notifyGroupsUpdated(); }, }) }, "about-source-code": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.AboutSourceCodeView * @alias SUGAR.App.view.views.BaseHomeAboutSourceCodeView * @extends View.View */ ({ // About-source-code View (base) /** * The server info object. See {@link Core.MetadataManager#getServerInfo}. * * @property {String} */ serverInfo: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.serverInfo = app.metadata.getServerInfo(); } }) }, "dashboard-filters-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.HomeDashboardFiltersDetail * @alias SUGAR.App.view.views.BaseHomeDashboardFiltersDetail * @extends View.View */ ({ // Dashboard-filters-detail View (base) className: 'dashboard-filter-container bg-[--foreground-base] h-full overflow-hidden w-[230px]', plugins: ['DashboardFilters'], events: { 'click [data-action="switch-to-edit"]': 'switchToEditMode', 'click [data-action="add-first-filter"]': 'addFirstFilter', 'click [data-action="apply-runtime-filters"]': 'applyRuntimeFilters', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * @inheritdoc */ _initProperties: function() { this._editMode = false; this.initFiltersProperties(); }, /** * @inheritdoc */ _registerEvents: function() { this.listenTo(this.context, 'dashboard-filter-group-invalid-save', this.invalidGroupSave, this); this.listenTo(this.context, 'filter-operator-data-changed', this.enableApplyButton, this); }, /** * Dashboard meta data has been loaded, we need to create the views */ manageDashboardFilters: function(filterGroups) { let showEditButton = false; let showApplyFiltersContainer = false; let showNoFilterContainer = false; let showAddContainer = false; this.handleDashboardFilters(filterGroups); this._hasGroupFilters = !_.isEmpty(this._filterGroups); this._hasEditAccess = app.acl.hasAccessToModel('edit', this.model); let isTemplateDashboard = false; if (!_.isUndefined(this.model) && !_.isUndefined(this.model.get('is_template'))) { isTemplateDashboard = this.model.get('is_template'); } showNoFilterContainer = !this._hasGroupFilters; showApplyFiltersContainer = this._hasGroupFilters; if (this._hasEditAccess && !isTemplateDashboard) { showEditButton = this._hasGroupFilters; showAddContainer = !this._hasGroupFilters; } this.$('[data-action="show-edit-button"]').toggleClass('hidden', !showEditButton); this.$('[data-container="apply-filters-container"]').toggleClass('hidden', !showApplyFiltersContainer); this.$('[data-container="no-filter-container"]').toggleClass('hidden', !showNoFilterContainer); this.$('[data-container="add-container"]').toggleClass('hidden', !showAddContainer); this.toggleLoading(false); }, /** * Enable the apply filters button */ enableApplyButton: function() { this.$('.apply-filters-btn').removeClass('disabled').removeAttr('disabled'); }, /** * Disable the apply filters button */ disableApplyButton: function() { this.$('.apply-filters-btn').addClass('disabled').attr('disabled', true); }, /** * Toggle loading screen * * @param {boolean} toggle */ toggleLoading: function(toggle) { this.$('.filters-skeleton-loader').toggleClass('hidden', !toggle); this.$('.dashboard-filters-detail-container').toggleClass('hidden', toggle); }, /** * Add first filter and go to edit mode */ addFirstFilter: function() { this.context.trigger('dashboard-filters-interaction'); this.switchToEditMode(); }, /** * Go to edit mode */ switchToEditMode: function() { let canEdit = this._canAlterFilters(); if (canEdit) { this.context.trigger('dashboard-filter-mode-changed', 'edit'); this.context.trigger('dashboard-filter-mode-updated', 'edit'); } else { this._showCantAlterWarning(); } }, /** * Apply runtime filters */ applyRuntimeFilters: function() { let canEdit = this._canAlterFilters(); if (canEdit) { this.context.trigger('dashboard-filters-apply'); this.context.trigger('refresh-dashlet-results'); this.disableApplyButton(); } else { this._showCantAlterWarning(); } }, /** * Show cannot alter filters warning */ _showCantAlterWarning: function() { app.alert.show('show_alter_alert', { level: 'warning', messages: app.lang.get('LBL_NO_ALTER_FILTERS', this.module), autoClose: true, }); }, /** * Can or cannot edit filters * * @return {boolean} */ _canAlterFilters: function() { const dashboardMain = this.closestComponent('dashboard-main'); const dashletMain = dashboardMain ? dashboardMain.getComponent('dashlet-main') : false; const dashboardGrid = dashletMain ? dashletMain.getComponent('dashboard-grid') : false; let canAlter = true; if (dashboardGrid) { _.each(dashboardGrid._components, (dashletWrapper) => { _.each(dashletWrapper._components, (dashlet) => { if (dashlet.isDashletLoading) { canAlter = false; } }); }); } return canAlter; }, /** * Build the unique key for the dashboard * * @param {string} dashboardId * * @return {string} */ _buildDashboardStateKey: function(dashboardId) { const module = this.module; const currentUserId = app.user.id; const stateKey = `${module}:${dashboardId}:${currentUserId}`; const lastStateKey = app.user.lastState.buildKey( 'dashboard-filters', stateKey ); return lastStateKey; }, /** * Get last state for dashboard * * @param {string} lastStateKey * * @return {string} */ _getDashboardLastState: function(lastStateKey) { const lastState = app.user.lastState.get(lastStateKey); if (lastState) { return JSON.parse(lastState); } return this._getDashboardDefaultState(); }, /** * Get the default state for user * * @return {Object} */ _getDashboardDefaultState: function() { const defaultState = {}; return defaultState; }, }) }, "hint-news-dashlet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Hint-news-dashlet View (base) plugins: ['Stage2CssLoader', 'Dashlet'], /** * Classes that are responsible of COG icon on the dashlet. */ cogIconDefaultClass: 'sicon-settings', cogIconLoadingClass: 'sicon-refresh sicon-is-spinning', events: { 'click [name=edit_button]': 'editClicked', 'click [name=refresh_button]': 'refreshClicked', 'click [data-action=newsClick]': 'trackNewsSelect' }, /** * Load threshold indicates how many news should be fake-loaded initially and on scroll. */ loadThreshold: 5, /** * Indicates if more news should be loaded. On initial load and on scroll * this variable should be true, but if we simply apply a search term, it should be false. */ loadMore: true, /** * Shows that how much did we scroll down. When a search is done, the scroll is reset. * We reapply the amount of scroll we did with the help of the scrollPosition. */ scrollPosition: 0, /** * Shows the order number of the last displayed news article. * searching will be done on all news. */ lastDisplayedNewsIndex: 0, /** * Initially and on loading we would like to show a placeholder for the user. * Since we start with loading the dependencies, we show the placeholder by default. */ isHintRequestLoading: true, /** * The list of news articles loaded. */ news: [], /** * Cache. We save any new articles loaded by the auto-updater. * We will show these news if the user click on the notification. */ newArticles: [], /** * Cache. We save the titles of the new artciles loaded by the auto-updater. * This will make it easier to identify if an article was already loeaded. */ newArticleTitles: [], /** * The list of news articles based on the search term. * searchResults will contain all news when the search is reset or there is no search term. */ searchResults: [], /** * The frequency of the auto-updater in seconds. */ autoUpdaterIntervals: 60, isNotHintUser: false, /** * Holds the time duration of a valid data enrichment access token (1 hour). */ tokenExpirationTimeOut: 60 * 60 * 1000, initDashlet: function() { this.moduleName = this.context.get('module'); this.getDependencies(); this.isDarkMode = app.hint.isDarkMode(); app.events.on('hint-news-dashlet:search', this.applySearchTerm, this); this.runAutoUpdater(); }, /** * It changes the cog icon into a spinning set of arrows (default dashlet way) * if the news are being loaded, reverts on load. * * @param {boolean} loading Flag indicating if the news are being loaded. */ toggleCogIcon: function(loading) { this.cogIconDefaultClass = 'sicon-settings'; this.cogIconLoadingClass = 'sicon-refresh sicon-is-spinning'; var iconToAdd = loading ? this.cogIconLoadingClass : this.cogIconDefaultClass; var iconToRemove = loading ? this.cogIconDefaultClass : this.cogIconLoadingClass; this.cogIcon.removeClass(iconToRemove).addClass(iconToAdd); }, /** * On selecting the edit from the dashlet menu, display the notification preferences. */ editClicked: function() { app.drawer.open({ layout: 'stage2-news-preferences-drawer' }); }, /** * Dashlet Refresh event handler. */ refreshClicked: function() { this.initNewsRequest(); }, /** * An error handler used by the stage2 dependency calls. * * @param {string} message Error message to log. * @param {Object} error Error object holding information about the failed request. */ setStage2errorCode: function(message, error) { app.logger.error(message.concat(JSON.stringify(error))); if (_.isUndefined(this.stage2errorCode)) { this.stage2errorCode = error.status; this.render(); } }, /** * First dependency of the news request. The news will be get using the url retrieved here. */ getStage2Url: function() { var self = this; app.api.call('GET', app.api.buildURL('stage2/params'), null, { success: function(data) { self.stage2url = data.enrichmentServiceUrl; self.notificationsServiceUrl = data.notificationsServiceUrl; self.initNewsRequest(); }, error: _.bind(this.setStage2errorCode, this, 'Failed to get Hint param: ') }); }, /** * Second dependency of the news request. News can be retrieved only with a valid access token. */ getStage2AccessToken: function() { app.api.call('create', app.api.buildURL('stage2/token'), null, { success: _.bind(function(data) { if (!_.isUndefined(data.errorMessage)) { this.setStage2errorCode('Failed to get Hint access token: ' ,data); return; } this.stage2accessTokenExpireDate = Date.now() + this.tokenExpirationTimeOut; this.stage2accessToken = data.accessToken; this.initNewsRequest(); }, this), error: _.bind(this.setStage2errorCode, this, ' ') }); }, getNotificationServiceToken: function() { app.api.call('create', app.api.buildURL('stage2/notificationsServiceToken'), null, { success: _.bind(function(data) { if (!_.isUndefined(data.errorMessage)) { this.setStage2errorCode('Failed to get Notifications Service Token: ' ,data); return; } var now = new Date(); now.setTime(now.getTime() + data.ttlMs); this.notificationsAccessTokenExpireDate = now; this.notificationsAccessToken = data.accessToken; this.initNewsRequest(); }, this), error: _.bind(function(err) { this.setStage2errorCode('Failed to get Notifications Service Token: ', err); }, this) }); }, /** * Checks the instance license AND user license to determine whether the hint dashlet should be displayed */ getLicenseMetadataForHint: function() { var self = this; var url = app.api.buildURL('hint/license/check'); app.api.call('GET', url, null, { success: function(data) { const instanceHasHintLicense = data.isHintUser; const userHasHintLicense = app.hint.isHintUser(); const isHintUser = instanceHasHintLicense && userHasHintLicense; self.isNotHintUser = !isHintUser; self.render(); }, error: function(err) { app.logger.error('Failed to get Hint license metadata: ' + JSON.stringify(err)); } }); }, /** * Will load every dependency of the news. * Without a context we will not be able to define what kind of news should be loaded. * In order to be able to retrieve some news, we need a url to address and an access token. */ getDependencies: function() { this.getStage2Url(); this.getStage2AccessToken(); this.getLicenseMetadataForHint(); }, /** * Checks if the notifications service access token is expired or not. * A non existing token is considered an expired token. * * @return {boolean} True if the token is expired. */ isNotificationsAccessTokenExpired: function() { var isExpired = true; if (this.notificationsAccessToken) { isExpired = this.notificationsAccessTokenExpireDate < new Date(); } return isExpired; }, isAccessTokenExpired: function() { var isExpired = true; if (this.stage2accessToken) { isExpired = this.stage2accessTokenExpireDate < new Date(); } return isExpired; }, /** * Checks all dependencies and if they are met a request will be made to retrieve the last 50 news. * In case the notifications service access token expires, it will get a new one and trigger this * method on a successful get. */ initNewsRequest: function() { if (this.isAccessTokenExpired()) { this.stage2accessToken = null; this.stage2accessTokenExpireDate = null; this.getStage2AccessToken(); return; } else { if (this.stage2url && this.stage2accessToken && this.notificationsServiceUrl) { if (this.isNotificationsAccessTokenExpired()) { this.getNotificationServiceToken(); } else { this.getNews(); } } } }, /** * If there is no active request for getting news it will send out a request. */ getNews: function() { if (this.activeRequest) { this.activeRequest.abort(); } this.toggleCogIcon(true); var headers = { authtoken: this.notificationsAccessToken }; if (this.etag) { headers['if-none-match'] = this.etag; } this.activeRequest = $.ajax({ type: 'GET', url: this.notificationsServiceUrl.concat('/getNotifications'), headers: headers, context: self, success: _.bind(this.loadNews, this), error: _.bind(this.handleFailedNewsFetch, this) }); }, /** * Error handler for the news request. Error should be handled only if the * request was not aborted manually. Also if the retrieval fails it will try * to fetch the news again for a limited number of times. */ handleFailedNewsFetch: function(error) { this.activeRequest = null; if (error && error.statusText !== 'abort') { app.logger.error('Failed to fetch news on Hint: ' + JSON.stringify(error)); this.toggleCogIcon(); // Retry to get new token duplets for accessToken and NotificationServiceToken. These // access tokens will be reset when the autoUpdater interval runs after 1 minute. this.stage2accessToken = null; this.notificationsAccessToken = null; } }, /** * Each article should receive the following information: * an id, used for searching the news titles for a specific value * hide flag, used to show/hide news on the page - initially all news are hidden * Aditionally we create a map of the news' titles for being able to use search. * * @param {Array} news A list of news articles. */ extendNews: function(news) { this.articleTitles = []; _.each(news, function(article, i) { article.id = i; article.hide = true; this.articleTitles.push(article.title); }, this); this.news = news; }, /** * All the news will be marked as hidden. * Used when applying a search term. */ markNewsAsHidden: function() { _.each(this.news, function(news) { news.hide = true; }); }, /** * This is where the actual search is happening. The searchResults will hold a list of news, * these news all contain the search term in their titles. Based on the searchResults * we will be able to tell, which articles need to be marked visible. * Important note: here we make the search on ALL available news, however we display none, * it is the responsibility of `toggleNews` to display news that are found in the results saved here. * * @param {string} searchTerm A phrase or a single word. */ searchNews: function(searchTerm) { if (searchTerm) { this.searchResults = []; _.each(this.news, function(article) { if (article.title.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1) { this.searchResults.push(article); } }, this); } else { this.searchResults = this.news; } }, /** * If the user did search already the news assigned to the current context * we reapply it on the list. Note that `null` means that the user desires to see all news. */ applyExistingSearchTerm: function() { var searchTerm = this.readFilter(); if (searchTerm) { app.events.trigger('hint-news-dashlet:apply', searchTerm); } this.searchNews(searchTerm); this.toggleNews(); }, /** * Initializes the search process when the user changes the assigned field. * * @param {string|null} searchTerm */ applySearchTerm: function(searchTerm) { if (this.news.length && this.readFilter() !== searchTerm) { this.loadMore = false; this.scrollPosition = 0; this.markNewsAsHidden(); this.writeFilter(searchTerm); this.searchNews(searchTerm); this.toggleNews(); } }, /** * Saves the last known ETag for later use. * Some browsers may return the ETag in lowercase format. * * @param {Object} request The request for getting news notifications. */ setEtag: function(request) { this.etag = request.getResponseHeader('ETag') || request.getResponseHeader('etag'); }, /** * News request success handler. If we managed to retrieve news, * they need to be processed, and displayed. Only a limited number of news will be displayed. * The rest of the news will get rendered on scroll. Render will happen through filtering. * In case the request returns 'notmodified' statues, we do not make any changes except cog icon. * * @param {Object} data The response to the news request. * @param {string} state Success or notmodified. * @param {Object} request The current request. */ loadNews: function(data, state, request) { this.activeRequest = null; if (!this.disposed) { this.toggleCogIcon(); this.isHintRequestLoading = false; if (!this.autoRefresh) { this.runAutoUpdater(); this.extendNews(data); this.applyExistingSearchTerm(); } else if (state !== 'notmodified') { this.setEtag(request); if (data && data.length) { if (this.news.length) { this.countAndCacheNewArticles(data); if (this.scrollPosition === 0) { this.insertLatestNewsIfAny(); } } else { this.extendNews(data); this.applyExistingSearchTerm(); } } else { app.events.trigger('hint-news-panel-filter:set', null); this._render(); } } else { this._render(); } } }, /** * An article is new if it has not yet been added to the list of articles * that can be displayed in the dashlet. * * @param {Object} article An article from the news request response. * @return {boolean} True if the article is new. */ isNewArticle: function(article) { return !_.contains(this.articleTitles, article.title); }, /** * Checks if there are any new articles and caches them for later use. * * @param {Array} data The newest news. */ countAndCacheNewArticles: function(data) { var newArticles = []; var showNotifier = false; _.every(data, function(article) { var isNewArticle = !_.contains(this.articleTitles, article.title); if (isNewArticle && !_.contains(this.newArticleTitles, article.title)) { newArticles.push(article); this.newArticleTitles.push(article.title); showNotifier = true; } return isNewArticle; }, this); this.newArticles = _.union(newArticles, this.newArticles); if (showNotifier) { this.showUpdateNotifier(); } }, /** * If there are any new news, we will insert them on top of the list. * We need to update the counters which depend on the amount of the news. */ insertLatestNewsIfAny: function() { if (this.newArticles.length) { var updatedNews = _.union(this.newArticles, this.news); this.lastDisplayedNewsIndex += this.newArticles.length; this.scrollPosition = 0; this.extendNews(updatedNews); this.applyExistingSearchTerm(); this.newArticles = []; this.newArticleTitles = []; } else { this.render(); /* The else part here should not happen with normal, standard workflow. Is is intended for use case, when etag status is not sent as expected. In other words: the same news are being sent again without a different status. */ } }, /** * In a given interval of times, we will fetch news again and * update the list accordingly. */ runAutoUpdater: function() { if (!this.autoRefresh) { this.autoRefresh = setInterval( _.bind(this.initNewsRequest, this), this.autoUpdaterIntervals * 1000 ); } }, /** * @return {string} A unique id for identifying the filter for the current context. */ getFilterKey: function() { return 'news-filter-option*dashlet'; }, /** * Retrieves the filter applyed by the user the last time. */ readFilter: function() { return app.user.lastState.get(this.getFilterKey()); }, /** * Saves the filter value as a user preference. `Null` means no filter. * * @param {string|null} filter */ writeFilter: function(filter) { app.user.lastState.set(this.getFilterKey(), filter); }, /** * This method may returns 2 kind of lists of news. * One is returned upon scrolling: * we need to apply the current search term on the news to be displayed. * The other just on searching/filtering: * we apply the search term on all the news. * * @return {Array} A list of news based on the last displayed news. */ getNextSegmentForFilter: function() { var start = this.loadMore ? this.lastDisplayedNewsIndex : 0; this.lastDisplayedNewsIndex += this.loadMore ? this.loadThreshold : 0; return this.searchResults; }, /** * News articles will be marked as hidden if their title does not contain the search term. * Applying a render will show/hide the corresponding news. */ toggleNews: function() { var targetNews = this.getNextSegmentForFilter(); _.each(targetNews, function(article) { if (_.findWhere(this.searchResults, {id: article.id})) { article.hide = false; } }, this); this.loadMore = false; this.render(); }, /** * Verifies if there are any news marked as visible among the news that have been already loaded. */ checkNewsToDisplay: function() { var segment = this.searchResults; this.displayNews = _.find(segment, function(article) { return article.hide === false; }); }, /** * Dashlet component usually means just the content area, however we would like to identify * the content area together with the header above as the dashlet. In here we do that by assigning * a class to the wrapper element. We also search and remember the cog icon element. */ setDashletElements: function() { if (!this.cogIcon || !this.cogIcon.length) { var fullDashlet = this.$el.parent().parent(); fullDashlet.addClass('hint-news-dashlet'); this.cogIcon = fullDashlet.find('[data-action=loading]'); } }, /** * Responsible for showing the message about new articles being ready to be shown. * The element is flushed on each render, so we need to attach the event each time. */ showUpdateNotifier: function() { if (this.scrollPosition > 0) { var notifierFadeOutTimeSecs = 10; var notifierEl = this.$('.hint-news-dashlet-auto-notifier'); notifierEl.removeClass('vanished'); notifierEl.on('click', _.bind(function() { this.$('#newsarea').scrollTop(0); this.insertLatestNewsIfAny(); notifierEl.addClass('vanished'); }, this)); setTimeout(function() { notifierEl.addClass('vanished'); }, notifierFadeOutTimeSecs * 1000); } }, /** * Scroll event has to be bound each time to the element because with each render * te attached event handler is cleared. Then again on each render we want to maintain * the last known scroll position. */ applyScrollingAndNotifier: function() { var newsarea = this.$('#newsarea'); newsarea.scrollTop(this.scrollPosition); }, /** * @inheritdoc */ _render: function(options) { this.setDashletElements(); this.checkNewsToDisplay(); this._super('_render', [options]); this.applyScrollingAndNotifier(); }, /** * @inheritdoc * Detach event handlers. Shut down the auto-updater. */ _dispose: function() { clearInterval(this.autoRefresh); app.events.off('hint-news-dashlet:search', this.applySearchTerm, this); this._super('_dispose'); }, /** * Tracks which news has been opened by the user. * * @param {EventObject} event Event information */ trackNewsSelect: function(event) { $.ajax({ type: 'POST', data: { clickType: 'news', origin: this.context.get('module'), clickedURL: event.currentTarget.href, title: event.currentTarget.innerText, metricsToken: app.user.get('hintMetricsToken') }, url: this.stage2url.concat('/url-click'), headers: { authToken: this.stage2accessToken }, error: function(err) { app.logger.error('Failed to record news click event: ' + JSON.stringify(err)); } }); } }) }, "twitter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.TwitterView * @alias SUGAR.App.view.views.BaseHomeTwitterView * @extends View.View */ ({ // Twitter View (base) plugins: ['Dashlet', 'RelativeTime', 'Connector'], limit : 20, events: { 'click .connect-twitter': 'onConnectTwitterClick', 'click .create-case': 'createCase' }, initDashlet: function() { this.initDashletConfig(); var serverInfo = app.metadata.getServerInfo(); this.tweet2case = serverInfo.system_tweettocase_on ? true : false; var limit = this.settings.get('limit') || this.limit; this.settings.set('limit', limit); this.cacheKey = 'twitter.dashlet.current_user_cache'; var currentUserCache = app.cache.get(this.cacheKey); if (currentUserCache && currentUserCache.current_twitter_user_name) { self.current_twitter_user_name = currentUserCache.current_twitter_user_name; } if (currentUserCache && currentUserCache.current_twitter_user_pic) { self.current_twitter_user_pic = currentUserCache.current_twitter_user_pic; } this.caseCreateAcl = app.acl.hasAccess('edit','Cases'); }, initDashletConfig: function() { this.moduleType = app.controller.context.get('module'); this.layoutType = app.controller.context.get('layout'); // if config view override with module specific if (this.meta.config && this.layoutType === 'record') { this.dashletConfig = app.metadata.getView(this.moduleType, this.name) || this.dashletConfig; // if record view that's not the Home module's record view, disable twitter name settings config if (this.moduleType !== 'Home' && this.dashletConfig.config && this.dashletConfig.config.fields) { // get rid of the twitter name field this.dashletConfig.config.fields = _.filter(this.dashletConfig.config.fields, function(field) { return field.name !== 'twitter'; }); } } }, onConnectTwitterClick: function(event) { if ( !_.isUndefined(event.currentTarget) ) { event.preventDefault(); var href = this.$(event.currentTarget).attr('href'); app.bwc.login(false, function(response){ window.open(href); }); } }, /** * opens case create drawer with case attributes prefilled * @param event */ createCase: function (event) { var module = 'Cases'; var layout = 'create'; var self = this; // set up and open the drawer app.drawer.reset(); app.drawer.open({ layout: layout, context: { create: true, module: module } }, function (refresh, createModelPointer) { if (refresh) { var collection = app.controller.context.get('collection'); if (collection && collection.module === module) { collection.fetch({ //Don't show alerts for this request showAlerts: false }); } } }); var createLayout = _.last(app.drawer._components), tweetId = this.$(event.target).data('url').split('/'); tweetId = tweetId[tweetId.length-1]; var createValues = { 'source':'Twitter', 'name': app.lang.get('LBL_CASE_FROM_TWITTER_TITLE', 'Cases') + ' ' + tweetId +' @'+ this.$(event.target).data('screen_name'), 'description': app.lang.get('LBL_TWITTER_SOURCE', 'Cases') +' '+ this.$(event.target).data('url') }; // update the create models values this.createModel = createLayout.model; if (this.model) { if(this.model.module == 'Accounts') { createValues.account_name = this.model.get('name'); createValues.account_id = this.model.get('id'); } else { createValues.account_name = this.model.get('account_name'); createValues.account_id = this.model.get('account_id'); } } this.setCreateModelFields(this.createModel, createValues); this.createModel.on('sync', _.once(function (model) { // add activity stream on save var activity = app.data.createBean('Activities', { activity_type: "post", comment_count: 0, data: { value: app.lang.get('LBL_TWITTER_SOURCE') +' '+ self.$(event.target).data('url'), tags: [] }, tags: [], value: app.lang.get('LBL_TWITTER_SOURCE') +' '+ self.$(event.target).data('url'), deleted: "0", parent_id: model.id, parent_type: "Cases" }); activity.save(); //relate contact if we can find one var contacts = app.data.createBeanCollection('Contacts'); var options = { filter: [ { "twitter": { "$equals": self.$(event.target).data('screen_name') } } ], success: function (data) { if (data && data.models && data.models[0]) { var url = app.api.buildURL('Cases', 'contacts', {id: self.createModel.id, relatedId: data.models[0].id, link: true}); app.api.call('create', url); } } }; contacts.fetch(options); })); }, /** * sets fields on model according to acls * @param model * @param fields * @return {Mixed} */ setCreateModelFields: function(model, fields) { var action = 'edit', module = 'Cases', ownerId = app.user.get('id'); _.each(fields, function(value, fieldName) { if(app.acl.hasAccess(action, module, ownerId, fieldName)) { model.set(fieldName, value); } }); return model; }, _render: function () { if (this.tweets || this.meta.config) { app.view.View.prototype._render.call(this); } }, bindDataChange: function(){ if(this.model) { this.model.on('change', this.loadData, this); } }, /** * Gets twitter name from one of various fields depending on context * @return {string} twitter name */ getTwitterName: function() { var mapping = this.getConnectorModuleFieldMapping('ext_rest_twitter', this.moduleType); var twitter = this.model.get('twitter') || this.model.get(mapping.name) || this.model.get('name') || this.model.get('account_name') || this.model.get('full_name'); //workaround because home module actually pulls a dashboard instead of an //empty home model if (this.layoutType === 'records' || this.moduleType === 'Home') { twitter = this.settings.get('twitter'); } if (!twitter) { return false; } twitter = twitter.replace(/ /g, ''); this.twitter = twitter; return twitter; }, /** * Load twitter data * * @param {object} options * @return {boolean} `false` if twitter name could not be established */ loadData: function(options) { if (this.disposed || this.meta.config) { return; } this.screen_name = this.settings.get('twitter') || false; this.tried = false; if (this.viewName === 'config') { return false; } this.loadDataCompleteCb = options ? options.complete : null; this.connectorCriteria = ['eapm_bean', 'test_passed']; this.checkConnector('ext_rest_twitter', _.bind(this.loadDataWithValidConnector, this), _.bind(this.handleLoadError, this), this.connectorCriteria); }, /** * With a valid connector, load twitter data * * @param {object} connector - connector will have been validated already */ loadDataWithValidConnector: function(connector) { if (!this.getTwitterName()) { if (_.isFunction(this.loadDataCompleteCb)) { this.loadDataCompleteCb(); } return false; } var limit = parseInt(this.settings.get('limit'), 10) || this.limit, self = this; var currentUserUrl = app.api.buildURL('connector/twitter/currentUser','','',{connectorHash:connector.connectorHash}); if (!this.current_twitter_user_name) { app.api.call('READ', currentUserUrl, {},{ success: function(data) { app.cache.set(self.cacheKey, { 'current_twitter_user_name': data.screen_name, 'current_twitter_user_pic': data.profile_image_url }); self.current_twitter_user_name = data.screen_name; self.current_twitter_user_pic = data.profile_image_url; if (!this.disposed) { self.render(); } }, complete: self.loadDataCompleteCb }); } var url = app.api.buildURL('connector/twitter','',{id:this.twitter},{count:limit,connectorHash:connector.connectorHash}); app.api.call('READ', url, {},{ success: function (data) { if (self.disposed) { return; } var tweets = []; if (data.success !== false) { _.each(data, function (tweet) { var time = new Date(tweet.created_at.replace(/^\w+ (\w+) (\d+) ([\d:]+) \+0000 (\d+)$/, '$1 $2 $4 $3 UTC')), date = app.date.format(time, 'Y/m/d H:i:s'), // retweeted tweets are sometimes truncated so use the original as source text text = tweet.retweeted_status ? 'RT @'+tweet.retweeted_status.user.screen_name+': '+tweet.retweeted_status.text : tweet.text, sourceUrl = tweet.source, id = tweet.id_str, name = tweet.user.name, tokenText = text.split(' '), screen_name = tweet.user.screen_name, profile_image_url = tweet.user.profile_image_url_https, j, rightNow = new Date(), diff = (rightNow.getTime() - time.getTime())/(1000*60*60*24), useAbsTime = diff > 1; // Search for links and turn them into hrefs for (j = 0; j < tokenText.length; j++) { if (tokenText[j].charAt(0) == 'h' && tokenText[j].charAt(1) == 't') { tokenText[j] = "<a class='googledoc-fancybox' href=" + '"' + tokenText[j] + '"' + "target='_blank'>" + tokenText[j] + "</a>"; } } text = tokenText.join(' '); tweets.push({id: id, name: name, screen_name: screen_name, profile_image_url: profile_image_url, text: text, source: sourceUrl, date: date, useAbsTime: useAbsTime}); }, this); } self.tweets = tweets; if (!this.disposed) { self.template = app.template.get(self.name + '.Home'); self.render(); } }, error: function(data) { if (self.tried === false) { self.tried = true; // twitter get returned error, so re-get connectors var name = 'ext_rest_twitter'; var funcWrapper = function () { self.checkConnector(name, _.bind(self.loadDataWithValidConnector, self), _.bind(self.handleLoadError, self), self.connectorCriteria); }; self.getConnectors(name, funcWrapper); } else { self.handleLoadError(null); } }, complete: self.loadDataCompleteCb }); }, /** * Error handler for if connector validation errors at some point * * @param {object} connector */ handleLoadError: function(connector) { if (this.disposed) { return; } this.showGeneric = true; this.errorLBL = app.lang.get('ERROR_UNABLE_TO_RETRIEVE_DATA'); this.template = app.template.get(this.name + '.twitter-need-configure.Home'); if (connector === null) { //Connector doesn't exist this.errorLBL = app.lang.get('LBL_ERROR_CANNOT_FIND_TWITTER') + this.twitter; } else if (!connector.test_passed && connector.testing_enabled) { //OAuth failed this.needOAuth = true; this.needConnect = false; this.showGeneric = false; this.showAdmin = app.acl.hasAccess('admin', 'Administration'); } else if (!connector.eapm_bean) { //Not connected this.needOAuth = false; this.needConnect = true; this.showGeneric = false; this.showAdmin = app.acl.hasAccess('admin', 'Administration'); } app.view.View.prototype._render.call(this); if (_.isFunction(this.loadDataCompleteCb)) { this.loadDataCompleteCb(); } }, _dispose: function() { if (this.model) { this.model.off('change', this.loadData, this); } app.view.View.prototype._dispose.call(this); } }) }, "dashboard-filter-group-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Dashboard filter group widget * * @class View.Views.Base.HomeDashboardFilterGroupWidget * @alias SUGAR.App.view.views.BaseHomeDashboardFilterGroupWidget * @extends View.View */ ({ // Dashboard-filter-group-widget View (base) events: { 'change [data-action="new-group-name"]': 'onGroupNameChanged', 'click [data-action="remove-group"]': 'onRemoveGroup', 'click [data-action="collapse-group"]': 'onCollapseGroup', 'click .filter-widget-headerpane': 'onCollapseGroup', 'click [data-action="remove-field"]': 'onRemoveField', 'click [data-action="dashlet-group-widget"]': 'onGroupClick', 'mouseenter .dashlet-group-filter-widget': 'mouseOverGroup', 'mouseleave .dashlet-group-filter-widget': 'mouseOutGroup', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { const options = this.options; this._groupId = options.groupId; this._groupMeta = options.groupMeta || {}; this._widgetNo = options.widgetNo; this._editMode = options.editMode || false; this._groupFields = this._groupMeta.fields || []; this._filterDef = this._groupMeta.filterDef || []; this._groupType = this._groupMeta.fieldType || ''; this._groupLabel = this._groupMeta.label || `Filter ${this._widgetNo}`; this._isEmptyGroup = _.isEmpty(this._groupFields); const metadata = this.model.get('metadata'); this._users = metadata.users || []; this._operators = metadata.runtimeFilterOperators || []; this._groupOperators = this._operators[this._groupType] || []; this._filterOperatorWidget = false; this._currentUserRestrictedGroup = !!this._groupMeta.currentUserRestrictedGroup; this._checkACLRestrictions(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'filter-groups-updated', this.groupsUpdated, this); this.listenTo(this.context, 'filter-operator-data-changed', this.updateItemsCount, this); this.listenTo(this.context, 'dashboard-filter-group-selected', this.newGroupSelected, this); }, /** * Check ACL restrictions */ _checkACLRestrictions: function() { if (this._currentUserRestrictedGroup) { this._disabledACL = true; return; } const metadata = this.model.get('metadata'); const restrictedGroups = metadata && _.has(metadata, 'currentUserRestrictedDashlets') ? metadata.currentUserRestrictedDashlets : []; const fieldNotAllowed = _.find(this._groupFields, (field) => { if (_.isArray(restrictedGroups) && restrictedGroups.includes(field.dashletId)) { return true; } const hasListAccess = app.acl.hasAccess('list', field.targetModule); if (!hasListAccess) { return true; } if (field.isRelated && !_.isEmpty(field.fieldDef.ext2)) { return !( app.acl.hasAccess('view', field.fieldDef.ext2) && app.acl.hasAccess('list', field.fieldDef.ext2) && app.acl.hasAccess('read', field.fieldDef.ext2, {field: field.fieldName}) ); } else if (field.isRelated && _.has(field, 'tableKey')) { const checkRelateAccess = function(relatedFieldsMeta) { let access = true; if (relatedFieldsMeta) { const module = relatedFieldsMeta.module; const relatedField = relatedFieldsMeta.fieldName; if (module && relatedField) { const fieldAccess = ( app.acl.hasAccess('view', module) && app.acl.hasAccess('list', module) && app.acl.hasAccess('read', module, {field: relatedField}) ); access = fieldAccess; } } return access; }; const directlyRelatedFieldsMeta = this._getRelatedFieldMeta(field); const relatedFieldsMeta = this._getRelatedFieldMetaRel(field); if (!directlyRelatedFieldsMeta && !relatedFieldsMeta) { return false; } let fieldhasAccess = true; fieldhasAccess = checkRelateAccess(directlyRelatedFieldsMeta); if (fieldhasAccess) { fieldhasAccess = checkRelateAccess(relatedFieldsMeta); } return !fieldhasAccess; } else { return !( app.acl.hasAccess('view', field.targetModule) && app.acl.hasAccess('list', field.targetModule) && app.acl.hasAccess('read', field.targetModule, {field: field.fieldName}) ); } }); if (!_.isEmpty(fieldNotAllowed)) { this._disabledACL = true; } }, /** * Get the full table list for a specific field * * @param {Object} field - metadata of the field */ _getFullTableListForField: function(field) { const meta = this.model.get('metadata'); if (!meta) { return false; } if (!_.has(meta, 'dashlets')) { return false; } const dashlets = meta.dashlets; if (!dashlets) { return false; } const fieldDashlet = _.find(dashlets, item => item.id === field.dashletId); if (!fieldDashlet || !_.has(fieldDashlet, 'view') || !_.has(fieldDashlet.view, 'fullTableList')) { return false; } const fullTableList = fieldDashlet.view.fullTableList; if (!fullTableList) { return false; } return fullTableList; }, /** * Metadata for the related fields without an Ext2 table * * @param {Object} field - metadata of the field */ _getRelatedFieldMetaRel: function(field) { if (!_.isString(field.tableKey)) { return false; } const linkPath = field.tableKey.split(':'); if (linkPath.length < 2) { return false; } const fullTableList = this._getFullTableListForField(field); if (!fullTableList) { return false; } const linkDef = fullTableList[field.tableKey]; if (!linkDef) { return false; } if (!_.has(linkDef, 'module')) { return false; } const linkDefKey = 'link_def'; const relationshipNameKey = 'relationship_name'; const beanSideKey = 'bean_is_lhs'; const rhsModuleKey = 'rhs_module'; const lhsModuleKey = 'lhs_module'; const lhsFieldKey = 'lhs_key'; const rhsFieldKey = 'rhs_key'; const linkNameKey = 'name'; if (!_.has(linkDef, 'module')) { return false; } if (!_.has(linkDef, linkDefKey)) { return false; } if (!_.has(linkDef[linkDefKey],[relationshipNameKey])) { return false; } const linkMeta = linkDef[linkDefKey]; if (!_.has(linkMeta, beanSideKey) || !_.has(linkMeta, linkNameKey)) { return false; } const relationshipName = linkMeta[relationshipNameKey]; if (!relationshipName) { return false; } const relMeta = app.metadata.getRelationship(relationshipName); if (!relMeta || !_.has(relMeta, rhsModuleKey) || !relMeta[rhsModuleKey]) { return false; } const isInLhs = linkMeta[beanSideKey]; let module = ''; let fieldName = ''; if (isInLhs && _.has(relMeta, lhsModuleKey)) { module = relMeta[lhsModuleKey]; fieldName = relMeta[lhsFieldKey]; } else if (_.has(relMeta, rhsModuleKey)) { module = relMeta[rhsModuleKey]; fieldName = relMeta[rhsFieldKey]; } if (module && fieldName) { return { 'module': module, 'fieldName': fieldName, }; } return false; }, /** * Metadata for the related fields without an Ext2 table * * @param {Object} field - metadata of the field */ _getRelatedFieldMeta: function(field) { if (!_.isString(field.tableKey)) { return false; } const linkPath = field.tableKey.split(':'); if (linkPath.length < 2) { return false; } const fullTableList = this._getFullTableListForField(field); if (!fullTableList) { return false; } const linkDef = fullTableList[field.tableKey]; if (!linkDef) { return false; } if (!_.has(linkDef, 'module')) { return false; } const targetModule = linkDef.module; return { 'module': targetModule, 'fieldName': field.fieldName, }; }, /** * Update items count */ updateItemsCount: function() { if (!this._filterOperatorWidget) { return; } const itemsCount = this._filterOperatorWidget.getItemsCount(); this.$('.items-selected-nr').html(itemsCount); }, /** * Listen when a group is updated * * @param {Object} filterGroups * @param {string} activeFilterGroupId * @param {boolean} isGroupClicked */ groupsUpdated: function(filterGroups, activeFilterGroupId, isGroupClicked) { this._isSelected = this._groupId === activeFilterGroupId; if (_.isUndefined(filterGroups[activeFilterGroupId])) { return; } const { fieldType, fields, filterDef, } = filterGroups[this._groupId]; this._groupType = fieldType; this._groupFields = fields; this._filterDef = filterDef; this._groupOperators = this._operators[fieldType] || []; this._isEmptyGroup = _.isEmpty(this._groupFields); const groupClicked = isGroupClicked ? isGroupClicked : false; if (this._isSelected && !groupClicked) { this.render(); } this._toggleGroupActive(this._isSelected); if (this._isSelected && this._isEmptyGroup && this._editMode) { this._highlightInput(); } if (!groupClicked) { this._updateFilterOperatorWidget(); } }, /** * Handle text changed * * @param {UIEvent} e */ onGroupNameChanged: function(e) { this._groupLabel = e.target.value; this.context.trigger('dashboard-filter-group-name-changed', this._groupLabel, this._groupId); this.context.trigger('dashboard-filters-interaction'); }, /** * On group hover in * * @param {UIEvent} e */ mouseOverGroup: function(e) { if (this._editMode) { return; } const highlight = true; this._groupHighlight(highlight, e); }, /** * On group hover in * * @param {UIEvent} e */ mouseOutGroup: function(e) { if (this._editMode) { return; } const highlight = false; this._groupHighlight(highlight, e); }, /** * Highlight current group and notify it's dashlets * * @param {boolean} highlight * @param {UIEvent} element */ _groupHighlight: function(highlight, element) { this.$(element.currentTarget).toggleClass('hover-highlight', highlight); if (!this._groupMeta || !_.has(this._groupMeta, 'fields') || this._groupMeta.fields === 0) { return; } this._notifyGroupHighlight(highlight); }, /** * Notify to show/hide the highlight * * @param {boolean} highlight */ _notifyGroupHighlight: function(highlight) { const dashletIds = _.pluck(this._groupMeta.fields, 'dashletId'); this.context.trigger('dashboard-filter-group-highlight', dashletIds, highlight); }, /** * On group click * * @param {UIEvent} e */ onGroupClick: function(e) { if (this._disabledACL) { return; } const $el = this.$('.dashlet-collapse-group'); const collapsed = $el.is('.sicon-chevron-down'); if (collapsed) { this.onCollapseGroup(e); } this.context.trigger('dashboard-filter-group-selected', this._groupId); }, /** * A new group was selected * * @param {string} selectedGroupId */ newGroupSelected: function(selectedGroupId) { this._isSelected = this._groupId === selectedGroupId; this._toggleGroupActive(this._isSelected); }, isValid: function() { return this._filterOperatorWidget && this._filterOperatorWidget.isValid(); }, /** * Mark the group as invalid * * @param {boolean} show */ toggleGroupInvalid: function(show) { this.$('.dashlet-group-filter-widget').toggleClass('dashlet-group-invalid', show); }, /** * Handle click on the remove group * * @param {UIEvent} e */ onCollapseGroup: function(e) { if (e.target && e.target.dataset && e.target.dataset.action === 'new-group-name') { return; } const $el = this.$('.dashlet-collapse-group'); const collapsed = $el.is('.sicon-chevron-up'); $el.toggleClass('sicon-chevron-down', collapsed); $el.toggleClass('sicon-chevron-up', !collapsed); this.$('.empty-group-message').toggleClass('hidden', collapsed); this.$('[data-container="fields-container"]').toggleClass('hidden', collapsed); this.$('[data-container="operators-container"]').toggleClass('hidden', collapsed); this.$('[data-container="collapsed-filter-group"]').toggleClass('hidden', !collapsed); this.$('[data-container="expanded-filter-group"]').toggleClass('hidden', collapsed); this.$('.dashlet-group-filter-widget').toggleClass('dashlet-group-filter-widget-collapsed', collapsed); this.$('.dashboard-group-edit-label').toggleClass('dashboard-group-edit-name', !collapsed); if (e) { e.stopPropagation(); } }, /** * Handle click on the remove group * * @param {UIEvent} e */ onRemoveGroup: function(e) { this.context.trigger('dashboard-filter-group-removed', this._groupId); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, /** * Handle click on the field to be removed * * @param {UIEvent} e */ onRemoveField: function(e) { const fieldName = e.currentTarget.getAttribute('data-field-name'); const dashletId = e.currentTarget.getAttribute('data-dashlet-id'); const fieldMeta = { dashletId, field: { name: fieldName, }, highlighted: false, }; this.context.trigger('dashboard-filter-widget-clicked', fieldMeta); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, /** * Select and highlight the input text */ _highlightInput: function() { const inputEl = this.$('[data-action="new-group-name"]'); inputEl.focus(); inputEl.select(); }, /** * Update filter operator widget */ _updateFilterOperatorWidget: function() { this._deleteFilterOperatorWidget(); this._createFilterOperatorWidget(); }, /** * Delete filter operator widget */ _deleteFilterOperatorWidget: function() { if (this._filterOperatorWidget) { this._filterOperatorWidget.dispose(); this._filterOperatorWidget = false; } }, /** * Create filter operator widget */ _createFilterOperatorWidget: function() { const field = _.first(this._groupFields); if (_.isUndefined(field)) { return; } this._filterOperatorWidget = app.view.createView({ type: 'filter-operator-widget', context: this.context, operators: this._groupOperators, users: this._users, filterData: this._filterDef, fieldType: this._groupType, filterId: this._groupId, seedFieldDef: field.fieldDef, seedModule: field.targetModule, tooltipTitle: this._groupLabel, manager: this, }); this._filterOperatorWidget.render(); this.$('[data-container="operators-container"]').append(this._filterOperatorWidget.$el); this.$('.runtime-filter-summary-text').html(this._filterOperatorWidget.getSummaryText()); this.$('[data-tooltip="filter-summary"]').tooltip({ delay: 200, container: 'body', placement: 'bottom', title: _.bind(this._filterOperatorWidget.getTooltipText, this._filterOperatorWidget), html: true, trigger: 'hover', }); this.updateItemsCount(); }, /** * Handle the group style if is active/inactive * * @param {boolean} isActive */ _toggleGroupActive: function(isActive) { isActive = isActive && this._editMode; this._toogleGroupSelected(isActive); }, /** * Handle selected group * * @param {boolean} selected */ _toogleGroupSelected: function(selected) { this.$('.dashlet-group-filter-widget').toggleClass('dashlet-group-active', selected); }, /** * @inheritdoc */ _dispose: function() { this._deleteFilterOperatorWidget(); this._super('_dispose'); }, }) }, "about-language-packs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.AboutLanguagePacksView * @alias SUGAR.App.view.views.BaseHomeAboutLanguagePacksView * @extends View.View */ ({ // About-language-packs View (base) linkTemplate: null, /** * @inheritdoc * * Initializes the link template to be used on the render. */ initialize: function(opts) { app.view.View.prototype.initialize.call(this, opts); this.linkTemplate = app.template.getView(this.name + '.link', this.module); }, /** * @inheritdoc * * Override the title to pass the context with the server info. */ _renderHtml: function() { _.each(this.meta.providers, function(provider) { provider.link = this.linkTemplate(provider); }, this); app.view.View.prototype._renderHtml.call(this); } }) }, "module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Module menu provides a reusable and easy render of a module Menu. * * This also helps doing customization of the menu per module and provides more * metadata driven features. * * @class View.Views.Base.Home.ModuleMenuView * @alias SUGAR.App.view.views.BaseHomeModuleMenuView * @extends View.Views.Base.ModuleMenuView */ ({ // Module-menu View (base) extendsFrom: 'ModuleMenuView', plugins: ['Previewable'], /** * The collection used to list dashboards on the dropdown. * * This is initialized on {@link #_initCollections}. * * @property * @type {Data.BeanCollection} */ dashboards: null, /** * The collection used to list the recently viewed on the dropdown, * since it needs to use a {@link Data.MixedBeanCollection} * * This is initialized on {@link #_initCollections}. * * @property * @type {Data.MixedBeanCollection} */ recentlyViewed: null, /** * Default settings used when none are supplied through metadata. * * Supported settings: * - {Number} dashboards Number of dashboards to show on the dashboards * container. Pass 0 if you don't want to support dashboards listed here. * - {Number} favorites Number of records to show on the favorites * container. Pass 0 if you don't want to support favorites. * - {Number} recently_viewed Number of records to show on the recently * viewed container. Pass 0 if you don't want to support recently viewed. * - {Number} recently_viewed_toggle Threshold of records to use for * toggling the recently viewed container. Pass 0 if you don't want to * support recently viewed. * * Example: * ``` * // ... * 'settings' => array( * 'dashboards' => 10, * 'favorites' => 5, * 'recently_viewed' => 9, * 'recently_viewed_toggle' => 4, * //... * ), * //... * ``` * * @protected */ _defaultSettings: { dashboards: 50, favorites: 3, recently_viewed: 10, recently_viewed_toggle: 3 }, /** * Key for storing the last state of the recently viewed toggle. * * @type {String} */ TOGGLE_RECENTS_KEY: 'more', /** * The lastState key to use in order to retrieve or save last recently * viewed toggle. */ _recentToggleKey: null, /** * @inheritdoc * * Initializes the collections that will be used when the dropdown is * opened. * * Initializes Legacy dashboards. * * Sets the recently viewed toggle key to be ready to use when the dropdown * is opened. */ initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [data-toggle="recently-viewed"]': 'handleToggleRecentlyViewed' }); this._initCollections(); this.meta.last_state = { id: 'recent' }; this._recentToggleKey = app.user.lastState.key(this.TOGGLE_RECENTS_KEY, this); this._setLogoImage(); }, /** * Sets the logo for Sugar top left corner * @param imageUrl * @protected */ _setLogoImage: function(imageUrl) { this.logoImage = imageUrl ? imageUrl : app.utils.buildUrl('styleguide/assets/img/sugar-cube-black.svg'); }, /** * Additional 'data:preview' callbacks to be invoked from Previewable * * @param hasChanges * @param data */ dataPreviewCallbacks: function(hasChanges, data) { if (_.contains(data.properties, 'logoImage')) { this._setLogoImage(this.logoImage); this.render(); } }, /** * Creates the collections needed for list of dashboards and recently * viewed. * * The views' collection is pointing to the Home module and we might need * to use that later for something that could be populated from that * module. Therefore, we create other collections to be used for extra * information that exists on the Home dropdown menu. * * @chainable * @private */ _initCollections: function() { this.dashboards = app.data.createBeanCollection('Dashboards'); this.recentlyViewed = app.data.createMixedBeanCollection(); return this; }, /** * @inheritdoc * * Adds the title and the class for the Home module (Sugar cube). */ _renderHtml: function() { this._super('_renderHtml'); this.$el.attr('title', app.lang.get('LBL_TABGROUP_HOME', this.module)); this.$el.addClass('home btn-group py-2 px-5'); }, /** * @override * * Populates all available dashboards when opening the menu. We override * this function without calling the parent one because we don't want to * reuse any of it. * * **Attention** We only populate up to 20 dashboards. * * TODO We need to keep changing the endpoint until SIDECAR-493 is * implemented. */ populateMenu: function() { var pattern = /^(LBL|TPL|NTC|MSG)_(_|[a-zA-Z0-9])*$/; this.$('.active').removeClass('active'); this.dashboards.fetch({ 'limit': this._settings['dashboards'], 'filter': [{ 'dashboard_module': 'Home', '$or': [ {'$favorite': ''}, {'default_dashboard': 1} ] }], 'order_by': {'date_modified': 'DESC'}, 'showAlerts': false, 'success': _.bind(function(data) { var module = this.module; _.each(data.models, function(model) { if (pattern.test(model.get('name'))) { model.set('name', app.lang.get(model.get('name'), module)); } // hardcode the module to `Home` due to different link that // we support model.module = 'Home'; }); this._renderPartial('dashboards', { collection: this.dashboards, active: this.context.get('module') === 'Home' && this.context.get('model') }); }, this), 'endpoint': function(method, model, options, callbacks) { app.api.records(method, 'Dashboards', model.attributes, options.params, callbacks); } }); this.populateRecentlyViewed(false); }, /** * Populates recently viewed records with a limit based on last state key. * * Based on the state it will read 2 different metadata properties: * * - `recently_viewed_toggle` for the value to start toggling * - `recently_viewed` for maximum records to retrieve * * Defaults to `recently_viewed_toggle` if no state is defined. * * @param {Boolean} focusToggle Whether to set focus on the toggle after rendering */ populateRecentlyViewed: function(focusToggle) { var visible = app.user.lastState.get(this._recentToggleKey), threshold = this._settings['recently_viewed_toggle'], limit = this._settings[visible ? 'recently_viewed' : 'recently_viewed_toggle']; if (limit <= 0) { return; } var modules = this._getModulesForRecentlyViewed(); if (_.isEmpty(modules)) { return; } this.recentlyViewed.fetch({ 'showAlerts': false, 'fields': ['id', 'name'], 'date': '-7 DAY', 'limit': limit, 'module_list': modules, 'success': _.bind(function(data) { this._renderPartial('recently-viewed', { collection: this.recentlyViewed, open: !visible, showRecentToggle: data.models.length > threshold || data.next_offset !== -1 }); if (focusToggle && this.isOpen()) { // put focus back on toggle after renderPartial this._focusRecentlyViewedToggle(); } }, this), 'endpoint': function(method, model, options, callbacks) { var url = app.api.buildURL('recent', 'read', options.attributes, options.params); app.api.call(method, url, null, callbacks, options.params); } }); return; }, /** * Set focus on the recently viewed toggle * @private */ _focusRecentlyViewedToggle: function() { this.$('[data-toggle="recently-viewed"]').focus(); }, /** * Retrieve a list of modules where support for recently viewed records is * enabled and current user has access to list their records. * * Dashboards is discarded since it is already populated by default on the * drop down list {@link #populateMenu}. * * To disable recently viewed items for a specific module, please set the * `recently_viewed => 0` on: * * - `{custom/,}modules/{module}/clients/{platform}/view/module-menu/module-menu.php` * * @return {Array} List of supported modules names. * @private */ _getModulesForRecentlyViewed: function() { // FIXME: we should find a better option instead of relying on visible // modules var modules = app.metadata.getModuleNames({filter: 'visible', access: 'list'}); modules = _.filter(modules, function(module) { var view = app.metadata.getView(module, 'module-menu'); return !view || !view.settings || view.settings.recently_viewed > 0; }); return modules; }, /** * Handles the toggle of the more recently viewed mixed records. * * This triggers a refresh on the data to be retrieved based on the amount * defined in metadata for the given state. This way we limit the amount of * data to be retrieve to the current state and not getting always the * maximum. * * @param {Event} event The click event that triggered the toggle. */ handleToggleRecentlyViewed: function(event) { app.user.lastState.set(this._recentToggleKey, Number(!app.user.lastState.get(this._recentToggleKey))); this.populateRecentlyViewed(true); event.stopPropagation(); } }) }, "top-activity-user": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.TopActivityUserView * @alias SUGAR.App.view.views.BaseHomeTopActivityUserView * @extends View.View */ ({ // Top-activity-user View (base) plugins: ['Dashlet', 'GridBuilder'], events: { 'change select[name=filter_duration]': 'filterChanged' }, /** * Track if current user is manager. */ isManager: false, initDashlet: function(viewName) { this.collection = new app.BeanCollection(); this.isManager = app.user.get('is_manager'); if(!this.meta.config) { this.collection.on("reset", this.render, this); } }, _mapping: { meetings: { icon: 'sicon-message', label: 'LBL_MOST_MEETING_HELD' }, inbound_emails: { icon: 'sicon-email', label: 'LBL_MOST_EMAILS_RECEIVED' }, outbound_emails: { icon: 'sicon-email', label: 'LBL_MOST_EMAILS_SENT' }, calls: { icon: 'sicon-phone', label: 'LBL_MOST_CALLS_MADE' } }, loadData: function(params) { if(this.meta.config) { return; } var url = app.api.buildURL('mostactiveusers', null, null, {days: this.settings.get("filter_duration")}), self = this; app.api.call("read", url, null, { success: function(data) { if(self.disposed) { return; } var models = []; _.each(data, function(attributes, module){ if(_.isEmpty(attributes)) { return; } var model = new app.Bean(_.extend({ id: _.uniqueId('aui') }, attributes)); model.module = module; model.set("name", model.get("first_name") + ' ' + model.get("last_name")); model.set("icon", self._mapping[module]['icon']); var template = Handlebars.compile(app.lang.get(self._mapping[module]['label'], self.module)); model.set("label", template({ count: model.get("count") })); model.set("pictureUrl", app.api.buildFileURL({ module: "Users", id: model.get("user_id"), field: "picture" })); models.push(model); }, this); self.collection.reset(models); }, complete: params ? params.complete : null }); }, filterChanged: function(evt) { this.loadData(); }, _dispose: function() { if(this.collection) { this.collection.off("reset", null, this); } app.view.View.prototype._dispose.call(this); } }) }, "about-resources": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.AboutResourcesView * @alias SUGAR.App.view.views.BaseHomeAboutResourcesView * @extends View.View */ ({ // About-resources View (base) /** * @inheritdoc * * Initializes the view with the serverInfo. */ initialize: function(opts) { this.serverInfo = app.metadata.getServerInfo(); app.view.View.prototype.initialize.call(this, opts); } }) }, "search-facet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Home.SearchFacetView * @alias SUGAR.App.view.views.BaseSearchFacetView * @extends View.View */ ({ // Search-facet View (base) className: 'group/search-facet', events: { 'click [data-facet-criteria]': 'itemClicked' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); /** * The facet id. * * @property {String} */ this.facetId = this.meta.facet_id; /** * Boolean to indicate if the facet is a single criteria facet or a multi * criterias facet. `true` is a single criteria facet. * * @type {boolean} */ this.isSingleItem = this.meta.ui_type === 'single'; /** * The array of facets criterias to be displayed. * * @property {Array} facetItems */ this.facetItems = []; if (this.context.get('facets') && this.context.get('facets')[this.facetId]) { this.parseFacetsData(); } this.bindFacetsEvents(); }, /** * Binds context events related to facets changes. */ bindFacetsEvents: function() { this.context.on('facets:change', this.parseFacetsData, this); }, /** * Parses facets data and renders the view. */ parseFacetsData: function() { var currentFacet = this.context.get('facets')[this.facetId]; var selectedFacets = this.context.get('selectedFacets'); if (_.isUndefined(currentFacet)) { this.render(); return; } if (this.isSingleItem && currentFacet.results.count === 0) { this.$el.addClass('disabled'); this.$el.data('action', 'disabled'); } else { this.$el.data('action', 'enabled'); this.$el.removeClass('disabled'); } if (this.clickedFacet) { this.clickedFacet = false; return; } if (_.isUndefined(currentFacet)) { app.logger.warn('The facet type : ' + this.facetId + 'is not returned by the server.' + ' Therefore, the facet dashlet will have no data.'); return; } var results = currentFacet.results; this.facetItems = []; if (this.isSingleItem) { this.facetItems = [{ key: this.facetId, label: app.lang.get(this.meta.label, 'Filters'), count: results.count, selected: selectedFacets[this.facetId] }]; this.render(); return; } var labelFunction = this._getDefaultLabel; let modulesCount = 0; _.each(results, function(val, key) { var selected = _.contains(selectedFacets[this.facetId], key); this.facetItems.push({key: key, label: labelFunction(key), count: val, selected: selected}); modulesCount++; }, this); if (_.isEmpty(this.facetItems)) { this.layout.context.trigger('dashboard:collapse:fire', true); } else { this.layout.context.trigger('dashboard:collapse:fire', false); this.facetItems = _.sortBy(this.facetItems, 'count').reverse(); } this.context.trigger('search:modules:number:change', modulesCount); this.render(); }, /** * Selects or unselect a facet item. * * @param {Event} event The `click` event. */ itemClicked: function(event) { var currentTarget = this.$(event.currentTarget); if (this.$el.data('action') === 'disabled') { return; } if (!this.clickedFacet && !this.collection.dataFetched) { return; } var facetCriteriaId = currentTarget.data('facet-criteria'); currentTarget.toggleClass('selected'); this.clickedFacet = true; this.context.trigger('facet:apply', this.facetId, facetCriteriaId, this.isSingleItem); }, /** * Gets the facet criteria id to use it as a label. * * @param {Object} bucket The facet item. * @return {string} The label for this item. * @private */ _getDefaultLabel: function(key) { return app.lang.getModuleName(key, {plural: true}); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "console-side-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Home.ConsoleSideDrawerLayout * @alias SUGAR.App.view.layouts.HomeConsoleSideDrawerLayout * @extends View.Layouts.Base.SideDrawerLayout * @deprecated ConsoleSideDrawerLayout controller is deprecated as of 11.2.0. Use SideDrawerLayout instead. */ ({ // Console-side-drawer Layout (base) extendsFrom: 'SideDrawerLayout', /** * @inheritdoc */ initialize: function(options) { app.logger.warn( 'ConsoleSideDrawerLayout controller is deprecated as of 11.2.0. Use SideDrawerLayout instead.' ); this._super('initialize', [options]); } }) }, "sidebar-nav-flyout-module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.HomeSidebarNavFlyoutModuleMenuLayout * @alias SUGAR.App.view.layouts.BaseHomeSidebarNavFlyoutModuleMenuLayout * @extends View.Layout */ ({ // Sidebar-nav-flyout-module-menu Layout (base) extendsFrom: 'SidebarNavFlyoutModuleMenuLayout', /** * @inheritdoc */ _getMenuActions: function() { const menu = this._super('_getMenuActions'); return _.filter(menu, (v) => !(v.route && v.route === '#activities' && !app.config.activityStreamsEnabled)); }, }) }, "dashboard-filters": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Home.DashboardFiltersLayout * @alias SUGAR.App.view.layouts.BaseHomeDashboardFiltersLayout * @extends View.Layouts.Base.Layout */ ({ // Dashboard-filters Layout (base) className: 'h-full hidden', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['DashboardFiltersVisibility']); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); this._manageDashboardFiltersVisibility(); }, /** * Init properties */ _initProperties: function() { this._activeFilterGroupId = false; this._dashboardFilterView = false; this._filterGroups = false; this.dashboardId = this.model.id; this.componentLastStateKey = 'dashboard-filters'; this.hasAccessToReports = app.acl.hasAccess('view', 'Reports'); this.initFiltersVisibilityProperties(); }, /** * @inheritdoc */ _registerEvents: function() { if (!this.hasAccessToReports) { return; } this.listenTo(this.context, 'dashboard-filter-toggled', this.toggleFilterComponent); this.listenTo(this.context, 'dashboard-filter-mode-updated', this.manageFilterState); this.listenTo(this.context, 'dashboard-filters-canceled', this.cancelDashboardFilters); this.listenTo(this.context, 'dashboard-filters-save', this.saveDashboardFilters); this.listenTo(this.context, 'dashboard-filters-apply', this.applyDashboardFilters); this.listenTo(this.model, 'sync', this.modelSync); this.listenTo(this.context, 'filter-operator-data-changed', this.updateGroupFilters); this.listenTo(this.context, 'dashboard-filters-interaction', this.updateGroupFilters); }, /** * Model synced */ modelSync: function() { const metadata = this.model.get('metadata'); let filterGroups = {}; if (_.has(metadata, 'filters')) { filterGroups = metadata.filters; } const currentUserRestrictedGroups = this.model.get('metadata').currentUserRestrictedGroups; if (!_.isUndefined(currentUserRestrictedGroups)) { this.currentUserRestrictedGroups = currentUserRestrictedGroups; } if (JSON.stringify(filterGroups) !== JSON.stringify(this._filterGroups)) { this._syncGroups(); } }, /** * Get the groups from meta, or create a new one * * @param {Object} options */ _syncGroups: function(options = {}) { let filterGroups = {}; const metadata = this.model.get('metadata'); if (_.has(metadata, 'filters')) { filterGroups = _.isEmpty(metadata.filters) ? {} : metadata.filters; } this._filterGroups = app.utils.deepCopy(filterGroups); this._applyUserFilters(metadata, options); this.context.trigger('dashboard-filters-metadata-loaded', this._filterGroups); this.manageFilterState('detail'); }, /** * Apply User filters * * @param {Object} metadata */ _applyUserFilters: function(metadata, options) { const lastStateKey = this._getUserLastStateKey(); const userDashboardMetadata = app.user.lastState.get(lastStateKey); if (this.model.get('is_template') && !_.isEmpty(userDashboardMetadata)) { this._filterGroups = userDashboardMetadata.filters; return; } if (_.isEmpty(userDashboardMetadata) || userDashboardMetadata.runtimeFiltersDateModified === metadata.runtimeFiltersDateModified) { return; } if (!_.isUndefined(metadata.runtimeFiltersDateModified) && moment(userDashboardMetadata.runtimeFiltersDateModified).isBefore(metadata.runtimeFiltersDateModified)) { let showNotifyLastRefresh = true; if (!_.isEmpty(options) && _.has(options, 'notifyLastRefresh')) { showNotifyLastRefresh = options.notifyLastRefresh; } if (showNotifyLastRefresh) { app.alert.show('modify_since_last_refresh', { level: 'info', messages: app.lang.get('LBL_FILTER_UPDATES_SINCE_LAST_REFRESH', 'Dashboards'), autoClose: true, }); } this._resetLastState(); return; } this._filterGroups = userDashboardMetadata.filters; }, /** * Manage dashboard filters visibility */ _manageDashboardFiltersVisibility: function() { const filtersVisible = this.isDashboardFiltersPanelActive(); if (filtersVisible === true) { this.toggleFilterComponent(true); } }, /** * Manage state * * @param {string} state */ manageFilterState: function(state) { const isEdit = state === 'edit'; const viewNameToBeCreated = isEdit ? 'dashboard-filters-edit' : 'dashboard-filters-detail'; if (!this._dashboardFilterView || this._dashboardFilterView._editMode !== isEdit) { this._destroyDashboardFilterView(); this._createDashboardFilterView(viewNameToBeCreated); this.toggleDashboardFabButton(state); return; } this._dashboardFilterView.manageDashboardFilters(this._filterGroups); }, /** * Show/Hide fab button * * @param {string} state */ toggleDashboardFabButton: function(state) { if (!_.has(this, 'layout') || !_.has(this.layout, 'layout')) { return; } const fabComponent = this.layout.layout.getComponent('dashboard-fab'); if (!fabComponent) { return; } state === 'edit' ? fabComponent.hide() : fabComponent.show(); }, /** * Destroy dashboard filter view */ _destroyDashboardFilterView: function() { if (this._dashboardFilterView) { this._dashboardFilterView.dispose(); this._dashboardFilterView = false; } }, /** * Create dashboard filter view * * @param {string} viewNameToBeCreated */ _createDashboardFilterView: function(viewNameToBeCreated) { this._dashboardFilterView = app.view.createView({ type: viewNameToBeCreated, context: this.context, model: this.model, layout: this, }); this._dashboardFilterView.render(); this.$('[data-container="dashboard-filters-container"]').append(this._dashboardFilterView.$el); this._dashboardFilterView.manageDashboardFilters(this._filterGroups); if (viewNameToBeCreated === 'dashboard-filters-detail') { this._dashboardFilterView.toggleLoading(true); } }, /** * Set the new filters on the model */ updateGroupFilters: function() { const metadata = app.utils.deepCopy(this.model.get('metadata')); metadata.filters = this._filterGroups; this.model.set('metadata', metadata, {silent: true}); this.context.trigger('filter-field-updated'); }, /** * Cancel the dashboard filters state */ cancelDashboardFilters: function() { if (this.model.revertAttributes) { this.model.revertAttributes({silent: true}); } this._syncGroups(); }, /** * Return invalid groups * * @return {Array} */ _invalidGroupsForSave: function() { const filterGroups = app.utils.deepCopy(this._filterGroups); const invalidGroups = _.chain(filterGroups) .map((item, key) => { return Object.assign({}, item, {key}); }) .filter((item) => { return item.fields.length < 1; }) .pluck('key') .value(); return invalidGroups; }, /** * Check if we are able to save * * @return {boolean} */ _isValidSave: function() { const invalidGroups = this._invalidGroupsForSave(); if (invalidGroups.length > 0) { this.context.trigger('dashboard-filter-group-invalid-save', invalidGroups); app.alert.show('invalid_groups', { level: 'error', messages: app.lang.get('LBL_ONE_GROUP_REQUIRED', this.module), autoClose: true, }); return false; } // validate each filter of each group if (this._dashboardFilterView && !this._dashboardFilterView.isValid()) { app.alert.show('runtime-filter-invalid', { level: 'error', messages: app.lang.get('LBL_RUNTIME_FILTERS_INVALID'), autoClose: true, }); return false; } return true; }, /** * Apply Dashboard Filters */ applyDashboardFilters: function() { if (!this._isValidSave()) { return; } this.context.trigger('show-dashlet-loading'); this._dashboardFilterView.toggleLoading(true); const metadata = app.utils.deepCopy(this.model.get('metadata')); const runtimeFiltersDateModified = this._getCurrentDatetime(); metadata.runtimeFiltersDateModified = runtimeFiltersDateModified; metadata.filters = this._filterGroups; this._updateUserLastState(metadata); if (this.model.revertAttributes) { this.model.revertAttributes({silent: true}); } }, /** * * Save the dashboard filters state * */ saveDashboardFilters: function() { if (!this._isValidSave()) { return; } this.context.trigger('show-dashlet-loading'); this._dashboardFilterView.toggleLoading(true); const metadata = app.utils.deepCopy(this.model.get('metadata')); metadata.filters = this._filterGroups; const runtimeFiltersDateModified = this._getCurrentDatetime(); metadata.runtimeFiltersDateModified = runtimeFiltersDateModified; this.model.set('metadata', metadata, {silent: true}); this.model.save({}, { silent: true, showAlerts: false, success: () => { this._syncGroups({ notifyLastRefresh: false, }); this._dashboardFilterView.toggleLoading(false); this.context.trigger('dashboard-saved-success'); this.context.trigger('dashboard-filter-mode-changed', 'detail', true); }, error: () => { app.alert.show('error_while_save', { level: 'error', title: app.lang.get('ERR_INTERNAL_ERR_MSG'), messages: ['ERR_HTTP_500_TEXT_LINE1', 'ERR_HTTP_500_TEXT_LINE2'] }); } }); }, /** * Update user last state * * Only update the values for the user and not for the dashboard * * @param {Object} metadata */ _updateUserLastState: function(metadata) { this._setLastState({ filters: metadata.filters, runtimeFiltersDateModified: metadata.runtimeFiltersDateModified, }); this._syncGroups(); this._dashboardFilterView.toggleLoading(false); this.context.trigger('silent-refresh-dashlet-results', true); }, /** * Get current UTC time * * @return {string} */ _getCurrentDatetime: function() { return app.date().format('YYYY-MM-DD HH:mm:ss'); }, /** * Set last state * * @param {Object} filtersMetadata */ _setLastState: function(filtersMetadata) { const lastStateKey = this._getUserLastStateKey(); app.user.lastState.set( lastStateKey, filtersMetadata ); }, /** * Reset last state */ _resetLastState: function() { const lastStateKey = this._getUserLastStateKey(); app.user.lastState.set(lastStateKey, {}); }, /** * Get the unique key for the Dashboard and User combination * * @return {string} */ _getUserLastStateKey: function() { const dashboardKey = `Dashboards:${this.dashboardId}`; const lastStateKey = app.user.lastState.buildKey( this.componentLastStateKey, app.user.id, dashboardKey ); return lastStateKey; }, /** * Function that adds the filter component to the dashboard * * @param {boolean} toggle */ toggleFilterComponent: function(toggle) { this.$el.toggleClass('hidden', !toggle); this.context.trigger('dashboard-filter-mode-changed', 'detail'); this.context.trigger('dashboard-filter-mode-updated', 'detail'); }, /** * @inheritdoc */ _dispose: function() { this._destroyDashboardFilterView(); this._super('_dispose'); }, }) }, "dashboard": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Home.DashboardLayout * @alias SUGAR.App.view.layouts.HomeDashboardLayout * @extends View.Layouts.DashboardLayout * @deprecated 7.9.0 Will be removed in 7.11.0. Use * {@link View.Layouts.Dashboards.DashboardLayout} instead. */ ({ // Dashboard Layout (base) extendsFrom: 'DashboardLayout', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Home.DashboardLayout has been deprecated since 7.9.0.0. ' + 'It will be removed in 7.11.0.0. Please use View.Layouts.Dashboards.DashboardLayout instead.'); this._super('initialize', [options]); } }) }, "records": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Home.RecordsLayout * @alias SUGAR.App.view.layouts.HomeRecordsLayout * @extends View.Layout * @deprecated 7.9.0 Will be removed in 7.11.0. Use * {@link View.Layouts.Home.Record} instead. */ ({ // Records Layout (base) /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Home.RecordsLayout has been deprecated since 7.9.0.0. ' + 'It will be removed in 7.11.0.0. Please use View.Layouts.Home.Record instead.'); this._super('initialize', [options]); } }) }, "list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Home.ListLayout * @alias SUGAR.App.view.layouts.HomeListLayout * @extends View.DashboardLayout * @deprecated 7.9.0 Will be removed in 7.11.0. Use * {@link View.Layouts.Home.Record} instead. */ ({ // List Layout (base) extendsFrom: 'DashboardLayout', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Home.ListLayout has been deprecated since 7.9.0.0. ' + 'It will be removed in 7.11.0.0. Please use View.Layouts.Home.Record instead.'); this._super('initialize', [options]); } }) } }} , "datas": {} }, "Contacts":{"fieldTemplates": {} , "views": { "base": { "subpanel-for-cases": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Custom RecordlistView used within Subpanel layouts. * * @class View.Views.Base.Contacts.SubpanelForCasesView * @alias SUGAR.App.view.views.SubpanelForCasesView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-for-cases View (base) extendsFrom: 'SubpanelListView', unlinkPrimary: false, /** * Formats the messages to display in the alerts when unlinking a record. * * @override * @param {Data.Bean} model The model concerned. * @return {Object} The list of messages. * @return {string} return.confirmation Confirmation message. * @return {string} return.success Success message. */ getUnlinkMessages: function(model) { var message = this._super('getUnlinkMessages', [model]); var context = this.getMessageContext(model); if (this.unlinkPrimary) { // Show the message about clearing Primary Conact Field // if it's similar with the unlinked Contact message.confirmation = app.utils.formatString( app.lang.get('NTC_UNLINK_CASES_CONTACT_CONFIRMATION'), [context] ); } return message; }, /** * Show the alert in the time of unlinking a record. * * @override * @param {Data.Bean} model The model concerned. */ showUnlinkMessage: function(model) { var recordModel = this.context.parent.children[0].parent.get('model'); var contactField = recordModel.fields.primary_contact_name; this.unlinkPrimary = (recordModel.get('primary_contact_id') === model.get('id')) || (recordModel.previous('primary_contact_id') === model.get('id')); if (this.unlinkPrimary && contactField.required === true) { var context = this.getMessageContext(model); // Show message about it's not possible to unlink the Primary Contact // if it's a required field app.alert.show('unlink_error', { level: 'error', messages: app.utils.formatString( app.lang.get('NTC_UNLINK_CASES_CONTACT_ERROR'), [context] ), }); this._modelToUnlink = null; return; } this._super('showUnlinkMessage', [model]); }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this.plugins.push('ContactsPortalMetadataFilter'); this._super('initialize', [options]); this.removePortalFieldsIfPortalNotActive(this.meta); this.listenTo(this.context, 'button:custom_sf_iframebutton:click', this.viewInMarket); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Contacts.CreateView * @alias SUGAR.App.view.views.ContactsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /* * Enable pre-population from Omnichanel */ omniPopulation: true, /** * Gets the portal status from metadata to know if we render portal specific fields. * @override * @param options */ initialize: function(options) { //Plugin is registered by the Contact record view this.plugins = _.union(this.plugins || [], ["ContactsPortalMetadataFilter"]); this._super("initialize", [options]); this.removePortalFieldsIfPortalNotActive(this.meta); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Contacts.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseContactsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'account_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Contacts.PreviewView * @alias SUGAR.App.view.views.BaseContactsPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', initialize: function(options) { //Plugin is registered by the Contact record view this.plugins = _.union(this.plugins || [], ["ContactsPortalMetadataFilter"]); this._super("initialize", [options]); }, /** * Gets the portal status from metadata to know if we render portal specific fields. * @override * @param options */ _previewifyMetadata: function(meta) { meta = this._super("_previewifyMetadata", [meta]); this.removePortalFieldsIfPortalNotActive(meta); return meta; } }) } }} , "layouts": {} , "datas": {} }, "Accounts":{"fieldTemplates": {} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Accounts.RecordView * @alias SUGAR.App.view.views.BaseAccountsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Opportunities":{"fieldTemplates": { "base": { "enum-cascade": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.EnumCascadeField * @alias SUGAR.App.view.fields.BaseOpportunitiesEnumCascadeField * @extends View.Fields.Base.EnumField */ ({ // Enum-cascade FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['Cascade']); this._super('initialize', [options]); }, /** * @inheritdoc */ _loadTemplate: function() { if (this.action !== 'edit' && this.action !== 'disabled') { this.type = 'enum'; } this._super('_loadTemplate'); this.type = this.def.type; } }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * extend save options * @param {Object} options save options. * @return {Object} modified success param. */ getCustomSaveOptions: function(options) { // make copy of original function we are extending var origSuccess = options.success; // return extended success function with added alert return { success: _.bind(function() { if (_.isFunction(origSuccess)) { origSuccess.apply(this, arguments); } if(this.context.parent) { var oppsCfg = app.metadata.getModule('Opportunities', 'config'), reloadLinks = ['opportunities']; if (oppsCfg && oppsCfg.opps_view_by == 'RevenueLineItems') { reloadLinks.push('revenuelineitems'); } this.context.parent.set('skipFetch', false); // reload opportunities subpanel this.context.parent.trigger('subpanel:reload', {links: reloadLinks}); // catch the after save moment this.context.parent.trigger('opportunities:record:saved'); } }, this) }; } }) }, "pipeline-type": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.PipelineTypeField * @alias SUGAR.App.view.fields.BaseOpportunitiesPipelineTypeField * @extends View.Fields.Base.PipelineTypeField */ ({ // Pipeline-type FieldTemplate (base) extendsFrom: 'PipelineTypeField', /** * @inheritdoc */ getTabs: function() { this.tabs = []; var fieldsForTabs = ['date_closed']; var config = app.metadata.getModule('VisualPipeline', 'config'); fieldsForTabs.push(config.table_header[this.module]); var fieldMeta = app.metadata.getModule(this.module, 'fields'); _.each(fieldsForTabs, function(field) { var label = field === 'date_closed' ? 'LBL_TIME' : fieldMeta[field].vname; var fieldLabel = app.lang.getModString(label, this.module); var metaObject = { headerLabel: fieldLabel, moduleField: field, tabLabel: app.lang.get('LBL_PIPELINE_VIEW_TAB_NAME', this.module, { module: app.lang.get('LBL_MODULE_NAME', this.module), fieldName: fieldLabel }) }; this.tabs.push(metaObject); }, this); } }) }, "rowactions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /* * @class View.Fields.Base.Opportunities.RowactionsField * @alias SUGAR.App.view.fields.BaseOpportunitiesRowactionsField * @extends View.Fields.Base.RowactionsField */ ({ // Rowactions FieldTemplate (base) extendsFrom: 'RowactionsField', /** * Enable or disable caret depending on if there are any enabled actions in the dropdown list * * @inheritdoc * @private */ _updateCaret: function() { // Left empty on purpose, the menu should always show } }) }, "renewal": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.RenewalField * @alias SUGAR.App.view.fields.BaseOpportunitiesRenewalField * @extends View.Fields.Base.BaseField */ ({ // Renewal FieldTemplate (base) /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, month: '', year: '', /** * @inheritdoc */ initialize: function(options) { options.def.readonly = true; var date = this.model.get('date_closed'); this.month = app.date(date).format('MMMM'); this.year = app.date(date).format('YYYY'); this._super('initialize', [options]); }, bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:date_closed', function() { var date = this.model.get('date_closed'); this.month = app.date(date).format('MMMM'); this.year = app.date(date).format('YYYY'); this.render(); }, this); }, }) }, "rowaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Rowaction FieldTemplate (base) extendsFrom: "RowactionField", /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins, ['DisableDelete']); this._super("initialize", [options]); } }) }, "date-cascade": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.DateCascadeField * @alias SUGAR.App.view.fields.BaseOpportunitiesDateCascadeField * @extends View.Fields.Base.DateField */ ({ // Date-cascade FieldTemplate (base) extendsFrom: 'DateField', /** * Name of validation task for Service Start Date */ validationName: null, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['Cascade']); this._super('initialize', [options]); var config = app.metadata.getModule('Opportunities', 'config'); if (this.name === 'service_start_date' && config.opps_view_by === 'RevenueLineItems') { app.error.errorName2Keys.service_start_date_exceeds_end_date = 'LBL_SERVICE_START_DATE_INVALID'; this.validationName = 'start_date_before_fixed_end_' + this.cid; this.model.addValidationTask(this.validationName, _.bind(this.validateServiceStartDate, this)); } }, /** * Validates that the service start date is not after the end date of any * add on RLIs. * @param fields * @param errors * @param callback */ validateServiceStartDate: function(fields, errors, callback) { // We don't want to perform this check when creating an opportunity or if the service // start date is empty (no service RLIs, for example) var serviceStartDate = this.model.get('service_start_date'); if (this.field.action !== 'edit' || _.isEmpty(serviceStartDate)) { callback(null, fields, errors); return; } // Show the Saving alert while validation runs. Otherwise the UI appears to be // unresponsive during this time. app.alert.show('service_start_date_validation', { level: 'process', title: app.lang.get('LBL_SAVING'), autoClose: false }); var forecastConfig = app.metadata.getModule('Forecasts', 'config'); var closedSalesStages = _.union(forecastConfig.sales_stage_won, forecastConfig.sales_stage_lost); var moduleName = app.data.getRelatedModule(this.model.module, 'revenuelineitems'); var rliCollection = app.data.createBeanCollection(moduleName); rliCollection.filterDef = { 'filter': [ {'opportunity_id': {'$equals': this.model.get('id')}}, {'add_on_to_id': {'$not_empty': ''}}, {'sales_stage': {'$not_in': closedSalesStages}}, {'service_end_date': {'$lt': serviceStartDate}} ] }; rliCollection.fetch({ showAlerts: false, fields: ['id'], relate: false, success: _.bind(function(data) { if (data.length > 0) { _.extend(errors, { 'service_start_date': { 'service_start_date_exceeds_end_date': true } }); this._showValidationMessage(); } }, this), complete: function() { app.alert.dismiss('service_start_date_validation'); callback(null, fields, errors); } }); }, /** * Shows the invalid service start date error message * @private */ _showValidationMessage: function() { app.alert.show('service_start_date_exceeds_end_date', { level: 'error', messages: app.lang.get('LBL_SERVICE_START_DATE_INVALID', 'Opportunities'), }); }, /** * @inheritdoc */ _dispose: function() { if (this.validationName) { this.model.removeValidationTask(this.validationName); } this._super('_dispose'); } }) }, "fieldset-cascade": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.FieldsetCascadeField * @alias SUGAR.App.view.fields.BaseOpportunitiesFieldsetCascadeField * @extends View.Fields.Base.FieldsetField */ ({ // Fieldset-cascade FieldTemplate (base) extendsFrom: 'FieldsetField', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['Cascade']); this._super('initialize', [options]); }, /** * @inheritdoc */ _loadTemplate: function() { // If the field isn't editable or disabled, fall back to fieldset's // base templates. if (this.action !== 'edit' && this.action !== 'disabled') { this.type = 'fieldset'; } // If this field is disabled, setDisabled will cascade that down // to the subfields if (this.action === 'disabled') { this.setDisabled(true); } this._super('_loadTemplate'); this.type = this.def.type; } }) }, "actiondropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Create a dropdown button that contains multiple * {@link View.Fields.Base.RowactionField} fields. * * @class View.Fields.Base.Opportunities.ActiondropdownField * @alias SUGAR.App.view.fields.BaseOpportunitiesActiondropdownField * @extends View.Fields.Base.ActiondropdownField */ ({ // Actiondropdown FieldTemplate (base) extendsFrom: 'ActiondropdownField', /** * Enable or disable caret depending on if there are any enabled actions in the dropdown list * * @inheritdoc * @private */ _updateCaret: function() { // Left empty on purpose, the menu should always show } }) } }} , "views": { "base": { "product-quick-picks": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Opportunities.ProductQuickPicksView * @alias SUGAR.App.view.views.OpportunitiesProductQuickPicksView * @extends View.Views.Base.ProductQuickPicksView * @deprecated Use {@link View.Views.Base.ProductQuickPicksView} instead */ ({ // Product-quick-picks View (base) extendsFrom: 'ProductQuickPicksView', initialize: function(options) { app.logger.warn('View.Views.Base.Opportunities.ProductQuickPicksView is deprecated. Use ' + 'View.Views.Base.ProductQuickPicksView instead'); this._super('initialize', [options]); } }) }, "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Massupdate View (base) extendsFrom: "MassupdateView", /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['DisableMassDelete', 'CommittedDeleteWarning']); this._super("initialize", [options]); }, /** * * @inheritdoc */ setMetadata: function(options) { var config = app.metadata.getModule('Forecasts', 'config'); this._super("setMetadata", [options]); if (!config || (config && !config.is_setup)) { _.each(options.meta.panels, function(panel) { _.every(panel.fields, function (item, index) { if (_.isEqual(item.name, "commit_stage")) { panel.fields.splice(index, 1); return false; } return true; }, this); }, this); } }, /** * @inheritdoc */ save: function(forCalcFields) { var forecastCfg = app.metadata.getModule("Forecasts", "config"); if (forecastCfg && forecastCfg.is_setup) { // Forecasts is enabled and setup var hasCommitStage = _.some(this.fieldValues, function(field) { return field.name === 'commit_stage'; }), hasClosedModels = false; if(!hasCommitStage && this.defaultOption.name === 'commit_stage') { hasCommitStage = true; } if(hasCommitStage) { hasClosedModels = this.checkMassUpdateClosedModels(); } if(!hasClosedModels) { // if this has closed models, first time through will uncheck but not save // if this doesn't it will save like normal this._super('save', [forCalcFields]); } } else { // Forecasts is not enabled and the commit_stage field isn't in the mass update list this._super('save', [forCalcFields]); } } }) }, "pipeline-recordlist-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Opportunities.PipelineRecordlistContentView * @alias App.view.views.BaseOpportunitiesPipelineRecordlistContentView * @extends View.Views.Base.PipelineRecordlistContentView */ ({ // Pipeline-recordlist-content View (base) extendsFrom: 'PipelineRecordlistContentView', /** * Don't change the expected close date or the sales stage of an opp that is already closed * @inheritdoc */ saveModel: function(model, pipelineData) { var oppCfg = app.metadata.getModule('Opportunities', 'config'); var rliMode = oppCfg.opps_view_by === 'RevenueLineItems'; if (_.contains(['date_closed', 'sales_stage'], this.headerField) && rliMode) { var forecastConfig = app.metadata.getModule('Forecasts', 'config') || {}; var closedWon = forecastConfig.sales_stage_won || ['Closed Won']; var closedLost = forecastConfig.sales_stage_lost || ['Closed Lost']; var closedStatuses = closedWon.concat(closedLost); var status = model.get('sales_status'); if (_.contains(closedStatuses, status)) { this._postChange(model, true, pipelineData); var moduleName = app.lang.getModuleName(this.module, {plural: false}); var fieldLabel = app.metadata.getField({module: 'Opportunities', name: this.headerField}).vname; var fieldName = app.lang.get(fieldLabel, this.module); app.alert.show('error_converted', { level: 'error', messages: app.lang.get( 'LBL_PIPELINE_ERR_CLOSED_SALES_STAGE', this.module, {moduleSingular: moduleName, fieldName: fieldName} ) }); return; } } this._super('saveModel', [model, pipelineData]); }, /** * @inheritdoc */ getFieldsForFetch: function() { var fields = this._super('getFieldsForFetch'); var cfg = app.metadata.getModule('Opportunities', 'config'); var newFields = ['closed_revenue_line_items']; if (cfg && cfg.opps_view_by) { newFields.push(cfg.opps_view_by === 'RevenueLineItems' ? 'sales_status' : 'sales_stage'); } return _.union(fields, newFields, [this.headerField]); }, /** * @inheritdoc */ _setNewModelValues: function(model, ui) { var ctxModel = this.context.get('model'); var $ulEl = this.$(ui.item).parent('ul'); var headerFieldValue = $ulEl.attr('data-column-key'); if (ctxModel && ctxModel.get('pipeline_type') === 'date_closed') { var dateClosed = app.date(headerFieldValue, 'MMMM YYYY') .endOf('month') .formatServer(true); model.set('date_closed', dateClosed); model.set('date_closed_cascade', dateClosed); } else { model.set(this.headerField, headerFieldValue); if (this.headerField === 'sales_stage') { model.set({ probability: app.utils.getProbabilityBySalesStage(headerFieldValue), commit_stage: app.utils.getCommitStageBySalesStage(headerFieldValue), sales_stage_cascade: headerFieldValue }); } } } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesRecordlistView * @alias SUGAR.App.view.views.BaseOpportunitiesRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['CommittedDeleteWarning']); this._super("initialize", [options]); }, /** * @inheritdoc */ parseFieldMetadata: function(options) { options = this._super('parseFieldMetadata', [options]); app.utils.hideForecastCommitStageField(options.meta.panels); return options; }, }) }, "subpanel-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesSubpanelListView * @alias SUGAR.App.view.views.BaseOpportunitiesSubpanelListView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-list View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['CommittedDeleteWarning']); this._super('initialize', [options]); }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * Holds a reference to the alert this view triggers */ alert: undefined, /** * Holds a reference to the alert this view triggers */ cancelClicked: function() { /** * todo: this is a sad way to work around some problems with sugarlogic and revertAttributes * but it makes things work now. Probability listens for Sales Stage to change and then by * SugarLogic, it updates probability when sales_stage changes. When the user clicks cancel, * it goes to revertAttributes() which sets the model back how it was, but when you try to * navigate away, it picks up those new changes as unsaved changes to your model, and tries to * falsely warn the user. This sets the model back to those changed attributes (causing them to * show up in this.model.changed) then calls the parent cancelClicked function which does the * exact same thing, but that time, since the model was already set, it doesn't see anything in * this.model.changed, so it doesn't warn the user. */ var changedAttributes = this.model.changedAttributes(this.model.getSynced()); this.model.set(changedAttributes, {revert: true, hideDbvWarning: true}); this._super('cancelClicked'); }, /** * @inheritdoc * @param {Object} options */ initialize: function(options) { this.plugins = _.union(this.plugins, ['LinkedModel', 'HistoricalSummary', 'CommittedDeleteWarning']); this.addInitListener(); this._super('initialize', [options]); app.utils.hideForecastCommitStageField(this.meta.panels); }, /** * Add the initListener if RLI's are being used and the current user has Edit access to RLI's */ addInitListener: function() { // if we are viewing by RevenueLineItems and we have access to edit/create RLI's then we should // display the warning if no rli's exist if (app.metadata.getModule('Opportunities', 'config').opps_view_by == 'RevenueLineItems' && app.acl.hasAccess('edit', 'RevenueLineItems')) { this.once('init', function() { var rlis = this.model.getRelatedCollection('revenuelineitems'); rlis.once('reset', function(collection) { // check if the RLI collection is empty // and make sure there isn't another RLI warning on the page if (collection.length === 0 && $('#createRLI').length === 0) { this.showRLIWarningMessage(this.model.module); } }, this); rlis.fetch({relate: true}); }, this); } }, /** * Loops through all fields on the model returning only the fields with calculated => true set * @returns {Array} */ getCalculatedFields: function() { return _.filter(this.model.fields, function (field) { return field.calculated; }); }, /** * @inheritdoc */ setupDuplicateFields: function(prefill) { // Clear sugar predict fields const predictFields = [ 'ai_opp_conv_score_enum', 'ai_opp_conv_score_enum_c' ]; predictFields.forEach(fieldName => prefill.unset(fieldName)); if (app.metadata.getModule('Opportunities', 'config').opps_view_by === 'RevenueLineItems') { var calcFields = this.getCalculatedFields(); if (calcFields) { _.each(calcFields, function(field) { prefill.unset(field.name); }, this); } } }, /** * Display the warning message about missing RLIs */ showRLIWarningMessage: function() { // add a callback to close the alert if users navigate from the page app.routing.before('route', this.dismissAlert, this); var message = app.lang.get('TPL_RLI_CREATE', 'Opportunities') + ' <a href="javascript:void(0);" id="createRLI">' + app.lang.get('TPL_RLI_CREATE_LINK_TEXT', 'Opportunities') + '</a>'; this.alert = app.alert.show('opp-rli-create', { level: 'warning', autoClose: false, title: app.lang.get('LBL_ALERT_TITLE_WARNING') + ':', messages: message, onLinkClick: _.bind(function() { this.openRLICreate(); }, this), onClose: _.bind(function() { app.routing.offBefore('route', this.dismissAlert, this); }, this) }); }, /** * Handle dismissing the RLI create alert */ dismissAlert: function() { // close RLI warning alert app.alert.dismiss('opp-rli-create'); // remove before route event listener app.routing.offBefore('route', this.dismissAlert, this); }, /** * Open a new Drawer with the RLI Create Form */ openRLICreate: function() { // close RLI warning alert this.dismissAlert(); var model = this.createLinkModel(this.createdModel || this.model, 'revenuelineitems'); app.drawer.open({ layout: 'create', context: { create: true, module: model.module, model: model } }, _.bind(this.rliCreateClose, this)); }, /** * Callback for when the create drawer closes * @param {String} model */ rliCreateClose: function(model) { if (!model) { return; } var ctx = this.listContext || this.context; ctx.resetLoadFlag(); ctx.set('skipFetch', false); ctx.loadData(); // find the child collection for the RLI subpanel // if we find one and it has the loadData method, call that method to // force the subpanel to load the data. var rli_ctx = _.find(ctx.children, function(child) { return child.get('module') === 'RevenueLineItems'; }, this); if (!_.isUndefined(rli_ctx) && _.isFunction(rli_ctx.loadData)) { rli_ctx.loadData(); } } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseOpportunitiesConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * The current opps_view_by config setting when the view is initialized */ currentOppsViewBySetting: undefined, /** * Stores if Forecasts is set up or not */ isForecastsSetup: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.currentOppsViewBySetting = this.model.get('opps_view_by'); // get the boolean form of if Forecasts is configured this.isForecastsSetup = !!app.metadata.getModule('Forecasts', 'config').is_setup; }, /** * Before the save triggers, we need to show the alert so the users know it's doing something. * @private */ _beforeSaveConfig: function() { app.alert.show('opp.config.save', {level: 'process', title: app.lang.getAppString('LBL_SAVING')}); }, /** * @inheritdoc * @param {function} onClose */ showSavedConfirmation: function(onClose) { app.alert.dismiss('opp.config.save'); this._super('showSavedConfirmation', [onClose]); }, /** * Displays the Forecast warning confirm alert */ displayWarningAlert: function() { var opportunity = this.model.get('opps_view_by') === 'Opportunities'; var message = opportunity ? app.lang.get('LBL_OPPS_CONFIG_ALERT_TO_OPPS', 'Opportunities') : app.lang.get('LBL_OPPS_CONFIG_ALERT', 'Opportunities'); app.alert.show('forecast-warning', { level: 'confirmation', title: app.lang.get('LBL_WARNING'), messages: message, onConfirm: _.bind(function() { this._super('saveConfig'); }, this), onCancel: _.bind(function() { this.model.set('opps_view_by', this.currentOppsViewBySetting); }, this) }); }, /** * Overriding the default saveConfig to display the warning alert first, then on confirm of the * warning alert, save the config settings. Reloads metadata. * * @inheritdoc */ saveConfig: function() { if (this.isForecastsSetup && this.currentOppsViewBySetting !== this.model.get('opps_view_by')) { this.displayWarningAlert(); } else { this._super('saveConfig'); } } }) }, "filter-rows": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-rows View (base) extendsFrom: 'FilterRowsView', /** * @inheritdoc */ loadFilterFields: function(moduleName) { this._super('loadFilterFields', [moduleName]); var cfg = app.metadata.getModule("Forecasts", "config"); if (cfg && cfg.is_setup === 1) { _.each(this.filterFields, function(field, key, list) { if (key.indexOf('_case') != -1) { var fieldName = 'show_worksheet_' + key.replace('_case', ''); if (cfg[fieldName] !== 1) { delete list[key]; delete this.fieldList[key]; } } }, this); } else { delete this.fieldList['commit_stage']; delete this.filterFields['commit_stage']; } } }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.RevenueLineItems.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseRevenueLineItemsActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) /** * @inheritdoc */ formatDate: function(date) { return date.formatUser(true); }, }) }, "config-opps-view-by": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesConfigOppsViewByView * @alias SUGAR.App.view.views.BaseOpportunitiesConfigOppsViewByView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-opps-view-by View (base) extendsFrom: 'ConfigPanelView', /** * The current opps_view_by config setting when the view is initialized */ currentOppsViewBySetting: undefined, /** * Are we currently waiting for the field items? */ waitingForFieldItems: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // get the initial opps_view_by setting this.currentOppsViewBySetting = this.model.get('opps_view_by'); }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:opps_view_by', function() { this.showRollupOptions(); }, this); }, /** * Displays the Latest/Earliest Date toggle */ showRollupOptions: function() { if (this.currentOppsViewBySetting === 'RevenueLineItems' && this.model.get('opps_view_by') === 'Opportunities') { this.getField('opps_closedate_rollup').show(); this.$('[for=opps_closedate_rollup]').show(); this.$('#sales-stage-text').show(); // if there's no value here yet, set to latest if (!this.model.has('opps_closedate_rollup')) { this.$('input[value="latest"]').prop('checked', true); } } else { this.getField('opps_closedate_rollup').hide(); this.$('[for=opps_closedate_rollup]').hide(); this.$('#sales-stage-text').hide(); } // update the title based on settings this.updateTitle(); }, /** * @inheritdoc */ _render: function(options) { this._super('_render', [options]); this.showRollupOptions(); }, /** * @inheritdoc */ _updateTitleValues: function() { var items = this._getFieldOptions(); if (items) { // defensive coding in case user removed this options dom var title = ''; if (items && _.isObject(items)) { title = items[this.model.get('opps_view_by')]; } this.titleSelectedValues = title; } }, /** * Get the options from the field, vs form the dom, since it's * customized to show the correct module names by the end point * * @return {boolean|Object} * @private */ _getFieldOptions: function() { var f = this.getField('opps_view_by'); if (_.isUndefined(f.items)) { if (this.waitingForFieldItems === false) { this.waitingForFieldItems = true; f.once('render', function() { this.waitingForFieldItems = false; this.updateTitle(); }, this); } return false; } else { return f.items; } } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Opportunities.CreateView * @alias SUGAR.App.view.views.OpportunitiesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Used by the alert openRLICreate method */ createdModel: undefined, /** * Used by the openRLICreate method */ listContext: undefined, /** * The original success message to call from the new one we set in the getCustomSaveOptions method */ originalSuccess: undefined, /** * Holds a reference to the alert this view triggers */ alert: undefined, /** * What are we viewing by */ viewBy: 'Opportunities', /** * Does the current user has access to RLI's? */ hasRliAccess: true, /** * Flag to store if the user has confirmed the save if there are validation warnings * * @property {boolean} */ hasConfirmedSave: false, /** * If subpanel models are valid */ validSubpanelModels: true, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins, ['LinkedModel']); this.viewBy = app.metadata.getModule('Opportunities', 'config').opps_view_by; this.hasRliAccess = app.acl.hasAccess('edit', 'RevenueLineItems'); this._super('initialize', [options]); app.utils.hideForecastCommitStageField(this.meta.panels); }, /** * @inheritdoc */ initiateSave: function(callback) { this.disableButtons(); async.waterfall([ _.bind(function(cb) { async.parallel([ _.bind(this.validateSubpanelModelsWaterfall, this), _.bind(this.validateModelWaterfall, this) ], function(err) { // err is undefined or null if no errors cb(!(_.isUndefined(err) || _.isNull(err))); }); }, this), _.bind(this.dupeCheckWaterfall, this), _.bind(this.createRecordWaterfall, this) ], _.bind(function(error) { this.enableButtons(); if (error && error.status == 412 && !error.request.metadataRetry) { this.handleMetadataSyncError(error); } else if (!error && !this.disposed) { this.context.lastSaveAction = null; callback(); } }, this)); }, /** * Check to see if all fields are valid * * @inheritdoc */ validateModelWaterfall: function(callback) { // override this.model.doValidate() to display error if subpanel model validation failed this.model.trigger('validation:start'); this.model.isValidAsync(this.getFields(this.module), _.bind(function(isValid, errors) { this._handleValidationComplete(isValid, errors, callback); }, this)); }, /** * Check to see if there are subpanel create models on this view * And trigger an event to tell the subpanel to validate itself * * @inheritdoc */ validateSubpanelModelsWaterfall: function(callback) { this.hasSubpanelModels = false; this.validSubpanelModels = true; _.each(this.context.children, function(child) { if (child.get('isCreateSubpanel')) { this.hasSubpanelModels = true; this.context.trigger('subpanel:validateCollection:' + child.get('link'), _.bind(function(notValid) { if (this.validSubpanelModels && notValid) { this.validSubpanelModels = false; } callback(notValid); }, this), true ); } }, this); // If there are no subpanel models, callback false so the waterfall can continue if (!this.hasSubpanelModels) { return callback(false); } }, /** * When validation is complete, see if there are any cascade warnings to show * @param isValid * @param errors * @param callback * @private */ _handleValidationComplete: function(isValid, errors, callback) { let hasRlis = this.viewBy === 'RevenueLineItems' && this.hasRliAccess; let cascadeWarning = this.validateCascadeFields(); if (this.validSubpanelModels && isValid && hasRlis && cascadeWarning && (!this._isOnLeadConvert() || !this.hasConfirmedSave) ) { app.alert.show('delete_recurrence_confirmation', { title: app.lang.get('LBL_WARNING'), level: 'confirmation', messages: cascadeWarning, onConfirm: () => { this.hasConfirmedSave = true; this.model.trigger('validation:success'); this.model.trigger('validation:complete', this.model._processValidationErrors(errors)); callback(!isValid); }, onCancel: () => { this.enableButtons(); } }); } else { if (this.validSubpanelModels && isValid) { this.model.trigger('validation:success'); } else if (!this.validSubpanelModels) { this.model.trigger('error:validation'); } this.model.trigger('validation:complete', this.model._processValidationErrors(errors)); callback(!isValid); } }, /** * Checks if the Opp create view is on the leads convert layout * @return {boolean} * @private */ _isOnLeadConvert: function() { return this.context && this.context.parent && this.context.parent.get('convertModuleList'); }, /** * Gets the RLIs under this Opp * @return {*} * @private */ _getRliCollection: function() { let rliContext = this.context.getChildContext({link: 'revenuelineitems'}); rliContext.prepare(); return rliContext.get('collection'); }, /** * Returns true if every RLI is not marked as a service * @param rliCollection * @return {boolean} * @private */ _checkForNonServiceRlis: function(rliCollection) { return rliCollection.models.every(model => !app.utils.isTruthy(model.get('service'))); }, /** * Returns true if every RLI has an uneditable duration * @param rliCollection * @return {boolean} * @private */ _checkForLockedDurationServiceRlis: function(rliCollection) { let serviceRlis = rliCollection.models.filter(model => app.utils.isTruthy(model.get('service'))); if (serviceRlis.length === 0) { return false; } return serviceRlis.every(model => { return !_.isEmpty(model.get('add_on_to_id')) || app.utils.isTruthy(model.get('lock_duration')); }); }, /** * Checks if there are any warnings to show for the cascade fields. Returns the message if a warning exists, * or null otherwise. * @return {null|string} */ validateCascadeFields: function() { if (this.viewBy !== 'RevenueLineItems' || !this.hasRliAccess) { return null; } let rliCollection = this._getRliCollection(); if (_.isEmpty(rliCollection)) { return null; } let durationFields = ['service_duration_value', 'service_duration_unit']; let durationFieldsEmpty = durationFields.every(field => !this.model.get(field)); let startDateEmpty = _.isEmpty(this.model.get('service_start_date')); let durationChecked = app.utils.isTruthy(this.model.get('service_duration_cascade_checked')); let startDateChecked = app.utils.isTruthy(this.model.get('service_start_date_cascade_checked')); if (this._checkForNonServiceRlis(rliCollection)) { let fieldsWithErrors = []; if (!durationFieldsEmpty && durationChecked) { fieldsWithErrors.push('LBL_SERVICE_DURATION'); } if (!startDateEmpty && startDateChecked) { fieldsWithErrors.push('LBL_SERVICE_START_DATE'); } if (!_.isEmpty(fieldsWithErrors)) { return this._buildCascadeWarning(fieldsWithErrors, 'LBL_CASCADE_SERVICE_WARNING'); } } if (this._checkForLockedDurationServiceRlis(rliCollection) && !durationFieldsEmpty && durationChecked) { return this._buildCascadeWarning(['LBL_SERVICE_DURATION'], 'LBL_CASCADE_DURATION_WARNING'); } return null; }, /** * Builds the cascade field warnings from the provided field labels * @param fieldLabels * @param baseLabel * @return {string} * @private */ _buildCascadeWarning: function(fieldLabels, baseLabel) { let translatedFieldLabels = fieldLabels.map(fieldLabel => app.lang.get(fieldLabel, 'Opportunities')); let andLabel = app.lang.get('LBL_AND').toLowerCase().trim(); return translatedFieldLabels.join(` ${andLabel} `) + app.lang.get(baseLabel, 'Opportunities'); }, /** * @inheritdoc */ resetDefaults: function() { this._super('resetDefaults'); if (this.viewBy === 'RevenueLineItems' && this.hasRliAccess) { // now lets check for RLI's let rliContext = this.context.getChildContext({link: 'revenuelineitems'}); rliContext.prepare(); // there should be only one record in the related context collection // when sugarlogic is initialized on create view if (rliContext.get('collection').length === 1) { let model = rliContext.get('collection').at(0); model.setDefault(model.attributes); } } }, /** * Custom logic to make sure that none of the rli records have changed * * @inheritdoc */ hasUnsavedChanges: function() { var ret = this._super('hasUnsavedChanges'); if (this.viewBy === 'RevenueLineItems' && this.hasRliAccess && ret === false) { // now lets check for RLI's var rli_context = this.context.getChildContext({link: 'revenuelineitems'}); rli_context.prepare(); // if there is more than one record in the related context collection, then return true if (rli_context.get('collection').length > 1) { ret = true; } else if (rli_context.get('collection').length === 0) { // if there is no RLI in the related context collection, then return false ret = false; } else { // if there is only one model, we need to verify that the model is not dirty. // check the non default attributes to make sure they are not empty. var model = rli_context.get('collection').at(0), attr_keys = _.difference(_.keys(model.attributes), ['id']), // if the value is not empty and it doesn't equal the default value // we have a dirty model unsavedRliChanges = _.find(attr_keys, function(attr) { var val = model.get(attr); return (!_.isEmpty(val) && (model._defaults[attr] !== val)); }); ret = (!_.isUndefined(unsavedRliChanges)); } } return ret; }, /** * @inheritdoc */ getCustomSaveOptions: function(options) { if (this.viewBy === 'RevenueLineItems') { this.createdModel = this.model; // since we are in a drawer this.listContext = this.context.parent || this.context; this.originalSuccess = options.success; if (app.metadata.getModule(this.module).isTBAEnabled === true) { // make sure new RLIs inherit opportunity's teamset and selected teams var addedRLIs = this.createdModel.get('revenuelineitems') || false; if (addedRLIs && addedRLIs.create && addedRLIs.create.length) { _.each(addedRLIs.create, function (data) { data.team_name = this.createdModel.get('team_name'); }, this); } } var success = _.bind(function(model) { this.originalSuccess(model); this._checkForRevenueLineItems(model, options); }, this); return { success: success }; } }, /** * Check for Revenue Line Items, if the user has edit access and non exist, then * display the RLI Warning Message. * * @param {{Data.Bean}} model * @param {{object}} options * @private */ _checkForRevenueLineItems: function(model, options) { // lets make sure we have edit/create access to RLI's // if we do, lets make sure that the values where added if (this.hasRliAccess) { // check to see if we added RLIs during create var addedRLIs = model.get('revenuelineitems') || false; addedRLIs = (addedRLIs && addedRLIs.create && addedRLIs.create.length); if (!addedRLIs) { this.showRLIWarningMessage(this.listContext.get('module')); } } }, /** * Display the warning message about missing RLIs */ showRLIWarningMessage: function() { // add a callback to close the alert if users navigate from the page app.routing.before('route', this.dismissAlert, this); var message = app.lang.get('TPL_RLI_CREATE', 'Opportunities') + ' <a href="javascript:void(0);" id="createRLI">' + app.lang.get('TPL_RLI_CREATE_LINK_TEXT', 'Opportunities') + '</a>'; this.alert = app.alert.show('opp-rli-create', { level: 'warning', autoClose: false, title: app.lang.get('LBL_ALERT_TITLE_WARNING') + ':', messages: message, onLinkClick: _.bind(function() { app.alert.dismiss('create-success'); this.openRLICreate(); }, this), onClose: _.bind(function() { app.routing.offBefore('route', this.dismissAlert, this); }, this) }); }, /** * Handle dismissing the RLI create alert */ dismissAlert: function(data) { // if we are not navigating to the Opps list view, dismiss the alert if (data && !(data.args && data.args[0] === 'Opportunities' && data.route === 'list')) { app.alert.dismiss('opp-rli-create'); // close RLI warning alert // remove before route event listener app.routing.offBefore('route', this.dismissAlert, this); } }, /** * Open a new Drawer with the RLI Create Form */ openRLICreate: function() { // close RLI warning alert this.dismissAlert(true); var model = this.createLinkModel(this.createdModel || this.model, 'revenuelineitems'); app.drawer.open({ layout: 'create', context: { create: true, module: model.module, model: model } }, _.bind(function(model) { if (!model) { return; } var ctx = this.listContext || this.context; ctx.reloadData({recursive: false}); // reload opportunities and RLIs subpanels ctx.trigger('subpanel:reload', {links: ['opportunities', 'revenuelineitems']}); }, this)); }, /** * @inheritdoc */ _dispose: function() { if (this.alert) { this.alert.getCloseSelector().off('click'); } this._super('_dispose', []); } }) }, "product-quick-picks-dashlet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesProductQuickPicksDashletView * @alias SUGAR.App.view.views.BaseOpportunitiesProductQuickPicksDashletView * @extends View.Views.Base.ProductQuickPicksDashletView * @deprecated Use {@link View.Views.Base.ProductQuickPicksDashletView} instead */ ({ // Product-quick-picks-dashlet View (base) extendsFrom: 'ProductQuickPicksDashletView', initialize: function(options) { app.logger.warn('View.Views.Base.Opportunities.ProductQuickPicksDashletView is deprecated. Use ' + 'View.Views.Base.ProductQuickPicksDashletView instead'); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "multi-line-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Opportunities.MultiLineListView * @alias SUGAR.App.view.views.BaseOpportunitiesMultiLineListView * @extends View.Views.Base.MultiLineListView */ ({ // Multi-line-list View (base) /** * Opportunities sales_status can be customized to included multiple values * @override */ setFilterDef: function(options) { var meta = options.meta || {}; // if filterDef exists in meta if (meta.filterDef) { // perform actions as per the parent class method this._super('setFilterDef', [options]); return; } var closedWon = ['Closed Won']; var closedLost = ['Closed Lost']; var forecastCfg = app.metadata.getModule('Forecasts', 'config'); if (forecastCfg && forecastCfg.is_setup) { closedWon = forecastCfg.sales_stage_won; closedLost = forecastCfg.sales_stage_lost; } var notIn = _.union(closedWon, closedLost); var filterDef = [ { sales_status: { $not_in: notIn, }, $owner: '', }, ]; options.context.get('collection').filterDef = filterDef; options.context.get('collection').defaultFilterDef = filterDef; } }) }, "panel-top": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Opportunities.PanelTopField * @alias App.view.fields.BaseOpportunitiesPanelTopField * @extends View.Fields.Base.PanelTopField */ ({ // Panel-top View (base) extendsFrom: "PanelTopView", /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if (['Accounts', 'Documents'].includes(this.parentModule)) { this.on('linked-model:create', this._reloadRevenueLineItems, this); } }, /** * Refreshes the RevenueLineItems subpanel when a new Opportunity is added * @private */ _reloadRevenueLineItems: function() { if (app.metadata.getModule('Opportunities', 'config').opps_view_by == 'RevenueLineItems') { var $rliSubpanel = $('div[data-subpanel-link="revenuelineitems"]'); // only reload RLI subpanel if it is opened if (!$('li.subpanel', $rliSubpanel).hasClass('closed')) { this.context.parent.trigger('subpanel:reload', {links: ['revenuelineitems']}); } else { // RLI Panel is closed, filter components to find the RLI panel and update count var rliComponent = _.find(this.layout.layout._components, function(component) { return component.module === 'RevenueLineItems'; }); var cc_field = rliComponent.getComponent('panel-top').getField('collection-count'); app.api.count(this.parentModule, { id: this.context.parent.get('modelId'), link:'revenuelineitems' }, { success: function(data) { cc_field.updateCount({ length: data.record_count }); } }); } } } }) }, "merge-duplicates": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Merge-duplicates View (base) extendsFrom: 'MergeDuplicatesView', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); var config = app.metadata.getModule('Forecasts', 'config'); if (config && config.is_setup && config.forecast_by === 'Opportunities' && app.metadata.getServerInfo().flavor !== 'PRO') { // make sure forecasts exists and is setup this.collection.on('change:sales_stage change:commit_stage reset', function(model) { var myModel = model; //check to see if this is a collection (for the reset event), use this.primaryRecord instead if true; if (!_.isUndefined(model.models)) { myModel = this.primaryRecord; } var salesStage = myModel.get('sales_stage'), commitStage = this.getField('commit_stage'); if (salesStage && commitStage) { if(_.contains(config.sales_stage_won, salesStage)) { // check if the sales_stage has changed to a Closed Won stage if(config.commit_stages_included.length) { // set the commit_stage to the first included stage myModel.set('commit_stage', _.first(config.commit_stages_included)); } else { // otherwise set the commit stage to just "include" myModel.set('commit_stage', 'include'); } commitStage.setDisabled(true); this.$('input[data-record-id="' + myModel.get('id') + '"][name="copy_commit_stage"]').prop("checked", true); } else if(_.contains(config.sales_stage_lost, salesStage)) { // check if the sales_stage has changed to a Closed Lost stage // set the commit_stage to exclude myModel.set('commit_stage', 'exclude'); commitStage.setDisabled(true); this.$('input[data-record-id="' + myModel.get('id') + '"][name="copy_commit_stage"]').prop("checked", true); } else { commitStage.setDisabled(false); } } }, this); } } }) } }} , "layouts": { "base": { "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.OpportunitiesConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseOpportunitiesConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'ConfigDrawerLayout', /** * Checks Opportunities ACLs to see if the User is a system admin * or if the user has a developer role for the Opportunities module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().Opportunities, isSysAdmin = (app.user.get('type') == 'admin'), isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isDev); } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.OpportunitiesConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseOpportunitiesConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'ConfigDrawerContentLayout', viewOppsByTitle: undefined, viewOppsByOppsTpl: undefined, viewOppsByRLIsTpl: undefined, /** * @inheritdoc */ _initHowTo: function() { this.viewOppsByTitle = app.lang.get('LBL_OPPS_CONFIG_VIEW_BY_LABEL', 'Opportunities'); var helpUrl = { more_info_url: '<a href="' + app.help.getMoreInfoHelpURL('config', 'OpportunitiesConfig') + '" target="_blank">', more_info_url_close: '</a>' }, viewOppsByOppsObj = app.help.get('Opportunities', 'config_opps', helpUrl), viewOppsByRLIsObj = app.help.get('Opportunities', 'config_rlis', helpUrl); this.viewOppsByOppsTpl = app.template.getLayout(this.name + '.help', this.module)(viewOppsByOppsObj); this.viewOppsByRLIsTpl = app.template.getLayout(this.name + '.help', this.module)(viewOppsByRLIsObj); }, bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:opps_view_by', function(model, oppsViewBy) { this.changeHowToData(this.viewOppsByTitle, this._getText(oppsViewBy)); }, this); }, /** * @inheritdoc */ _switchHowToData: function(helpId) { switch(helpId) { case 'config-opps-view-by': this.currentHowToData.title = this.viewOppsByTitle; this.currentHowToData.text = this._getText(this.model.get('opps_view_by')); } this._super('_switchHowToData'); }, /** * Returns the proper template text depending on the opps_view_by setting being passed in * * @param {String} oppsViewBy The Opps View By setting 'Opportunities' | 'RevenueLineItems' * @returns {String} HTML Template text for the right help text * @private */ _getText: function(oppsViewBy) { return (oppsViewBy === 'Opportunities') ? this.viewOppsByOppsTpl : this.viewOppsByRLIsTpl; } }) } }} , "datas": {} }, "Cases":{"fieldTemplates": { "base": { "case-status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Cases.CaseStatusField * @alias SUGAR.App.view.fields.BaseCasesCaseStatusField * @extends View.Fields.Base.EnumColorcodedField */ ({ // Case-status FieldTemplate (base) extendsFrom: 'EnumColorcodedField', }) } }} , "views": { "base": { "request-closed-cases-dashlet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Cases.RequestClosedCasesDashlet * @alias SUGAR.App.view.views.BaseCasesRequestClosedCasesDashlet * @extends @extends View.Views.Base.ListView */ ({ // Request-closed-cases-dashlet View (base) plugins: ['Dashlet'], extendsFrom: 'ListView', /** * Fields displayed in dashlet * * @property {Array} */ displayedFields: [ 'case_number', 'name', 'priority', 'status', 'date_modified', ], /** * Cases bean collection. * * @property {Data.BeanCollection} */ collection: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initCollection(); }, /** * Initialize feature collection. */ _initCollection: function() { var today = app.date().formatServer(true); var self = this; this.collection = app.data.createBeanCollection(this.module); this.collection.setOption({ fields: this.displayedFields, filter: { 'request_close': { '$equals': 1 }, 'status': { '$not_in': ['Closed'] }, '$owner': '' }, }); this.collection.displayedFields = this._initDisplayedFields(); // set meta last state id so sorting order is maintained this.meta.last_state = {id: 'request-closed-cases-dashlet'}; this.orderByLastStateKey = app.user.lastState.key('order-by', this); this.orderBy = this._initOrderBy(); if (this.collection) { this.collection.orderBy = this.orderBy; } return this; }, /** * Returns the displayed field objects * * @return {Array} the field objects * @private */ _initDisplayedFields: function() { var displayedFields = []; _.each(this.displayedFields, function(field) { if (!this.model.fields) { return; } var toPush = this.model.fields[field]; toPush.link = (field === 'name') ? true : false; displayedFields.push(toPush); }, this); return displayedFields; }, /** * @inheritdoc * * Once collection has been changed, the view should be refreshed. */ bindDataChange: function() { if (this.collection) { this.collection.on('add remove reset', function() { if (this.disposed) { return; } this.render(); }, this); } }, /** * @inheritdoc */ _setOrderBy: function(options) { if (this.orderByLastStateKey) { app.user.lastState.set(this.orderByLastStateKey, this.orderBy); } this.loadData(options); }, /** * @inheritdoc */ loadData: function(options) { this.collection.fetch(options); }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', contactsSubpanel: null, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary', 'KBContent']); this._super('initialize', [options]); this._bindEvents(); }, /** * Initiates listening to application events. */ _bindEvents: function() { this.context.on('context:child:add', this.addChildHandler, this); }, /** * Bind events on Contacts subpanel */ addChildHandler: function(childModel) { if (childModel.get('link') === 'contacts') { this.contactsSubpanel = childModel; this.contactsSubpanel.on('reload', _.bind(function() { this.context.reloadData(); }, this)); } }, /** * @inheritdoc */ _dispose: function() { this.context.off('context:child:add', this.addChildHandler, this); this._super('_dispose'); }, }) }, "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Cases.ActivityCardContentView * @alias SUGAR.App.view.views.BaseCasesActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.initAttachmentDetails('attachment_list'); }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Cases.CreateView * @alias SUGAR.App.view.views.CasesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /* * Enable pre-population from Omnichanel */ omniPopulation: true, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Cases.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseNotesActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "multi-line-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Cases.MultiLineListView * @alias SUGAR.App.view.views.CasesCreateView * @extends View.Views.Base.MultiLineListView */ ({ // Multi-line-list View (base) /** * @inheritdoc */ extendsFrom: 'MultiLineListView', /** * @inheritdoc */ _setConfig: function(options) { this._super('_setConfig', [options]); if (this.metric && ['follow_up_datetime'].includes(this.metric.order_by_primary, this.metric.order_by_secondary)) { options.meta.collectionOptions.params.order_by += ',case_number:asc'; } } }) } }} , "layouts": {} , "datas": {} }, "Notes":{"fieldTemplates": { "base": { "multi-attachments": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Multi Attachment modifications specific to Notes. These * modifications create a pill for files stored directly * on the note record's file fields. * * @class View.Fields.Base.Notes.MultiAttachmentsField * @alias SUGAR.App.view.fields.BaseNotesMultiAttachmentsField * @extends View.Fields.Base.MultiAttachmentsField */ ({ // Multi-attachments FieldTemplate (base) /** * Override multi-attachments field for notes */ extendsFrom: 'BaseMultiAttachmentsField', /** * Name of file field on model */ fileFieldName: 'filename', /** * Name of field holding file mime type */ fileMimeTypeFieldName: 'file_mime_type', /** * If we are displaying only a single image, we show preview for it */ singleImage: false, /** * Mapping of accepted image mimetypes to file extensions */ supportedImageExtensions: { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif' }, /** * Re-render when the file and mimetype fields change */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:' + this.fileFieldName, this.render, this); this.model.on('change:' + this.fileMimeTypeFieldName, this.render, this); }, /** * Override format to add/remove single file pills, and set flag for image * preview * * @param {array} value array of field pills * @returns {array} formatted array of field pills */ format: function(value) { if (!this._pillAdded(value) && this._modelHasFileAttachment()) { var attr = this._getPillFromFile(); if (value instanceof app.BeanCollection) { var model = app.data.createBean('Notes', attr); value.models.push(model); } else { value.push(attr); } } value = this._super('format', [value]); this.singleImage = this._singleImagePill(value); return value; }, /** * Determine if a pill has already been added to this field for an * attachment stored directly on the note. * @param value * @returns {boolean} * @private */ _pillAdded: function(value) { value = value instanceof app.BeanCollection ? value.models : value; return _.reduce(value, function(base, item) { item = item instanceof Backbone.Model ? item.toJSON() : item; return base || item.id === this.model.id; }, false, this); }, /** * Util to see if this model has a file stored directly * @returns {boolean} * @private */ _modelHasFileAttachment: function() { return !!(this.model.get(this.fileFieldName) && this.model.get(this.fileMimeTypeFieldName)); }, /** * Creates a pill in the format needed by select2 for file stored directly * on the note. * @returns {Object} {filename: file name, * id: ID of this note for file link, * file_mime_type: file mime type} * @private */ _getPillFromFile: function() { var attr = {id: this.model.get('id')}; attr[this.fileFieldName] = this.model.get(this.fileFieldName); attr[this.fileMimeTypeFieldName] = this.model.get(this.fileMimeTypeFieldName); return attr; }, /** * Override base field's removeAttachment method to remove file stored * directly on the note if needed * @param event {Object} */ removeAttachment: function(event) { if (event.val === this.model.get('id')) { this._removeLegacyAttachment(); this.pillAdded = false; this.render(); } else { this._super('removeAttachment', [event]); } }, /** * Removes file stored directly on the note * @private */ _removeLegacyAttachment: function() { this.model.set(this.fileFieldName, ''); var value = this.model.get(this.name).models; this.model.get(this.name).models = _.filter(value, function(model) { return model.get('id') !== this.model.get('id'); }, this); }, /** * Check if input mime type is an image or not. * * @param {String} mimeType - file mime type. * @return {Boolean} true if mime type is an image. * @private */ _isImage: function(mimeType) { return !!this.supportedImageExtensions[mimeType]; }, /** * Check if we're rendering a single pill for an image file * @param {array} value - list of pills to display * @returns {boolean} * @private */ _singleImagePill: function(value) { return value && value.length === 1 && value[0].mimeType === 'image'; } }) } }} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.NotesRecordlistView * @alias SUGAR.App.view.views.BaseNotesRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * Get row fields (except 'multi-attachments') of the model * * @param {string} modelId Model Id. * @return {Array} list of fields objects */ getModelRowFields: function(modelId) { var fields = _.filter(this.rowFields[modelId], _.bind(function(field) { return (field.type !== 'multi-attachments'); }, this)); return fields; }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', duplicateClicked: function() { let self = this; let prefill = app.data.createBean('Notes'); prefill.copy(this.model); this._copyNestedCollections(this.model, prefill); self.model.trigger('duplicate:before', prefill); prefill.unset('id'); prefill.unset('is_escalated'); prefill.attributes.attachment_list.models = []; app.drawer.open({ layout: 'create', context: { create: true, model: prefill, copiedFromModelId: this.model.get('id') } }, function(context, newModel) { if (newModel && newModel.id) { if (self.closestComponent('side-drawer')) { let recordContext = { layout: 'record', dashboardName: newModel.get('name'), context: { layout: 'record', name: 'record-drawer', contentType: 'record', modelId: newModel.id, dataTitle: app.sideDrawer.getDataTitle('Notes', 'LBL_RECORD', newModel.get('name')) } }; app.sideDrawer.open(recordContext, null, true); return; } app.router.navigate('Notes' + '/' + newModel.id, {trigger: true}); } }); prefill.trigger('duplicate:field', self.model); } }) }, "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Notes.ActivityCardContentView * @alias SUGAR.App.view.views.BaseNotesActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.formatDescriptionField(); this.initAttachmentDetails('attachment_list'); }, /** * Formats the description field to account for line breaks */ formatDescriptionField: function() { if (this.activity) { var description = this.activity.get('description'); this.descriptionField = this.formatContent(description); } } }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Notes.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseNotesActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) extendsFrom: 'ActivityCardDetailView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.initPortalFlagDetails(); }, /** * Initializes hbs entry source variable */ initPortalFlagDetails: function() { if (this.activity) { this.portalFlag = this.activity.get('portal_flag'); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Notes.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseNotesActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'modified_by_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Calls":{"fieldTemplates": { "base": { "label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Calls.LabelField * @alias SUGAR.App.view.fields.BaseCallsLabelField * @extends View.Fields.Base.LabelField */ ({ // Label FieldTemplate (base) /** * @inheritdoc * * Returns the `detail` template for this type of field */ _getFallbackTemplate: function(viewName) { return 'detail'; }, }) }, "call-recording": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Calls.CallRecordingField * @alias SUGAR.App.view.fields.BaseCallsCallRecordingField * @extends View.Fields.Base.BaseField */ ({ // Call-recording FieldTemplate (base) /** * The call recording URL */ recordingUrl: '', /** * The friendly display name */ recordingName: '', /** * @inheritdoc */ _render: function() { this.setRecordingUrl(); this._super('_render'); }, /** * Set the call recording URL and the friendly display name */ setRecordingUrl: function() { this.recordingUrl = this.model.get('call_recording_url'); if (this.recordingUrl) { this.recordingName = app.date(this.model.get('date_entered')).formatUser(); } } }) }, "record-decor": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Calls.RecordDecorFields * @alias SUGAR.App.view.fields.BaseCallsRecordDecorField * @extends View.Fields.Base.RecordDecorField */ ({ // Record-decor FieldTemplate (base) extendsFrom: 'RecordDecorField', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['RecurringEvents']); this._super('initialize', [options]); } }) }, "textarea": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Calls.TextareaField * @alias SUGAR.App.view.fields.BaseCallsTextareaField * @extends View.Fields.Base.TextareaField */ ({ // Textarea FieldTemplate (base) extendsFrom: 'textarea', transcriptFields: [ 'transcript', ], /** * Add empty-transcript class to transcript fields if the field is empty. * * @param {string} value - field contents * @return {string} value - formatted field contents */ format: function(value) { if (_.contains(this.transcriptFields, this.name)) { this.$el.toggleClass('empty-transcript', !value); } return this._super('format', [value]); } }) } }} , "views": { "base": { "create-no-cancel-button": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.CreateNoCancelButtonView * @alias SUGAR.App.view.views.CallsCreateNoCancelButtonView * @extends View.Views.Base.CreateNoCancelButtonView */ ({ // Create-no-cancel-button View (base) extendsFrom: 'CreateNoCancelButtonView', /** * Additional plugins for this module */ additionalPlugins: [ 'AddAsInvitee', 'ReminderTimeDefaults', ], /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], this.additionalPlugins); this._super('initialize', [options]); } }) }, "dashablerecord": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.DashablerecordView * @alias SUGAR.App.view.views.CallsDashablerecordView * @extends View.Views.Base.DashablerecordView */ ({ // Dashablerecord View (base) extendsFrom: 'DashablerecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['RecurringEvents']); this._super('initialize', [options]); }, }) }, "resolve-conflicts-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.ResolveConflictsListView * @alias SUGAR.App.view.views.BaseCallsResolveConflictsListView * @extends View.Views.Base.ResolveConflictsListView */ ({ // Resolve-conflicts-list View (base) extendsFrom: 'ResolveConflictsListView', /** * @inheritdoc * * The invitees field should not be displayed on list views. It is removed * before comparing models so that it doesn't get included. */ _buildFieldDefinitions: function(modelToSave, modelInDb) { modelToSave.unset('invitees'); this._super('_buildFieldDefinitions', [modelToSave, modelInDb]); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['EditAllRecurrences', 'AddAsInvitee', 'RecurringEvents']); this._super('initialize', [options]); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.CreateView * @alias SUGAR.App.view.views.CallsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee', 'ReminderTimeDefaults']); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseCallsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setUsersFields(); }, /** * @inheritdoc * * Do not set user fields as that will be set after activity fetch */ setUsersPanel: function() { this.setUsersTemplate(); }, /** * @inheritdoc */ setUsersFields: function() { this.setInvitees(); }, }) }, "create-nodupecheck": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calls.CreateNodupecheckView * @alias SUGAR.App.view.views.CallsCreateNodupecheckView * @extends View.Views.Base.CreateNodupecheckView */ ({ // Create-nodupecheck View (base) extendsFrom: 'CreateNodupecheckView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee', 'ReminderTimeDefaults']); this._super('initialize', [options]); } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.CallsModel * @alias SUGAR.App.model.datas.BaseCallsModel * @extends Model.Bean */ ({ // Model Data (base) plugins: ['VirtualCollection'] }) } }} }, "Emails":{"fieldTemplates": { "base": { "sender": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.SenderField * @alias SUGAR.App.view.fields.BaseEmailsSenderField * @extends View.Fields.Base.BaseField * @deprecated Use {@link View.Fields.Base.Emails.OutboundEmailField} instead. */ ({ // Sender FieldTemplate (base) fieldTag: 'input.select2', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Fields.Base.Emails.SenderField is deprecated. Use ' + 'View.Fields.Base.Emails.OutboundEmailField instead.'); this._super('initialize', [options]); this.endpoint = this.def.endpoint; }, _render: function() { var result = app.view.Field.prototype._render.call(this); if (this.tplName === 'edit') { var action = (this.endpoint.action) ? this.endpoint.action : null, attributes = (this.endpoint.attributes) ? this.endpoint.attributes : null, params = (this.endpoint.params) ? this.endpoint.params : null, myURL = app.api.buildURL(this.endpoint.module, action, attributes, params); app.api.call('GET', myURL, null, { success: _.bind(this.populateValues, this), error: function(error) { // display error if not a metadata refresh if (error.status !== 412) { app.alert.show('server-error', { level: 'error', messages: 'ERR_GENERIC_SERVER_ERROR' }); } app.error.handleHttpError(error); } }); } return result; }, populateValues: function(results) { var self = this, defaultResult, defaultValue = {}; if (this.disposed === true) { return; //if field is already disposed, bail out } if (!_.isEmpty(results)) { defaultResult = _.find(results, function(result) { return result.default; }); defaultValue = (defaultResult) ? defaultResult : results[0]; if (!this.model.has(this.name)) { this.model.set(this.name, defaultValue.id); this.model.setDefault(this.name, defaultValue.id); } } var format = function(item) { return item.display; }; this.$(this.fieldTag).select2({ data:{ results: results, text: 'display' }, formatSelection: format, formatResult: format, width: '100%', placeholder: app.lang.get('LBL_SELECT_FROM_SENDER', this.module), initSelection: function(el, callback) { if (!_.isEmpty(defaultValue)) { callback(defaultValue); } } }).on("change", function(e) { if (self.model.get(self.name) !== e.val) { self.model.set(self.name, e.val, {silent: true}); } }); }, /** * @inheritdoc * * We need this empty so it won't affect refresh the select2 plugin */ bindDomChange: function() { } }) }, "attachment-button": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Attachment button is a label that is styled like a button and will trigger a * given file input field. * * @class View.Fields.Base.Emails.AttachmentButtonField * @alias SUGAR.App.view.fields.BaseEmailsAttachmentButtonField * @extends View.Fields.Base.ButtonField * @deprecated Use {@link View.Fields.Base.Emails.EmailAttachmentsField} * instead. */ ({ // Attachment-button FieldTemplate (base) extendsFrom: 'ButtonField', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Fields.Base.Emails.AttachmentButtonField is deprecated. Use ' + 'View.Fields.Base.Emails.EmailAttachmentsField instead.'); this._super('initialize',[options]); this.fileInputId = this.context.get('attachment_field_email_attachment'); } }) }, "from": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.FromField * @alias SUGAR.App.view.fields.BaseEmailsFromField * @extends View.Fields.Base.BaseField */ ({ // From FieldTemplate (base) /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, /** * The selector for accessing the Select2 field when in edit mode. The * Select2 field is where the sender is displayed. * * @property {string} */ fieldTag: 'input.select2', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['EmailParticipants', 'ListEditable']); this._super('initialize', [options]); // Specify the error label for when the sender's email address is // invalid. app.error.errorName2Keys[this.type] = app.lang.get('ERR_INVALID_SENDER', this.module); }, /** * @inheritdoc */ bindDataChange: function() { if (this.model) { // Avoids a full re-rendering when editing. The current value of // the field is formatted and passed directly to Select2 when in // edit mode. this.listenTo(this.model, 'change:' + this.name, _.bind(function() { var $el = this.$(this.fieldTag); if (_.isEmpty($el.data('select2'))) { this.render(); } else { $el.select2('data', this.getFormattedValue()); } }, this)); } }, /** * @inheritdoc */ bindDomChange: function() { var $el = this.$(this.fieldTag); $el.on('select2-selecting', _.bind(function(event) { if (this.disposed) { event.preventDefault(); } }, this)); $el.on('change', _.bind(function(event) { var collection; if (this.model && !this.disposed) { collection = this.model.get(this.name); if (!_.isEmpty(event.added)) { // Replace the current model in the collection, as there // can only be one. collection.remove(collection.models); collection.add(event.added); } if (!_.isEmpty(event.removed)) { collection.remove(event.removed); } } }, this)); }, /** * @inheritdoc * * Destroys the Select2 element. */ unbindDom: function() { this.$(this.fieldTag).select2('destroy'); this._super('unbindDom'); }, /** * @inheritdoc */ _render: function() { var $el; var options; this._super('_render'); $el = this.$(this.fieldTag); if ($el.length > 0) { options = this.getSelect2Options(); options = _.extend(options, { allowClear: !this.def.required, multiple: false, /** * Constructs a representation for a selected sender to be * displayed in the field. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Data.Bean} sender * @return {string} * @private */ formatSelection: _.bind(function(sender) { var template = app.template.getField(this.type, 'select2-selection', this.module); return sender ? template({value: sender.toHeaderString({quote_name: true})}) : ''; }, this), /** * Constructs a representation for the sender to be displayed * in the dropdown options after a query. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Data.Bean} sender * @return {string} */ formatResult: _.bind(function(sender) { var template = app.template.getField(this.type, 'select2-result', this.module); return template({ value: sender.toHeaderString({quote_name: true}), module: sender.get('parent_type') }); }, this), /** * Don't escape a choice's markup since we built the HTML. * * See [Select2 Documentation](https://select2.github.io/select2/#documentation). * * @param {string} markup * @return {string} */ escapeMarkup: function(markup) { return markup; } }); $el.select2(options).select2('val', []); if (this.isDisabled()) { $el.select2('disable'); } } }, /** * @inheritdoc * @return {Data.Bean} */ format: function(value) { // Reset the tooltip. this.tooltip = ''; if (value instanceof app.BeanCollection) { value = value.first(); if (value) { value = this.prepareModel(value); } if (value) { this.tooltip = value.toHeaderString(); } } return value; } }) }, "forward-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Forward action. * * This allows a user to "forward" an existing email. * * @class View.Fields.Base.Emails.ForwardActionField * @alias SUGAR.App.view.fields.EmailsBaseForwardActionField * @extends View.Fields.Base.EmailactionField */ ({ // Forward-action FieldTemplate (base) extendsFrom: 'EmailactionField', /** * Template for forward header. * * @protected */ _tplHeaderHtml: null, /** * The name of the template for forward header. * * @protected */ _tplHeaderHtmlName: 'forward-header-html', /** * The prefix to apply to the subject. * * @protected */ _subjectPrefix: 'LBL_FW', /** * The element ID to use to identify the forward content. * * The ID is added to the div wrapper around the content for later * identifying the portion of the email body which is the forward content * (e.g., when inserting templates into an email, but maintaining the * forward content). * * @protected */ _contentId: 'forwardcontent', /** * @inheritdoc * * The forward content is built ahead of the button click to support the * option of doing a mailto link which needs to be built and set in the DOM * at render time. */ initialize: function(options) { this._super('initialize', [options]); // Use field templates from emailaction. this.type = 'emailaction'; this.addEmailOptions({ // If there is a default signature in email compose, it should be // placed above the forward content in the email body. signature_location: 'above', // Focus the editor and place the cursor at the beginning of all // content. cursor_location: 'above', // Prevent prepopulating the email with case data. skip_prepopulate_with_case: true }); }, /** * Returns the subject to use in the email. * * Any instances of "Re: ", "FW: ", and "FWD: " (case-insensitive) found at * the beginning of the subject are removed prior to applying the prefix. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when constructing the subject. * @return {undefined|string} */ emailOptionSubject: function(model) { var pattern = /^((?:re|fw|fwd): *)*/i; var subject = model.get('name') || ''; return app.lang.get(this._subjectPrefix, model.module) + ': ' + subject.replace(pattern, ''); }, /** * Returns the plain-text body to use in the email. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when constructing the body. * @return {undefined|string} */ emailOptionDescription: function(model) { var headerParams; var header; var body; var description; if (!this.useSugarEmailClient()) { headerParams = this._getHeaderParams(model); header = this._getHeader(headerParams); body = model.get('description') || ''; description = '\n' + header + '\n' + body; } return description; }, /** * Returns the HTML body to use in the email. * * Ensure the result is a defined string and strip any signature wrapper * tags to ensure it doesn't get stripped if we insert a signature above * the forward content. Also strip any reply content class if this is a * forward to a previous reply. And strip any forward content class if this * is a forward to a previous forward. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when constructing the body. * @return {undefined|string} */ emailOptionDescriptionHtml: function(model) { var tplHeaderHtml = this._getHeaderHtmlTemplate(); var headerParams = this._getHeaderParams(model); var headerHtml = tplHeaderHtml(headerParams); var body = model.get('description_html') || ''; body = body.replace('<div class="signature">', '<div>'); body = body.replace('<div id="replycontent">', '<div>'); body = body.replace('<div id="forwardcontent">', '<div>'); return '<div></div><div id="' + this._contentId + '">' + headerHtml + body + '</div>'; }, /** * Returns the attachments to use in the email. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when building the attachments. * @return {undefined|Array} */ emailOptionAttachments: function(model) { return model.get('attachments_collection').map(function(attachment) { var filename = attachment.get('filename') || attachment.get('name'); return { _link: 'attachments', upload_id: attachment.get('upload_id') || attachment.get('id'), name: filename, filename: filename, file_mime_type: attachment.get('file_mime_type'), file_size: attachment.get('file_size'), file_ext: attachment.get('file_ext') }; }); }, /** * Returns the bean to use as the email's related record. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model This model's parent is used as the email's * related record. * @return {undefined|Data.Bean} */ emailOptionRelated: function(model) { if (model.link && model.link.type === 'card-link' && model.link.bean) { return model.link.bean; } var parent; if (model.get('parent') && model.get('parent').type && model.get('parent').id) { // We omit type because it is actually the module name and should // not be treated as an attribute. parent = app.data.createBean(model.get('parent').type, _.omit(model.get('parent'), 'type')); } else if (model.get('parent_type') && model.get('parent_id')) { parent = app.data.createBean(model.get('parent_type'), { id: model.get('parent_id'), name: model.get('parent_name') }); } return parent; }, /** * Returns the teamset array to seed the email's teams. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model This model's teams is used as the email's * teams. * @return {undefined|Array} */ emailOptionTeams: function(model) { return model.get('team_name'); }, /** * Build the header for text only emails. * * @param {Object} params * @param {string} params.from * @param {string} [params.date] Date original email was sent * @param {string} params.to * @param {string} [params.cc] * @param {string} params.name The subject of the original email. * @return {string} * @private */ _getHeader: function(params) { var header = '-----\n' + app.lang.get('LBL_FROM', params.module) + ': ' + (params.from || '') + '\n'; var date; if (params.date) { date = app.date(params.date).formatUser(); header += app.lang.get('LBL_DATE', params.module) + ': ' + date + '\n'; } header += app.lang.get('LBL_TO_ADDRS', params.module) + ': ' + (params.to || '') + '\n'; if (params.cc) { header += app.lang.get('LBL_CC', params.module) + ': ' + params.cc + '\n'; } header += app.lang.get('LBL_SUBJECT', params.module) + ': ' + (params.name || '') + '\n'; return header; }, /** * Returns the template for producing the header HTML for the top of the * forward content. * * @return {Function} * @private */ _getHeaderHtmlTemplate: function() { // Use `this.def.type` because `this.type` was changed to `emailaction` // during initialization. this._tplHeaderHtml = this._tplHeaderHtml || app.template.getField(this.def.type, this._tplHeaderHtmlName, this.module); return this._tplHeaderHtml; }, /** * Get the data required by the header template. * * @param {Data.Bean} model The params come from this model's attributes. * EmailClientLaunch plugin should dictate the model based on the context. * @return {Object} * @protected */ _getHeaderParams: function(model) { return { module: model.module, from: this._formatEmailList(model.get('from_collection')), date: model.get('date_sent'), to: this._formatEmailList(model.get('to_collection')), cc: this._formatEmailList(model.get('cc_collection')), name: model.get('name') }; }, /** * Given a list of people, format a text only list for use in a forward * header. * * @param {Data.BeanCollection} collection A list of models * @protected */ _formatEmailList: function(collection) { return collection.map(function(model) { return model.toHeaderString(); }).join(', '); } }) }, "recipients-fieldset": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Recipients field group for handling expand to edit * * @class View.Fields.Base.Emails.RecipientsFieldsetField * @alias SUGAR.App.view.fields.BaseEmailsRecipientsFieldsetField * @extends View.Fields.Base.FieldsetField */ ({ // Recipients-fieldset FieldTemplate (base) extendsFrom: 'FieldsetField', /** * @inheritdoc */ initialize: function(options) { this.events = _.extend({}, this.events, { 'click [data-toggle-field]': '_handleToggleButtonClick' }); this._super('initialize', [options]); }, /** * Adds the CC and BCC toggle buttons to the From field and sets the * visibility of those fields. Switches the field to edit mode when there * are no recipients and the user is creating an email. * * @inheritdoc */ _render: function() { var cc = this.model.get('cc_collection'); var bcc = this.model.get('bcc_collection'); this._super('_render'); this._addToggleButtons('outbound_email_id'); this._toggleFieldVisibility('cc_collection', !!cc.length); this._toggleFieldVisibility('bcc_collection', !!bcc.length); }, /** * @inheritdoc * @example * // Only the To field has recipients. * a@b.com, b@c.com * @example * // All fields have recipients. * a@b.com; CC: c@d.com; BCC: e@f.com * @example * // CC does not have recipients. * a@b.com; BCC: e@f.com * @example * Only the CC field has recipients. * CC: c@d.com */ format: function(value) { return _.chain(this.fields) // The from field is not used for calculating the value. .where({type: 'email-recipients'}) // Construct each field's string from it's formatted value. .reduce(function(fields, field) { var models = field.getFormattedValue(); var str = _.map(models, function(model) { var name = model.get('parent_name') || ''; var email = model.get('email_address') || ''; // The name was erased, so let's use the label. if (_.isEmpty(name) && model.isNameErased()) { name = app.lang.get('LBL_VALUE_ERASED', model.module); } if (!_.isEmpty(name)) { return name; } // The email was erased, so let's use the label. if (_.isEmpty(email) && model.isEmailErased()) { email = app.lang.get('LBL_VALUE_ERASED', model.module); } return email; }).join(', '); if (!_.isEmpty(str)) { fields[field.name] = str; } return fields; }, {}) // Add the label for each field's string. .map(function(field, fieldName) { var label = ''; if (fieldName === 'cc_collection') { label = app.lang.get('LBL_CC', this.module) + ': '; } else if (fieldName === 'bcc_collection') { label = app.lang.get('LBL_BCC', this.module) + ': '; } return label + field; }, this) .value() // Separate each field's string by a semi-colon. .join('; '); }, /** * Add CC and BCC toggle buttons to the field. * * @param {string} fieldName The name of the field where the buttons are * added. * @private */ _addToggleButtons: function(fieldName) { var field = this.view.getField(fieldName); var $field; var template; var html; if (!field) { return; } $field = field.$el.closest('.fieldset-field'); if ($field.length > 0) { template = app.template.getField(this.type, 'recipient-options', this.module); html = template({module: this.module}); $(html).appendTo($field); } }, /** * Toggle the visibility of the field associated with the button that was * clicked. * * @param {Event} event * @private */ _handleToggleButtonClick: function(event) { var $toggleButton = $(event.currentTarget); var fieldName = $toggleButton.data('toggle-field'); this._toggleFieldVisibility(fieldName); }, /** * Toggles the visibility of the field and the toggle state of its * associated button. * * @param {string} fieldName The name of the field to toggle. * @param {boolean} [show] True when the button should be inactive and the * field should be shown. The toggle is flipped when undefined. * @private */ _toggleFieldVisibility: function(fieldName, show) { var toggleButtonSelector = '[data-toggle-field="' + fieldName + '"]'; var $toggleButton = this.$(toggleButtonSelector); var field = this.view.getField(fieldName); // if explicit active state not set, toggle to opposite if (_.isUndefined(show)) { show = !$toggleButton.hasClass('active'); } $toggleButton.toggleClass('active', show); if (field) { field.$el.closest('.fieldset-group').toggleClass('hide', !show); } this.view.trigger('email-recipients:toggled'); } }) }, "reply-all-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Reply all action. * * This allows a user to "reply all" to an existing email. * * @class View.Fields.Base.Emails.ReplyAllActionField * @alias SUGAR.App.view.fields.EmailsBaseReplyAllActionField * @extends View.Fields.Base.Emails.ReplyActionField */ ({ // Reply-all-action FieldTemplate (base) extendsFrom: 'EmailsReplyActionField', /** * Returns the recipients to use in the To field of the email. The sender * and the recipients in the To field from the original email are included. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when identifying the recipients. * @return {undefined|Array} */ emailOptionTo: function(model) { var originalTo = model.get('to_collection'); var to = this._super('emailOptionTo', [model]) || []; to = _.union(to, this._createRecipients(originalTo)); return to; }, /** * Returns the recipients to use in the CC field of the email. These * recipients are the same ones who appeared in the original email's CC * field. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when identifying the recipients. * @return {undefined|Array} */ emailOptionCc: function(model) { var originalCc = model.get('cc_collection'); var cc = this._createRecipients(originalCc); return cc; }, /** * Returns the template from View.Fields.Base.Emails.ReplyActionField. * * @inheritdoc */ _getHeaderHtmlTemplate: function() { this._tplHeaderHtml = this._tplHeaderHtml || app.template.getField('reply-action', this._tplHeaderHtmlName, 'Emails'); return this._tplHeaderHtml; } }) }, "quickcreate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.QuickcreateField * @alias SUGAR.App.view.fields.BaseEmailsQuickcreateField * @extends View.Fields.Base.QuickcreateField */ ({ // Quickcreate FieldTemplate (base) extendsFrom: 'QuickcreateField', events: { 'click [data-event]': '_handleEventItemClick', }, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['EmailClientLaunch']); this._super('initialize', [options]); }, bindDataChange: function() { this._super('bindDataChange'); if (this.context && this.context.has('model')) { // call updateEmailLinks if the user changes something on the context model // so if user changes the email address we make sure we've got the latest // email address in the quick Compose Email link this.context.get('model').on('change', this.updateEmailLinks, this); } app.routing.before('route', this._beforeRouteChanged, this); app.router.on('route', this._routeChanged, this); }, /** * Trigger sidebar collapse event */ _handleEventItemClick: function() { app.events.trigger('sidebar-nav:expand:toggle', false); }, /** * Before we navigate to a different page, we need to remove the * change event listener we added on the context model * * @protected */ _beforeRouteChanged: function() { if (this.context && this.context.has('model')) { // route is about to change, need to remove previous // listeners before model gets changed this.context.get('model').off('change', null, this); } }, /** * After the route has changed, we need to re-add the model listener * on the new context model. This also calls updateEmailLinks to blank * out any existing email on the current quickcreate link; e.g. re-set the * quick Compose Email link back to "mailto:" * * @protected */ _routeChanged: function() { if (this.context && this.context.has('model')) { // route has changed, most likely a new model, need to add new listeners this.context.get('model').on('change', this.updateEmailLinks, this); } this.updateEmailLinks(); }, /** * @inheritdoc * @private */ _render: function() { this.usingInternalEmailClient = this.useSugarEmailClient(); this._super('_render'); }, /** * Used by EmailClientLaunch as a hook point to retrieve email options that are specific to a view/field * In this case we are using it to retrieve the parent model to make this email compose launching * context aware - prepopulating the to address with the given model and the parent relate field * * @return {Object} * @private */ _retrieveEmailOptionsFromLink: function() { var context = this.context.parent || this.context, parentModel = context.get('model'), emailOptions = {}; if (parentModel && parentModel.id) { // set parent model as option to be passed to compose for To address & relate // if parentModel does not have email, it will be ignored as a To recipient // if parentModel's module is not an available module to relate, it will also be ignored emailOptions = { to: [{bean: parentModel}], related: parentModel }; } return emailOptions; }, /** * @inheritdoc */ _dispose: function() { // remove context model change listeners if they exist this._beforeRouteChanged(); app.routing.offBefore('route', this.beforeRouteChanged, this); app.router.off('route', this.routeChanged, this); this._super('_dispose'); } }) }, "compose-actionbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Actionbar for the email compose view * * @class View.Fields.Base.Emails.ComposeActionbarField * @alias SUGAR.App.view.fields.BaseEmailsComposeActionbarField * @extends View.Fields.Base.FieldsetField * * @deprecated Use {@link View.Fields.Base.Emails.Htmleditable_tinymceField} * instead to add buttons for email composition. */ ({ // Compose-actionbar FieldTemplate (base) extendsFrom: 'FieldsetField', events: { 'click a:not(.dropdown-toggle)': 'handleButtonClick' }, /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Fields.Base.Emails.SenderField is deprecated. Use ' + 'View.Fields.Base.Emails.Htmleditable_tinymceField instead.'); this._super('initialize', [options]); this.type = 'fieldset'; }, /** * Fire an event when any of the buttons on the actionbar are clicked * Events could be set via the data-event attribute or an event is built using the button name * * @param evt */ handleButtonClick: function(evt) { var triggerName, buttonName, $currentTarget = $(evt.currentTarget); if ($currentTarget.data('event')) { triggerName = $currentTarget.data('event'); } else { buttonName = $currentTarget.attr('name') || 'button'; triggerName = 'actionbar:' + buttonName + ':clicked'; } this.view.context.trigger(triggerName); } }) }, "email-attachments": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.EmailAttachmentsField * @alias SUGAR.App.view.fields.BaseEmailsEmailAttachmentsField * @extends View.Fields.Base.EmailAttachmentsField */ ({ // Email-attachments FieldTemplate (base) extendsFrom: 'BaseEmailAttachmentsField', /** * @inheritdoc * * Adds a listener for the `email_attachments:template` event, which is * triggered on the view to add attachments. The handler will fetch the * attachments from a template, so that they can be copied to the email. */ initialize: function(options) { this._super('initialize', [options]); this.listenTo(this.view, 'email_attachments:template', this._fetchTemplateAttachments); }, /** * Retrieves all of an email template's attachments so they can be added to * the email. * * @param {Data.Bean} template The email template whose attachments are to * be added. * @private */ _fetchTemplateAttachments: function(template) { var notes = app.data.createRelatedCollection(template, 'attachments'); var request; if (this.disposed === true) { return; } request = notes.fetch({ success: _.bind(this._handleTemplateAttachmentsFetchSuccess, this), complete: _.bind(function(request) { if (request && request.uid) { delete this._requests[request.uid]; } }, this) }); // This request is not associated with a placeholder because // placeholders aren't used when handling templates. if (request && request.uid) { this._requests[request.uid] = request; } }, /** * Handles a successful response from the API for retrieving an email * template's attachments. * * The relevant data is taken from each record and added as an attachment. * Before adding the new attachments, all existing attachments that came * from another email template are removed. * * @param {Data.BeanCollection} notes The collection of attachments from * the template. * @private */ _handleTemplateAttachmentsFetchSuccess: function(notes) { var attachments; var existingTemplateAttachments; var newTemplateAttachments; if (this.disposed === true) { return; } // Remove all existing attachments that came from an email template. attachments = this.model.get(this.name); existingTemplateAttachments = attachments.where({file_source: 'EmailTemplates'}); attachments.remove(existingTemplateAttachments); // Add the attachments from the new email template. newTemplateAttachments = notes.map(function(model) { return { _link: 'attachments', upload_id: model.get('upload_id') || model.get('id'), name: model.get('filename') || model.get('name'), filename: model.get('filename') || model.get('name'), file_mime_type: model.get('file_mime_type'), file_size: model.get('file_size'), file_ext: model.get('file_ext'), file_source: 'EmailTemplates' }; }); attachments.add(newTemplateAttachments, {merge: true}); } }) }, "outbound-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.OutboundEmailField * @alias SUGAR.App.view.fields.BaseEmailsOutboundEmailField * @extends View.Fields.Base.EnumField */ ({ // Outbound-email FieldTemplate (base) extendsFrom: 'BaseEnumField', /** * Sets the field type to `enum` so that the `BaseEnumField` templates are * loaded. This is necessary when extending a field and using a * different name without any custom templates. * * Adds help text (LBL_OUTBOUND_EMAIL_ID_HELP) for admins. * * @inheritdoc */ initialize: function(options) { if (app.user.get('type') === 'admin') { options.def.help = 'LBL_OUTBOUND_EMAIL_ID_HELP'; } this._super('initialize', [options]); this.type = 'enum'; }, /** * @inheritdoc * * Only add the help tooltip if the help text is being hidden. */ decorateHelper: function() { if (this.def.hideHelp) { this._super('decorateHelper'); } }, /** * @inheritdoc * * Dismisses any alerts with the key `email-client-status`. */ _dispose: function() { app.alert.dismiss('email-client-status'); this._super('_dispose'); }, /** * Shows a warning to the user when a not_authorized error is returned. * * @inheritdoc * @fires email_not_configured Triggered on the view to allow the view to * decide what should be done beyond warning the user. The error is passed * to listeners. */ loadEnumOptions: function(fetch, callback, error) { var oError = error; const oCallback = _.bind(callback, this); callback = _.bind(function() { if (!this.items || _.isEmpty(this.items)) { const messageLabel = app.user.get('type') === 'admin' ? 'LBL_EMAIL_INVALID_USER_CONFIGURATION' : 'LBL_EMAIL_INVALID_SYSTEM_CONFIGURATION'; app.alert.show('email-client-status', { level: 'error', messages: app.lang.get(messageLabel, this.module), autoClose: false, onLinkClick: function() { app.alert.dismiss('email-client-status'); } }); } if (oCallback) { oCallback(); } }, this); error = _.bind(function(e) { if (e.code === 'not_authorized') { // Mark the error as having been handled so that it doesn't get // handled again. e.handled = true; app.alert.show('email-client-status', { level: 'warning', messages: app.lang.get(e.message, this.module), autoClose: false, onLinkClick: function() { app.alert.dismiss('email-client-status'); } }); this.view.trigger('email_not_configured', e); } if (oError) { oError(e); } }, this); this._super('loadEnumOptions', [fetch, callback, error]); } }) }, "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.NameField * @alias SUGAR.App.view.fields.BaseEmailsNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) extendsFrom: 'BaseNameField', /** * @inheritdoc * * Returns "(no subject)" when the email has no subject and not in edit * mode. This allows for the subject to be a link in a list view. */ format: function(value) { if (_.isEmpty(value) && this.action !== 'edit') { return app.lang.get('LBL_NO_SUBJECT', this.module); } return value; }, /** * Build email record route depending on whether or not the email is a * draft and whether the user has the Sugar Email Client option enabled. * * @return {string} */ buildHref: function() { var action = this.def.route && this.def.route.action ? this.def.route.action : null; var module = this.model.module || this.context.get('module'); if (this.model.get('state') === 'Draft' && app.acl.hasAccessToModel('edit', this.model) && this._useSugarEmailClient() && !action ) { action = 'compose'; } return '#' + app.router.buildRoute(module, this.model.get('id'), action); }, /** * Determine if the user is configured to use the Sugar Email Client for * editing existing draft emails. * * @return {boolean} * @private */ _useSugarEmailClient: function() { var emailClientPreference = app.user.getPreference('email_client_preference'); return ( emailClientPreference && emailClientPreference.type === 'sugar' && app.acl.hasAccess('edit', 'Emails') ); } }) }, "reply-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Reply action. * * This allows a user to "reply" to an existing email. * * @class View.Fields.Base.Emails.ReplyActionField * @alias SUGAR.App.view.fields.EmailsBaseReplyActionField * @extends View.Fields.Base.Emails.ForwardActionField */ ({ // Reply-action FieldTemplate (base) extendsFrom: 'EmailsForwardActionField', /** * The name of the template for the reply header. * * @inheritdoc */ _tplHeaderHtmlName: 'reply-header-html', /** * @inheritdoc */ _subjectPrefix: 'LBL_RE', /** * The element ID to use to identify the reply content. * * @inheritdoc */ _contentId: 'replycontent', /** * @inheritdoc * * Updates the reply_to_id email option anytime the model's id attribute * changes. */ bindDataChange: function() { var context = this.context.parent || this.context; var model = context.get('model'); this._super('bindDataChange'); if (model) { // Set the reply_to_id email option if the ID already exists. this.addEmailOptions({reply_to_id: model.get('id')}); // Update the reply_to_id email option anytime the ID changes. This // might occur if the ID was discovered later. It is an edge-case. this.listenTo(model, 'change:id', function() { this.addEmailOptions({reply_to_id: model.get('id')}); }); } }, /** * Returns the recipients to use in the To field of the email. The sender * from the original email is included. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when identifying the recipients. * @return {undefined|Array} */ emailOptionTo: function(model) { var originalTo; var originalSender = model.get('from_collection'); var to = this._createRecipients(originalSender); if (this.def.reply_all) { app.logger.warn('The reply_all option is deprecated. Use View.Fields.Base.Emails.ReplyAllActionField ' + 'instead.'); originalTo = model.get('to_collection'); to = _.union(to, this._createRecipients(originalTo)); } return to; }, /** * Returns the recipients to use in the CC field of the email. The * `reply_all` option is deprecated. Use * View.Fields.Base.Emails.ReplyAllActionField instead. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when identifying the recipients. * @return {undefined|Array} */ emailOptionCc: function(model) { var originalCc; var cc; if (this.def.reply_all) { app.logger.warn('The reply_all option is deprecated. Use View.Fields.Base.Emails.ReplyAllActionField ' + 'instead.'); originalCc = model.get('cc_collection'); cc = this._createRecipients(originalCc); } return cc; }, /** * Attachments are not carried over to replies. * * @inheritdoc */ emailOptionAttachments: function(model) { }, /** * Sets up the email options for the EmailClientLaunch plugin to use - * passing to the email compose drawer or building up the mailto link. * * @protected * @deprecated The EmailClientLaunch plugin handles email options. */ _updateEmailOptions: function() { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_updateEmailOptions is deprecated. ' + 'The EmailClientLaunch plugin handles email options.'); }, /** * Build the reply recipients based on the original email's from, to, and cc * * @param {boolean} all Whether this is reply to all (true) or just a standard * reply (false). * @return {Object} To and Cc values for the reply email. * @return {Array} return.to The to values for the reply email. * @return {Array} return.cc The cc values for the reply email. * @protected * @deprecated Use * View.Fields.Base.Emails.ReplyActionField#emailOptionTo and * View.Fields.Base.Emails.ReplyActionField#emailOptionCc instead. */ _getReplyRecipients: function(all) { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_getReplyRecipients is deprecated. Use ' + 'View.Fields.Base.Emails.ReplyActionField#emailOptionTo and ' + 'View.Fields.Base.Emails.ReplyActionField#emailOptionCc instead.'); if (all) { app.logger.warn('The reply_all option is deprecated. Use View.Fields.Base.Emails.ReplyAllActionField ' + 'instead.'); } return { to: this.emailOptionTo(this.model) || [], cc: this.emailOptionCc(this.model) || [] }; }, /** * Given the original subject, generate a reply subject. * * @param {string} subject * @protected * @deprecated Use * View.Fields.Base.Emails.ReplyActionField#emailOptionSubject instead. */ _getReplySubject: function(subject) { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_getReplySubject is deprecated. Use ' + 'View.Fields.Base.Emails.ReplyActionField#emailOptionSubject instead.'); return this.emailOptionSubject(this.model); }, /** * Get the data required by the header template. * * @return {Object} * @protected * @deprecated Use * View.Fields.Base.Emails.ReplyActionField#_getHeaderParams instead. */ _getReplyHeaderParams: function() { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_getReplyHeaderParams is deprecated. Use ' + 'View.Fields.Base.Emails.ReplyActionField#_getHeaderParams instead.'); return this._getHeaderParams(this.model); }, /** * Build the reply header for text only emails. * * @param {Object} params * @param {string} params.from * @param {string} [params.date] Date original email was sent * @param {string} params.to * @param {string} [params.cc] * @param {string} params.name The subject of the original email. * @return {string} * @private * @deprecated Use * View.Fields.Base.Emails.ReplyActionField#_getHeader instead. */ _getReplyHeader: function(params) { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_getReplyHeader is deprecated. Use ' + 'View.Fields.Base.Emails.ReplyActionField#_getHeader instead.'); return this._getHeader(params); }, /** * Create an array of email recipients from the collection, which can be * used as recipients to pass to the new email. * * @param {Data.BeanCollection} collection * @return {Array} * @private */ _createRecipients: function(collection) { return collection.map(function(recipient) { var data = { email: app.data.createBean('EmailAddresses', recipient.get('email_addresses')) }; var parent; if (recipient.hasParent()) { parent = recipient.getParent(); if (parent) { data.bean = parent; } } return data; }); }, /** * Retrieve the plain text version of the reply body. * * @param {Data.Bean} model The body should come from this model's * attributes. EmailClientLaunch plugin should dictate the model based on * the context. * @return {string} The reply body * @private */ _getReplyBody: function(model) { // Falls back to the `this.model` for backward compatibility. model = model || this.model; return model.get('description') || ''; }, /** * Retrieve the HTML version of the email body. * * Ensure the result is a defined string and strip any signature wrapper * tags to ensure it doesn't get stripped if we insert a signature above * the forward content. Also strip any reply content class if this is a * forward to a previous reply. And strip any forward content class if this * is a forward to a previous forward. * * @return {string} * @protected * @deprecated Use * View.Fields.Base.Emails.ReplyActionField#emailOptionDescriptionHtml * instead. */ _getReplyBodyHtml: function() { app.logger.warn('View.Fields.Base.Emails.ReplyActionField#_getReplyBodyHtml is deprecated. Use ' + 'View.Fields.Base.Emails.ReplyActionField#emailOptionDescriptionHtml instead.'); return this.emailOptionDescriptionHtml(this.model); } }) }, "recipients": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.RecipientsField * @alias SUGAR.App.view.fields.BaseEmailsRecipientsField * @extends View.Fields.Base.BaseField * @deprecated Use {@link View.Fields.Base.Emails.EmailRecipientsField} * instead. */ ({ // Recipients FieldTemplate (base) /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, events: { 'click .btn': '_showAddressBook' }, fieldTag: 'input.select2', plugins: ['DragdropSelect2'], /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Fields.Base.Emails.RecipientsField is deprecated. Use ' + 'View.Fields.Base.Emails.EmailRecipientsField instead.'); this._super('initialize', [options]); // initialize the value to an empty collection this.model.setDefault(this.name, new Backbone.Collection); }, /** * Sets up event handlers for syncing between the model and the recipients field. * * See {@link #format} for the acceptable formats for recipients. */ bindDataChange: function() { /** * Sets up event handlers that allow external forces to manipulate the contents of the collection, while * maintaining the requirement for storing formatted recipients. */ var bindCollectionChange = _.bind(function() { var value = this.model.get(this.name); if (value instanceof Backbone.Collection) { // on "add" we want to force the collection to be reset to guarantee that all models in the collection // have been properly formatted for use in this field value.on('add', function(models, collection) { // Backbone destroys the models currently in the collection on reset, so we must clone the // collection in order to add the same models again collection.reset(collection.clone().models); }, this); // on "remove" the requisite models have already been removed, so we only need to bother updating the // value in the DOM value.on('remove', function(models, collection) { // format the recipients and put them in the DOM this._updateTheDom(this.format(this.model.get(this.name))); }, this); // on "reset" we want to replace all models in the collection with their formatted versions value.on('reset', function(collection) { var recipients = this.format(collection.models); // do this silently so we don't trigger another reset event and end up in an infinite loop collection.reset(recipients, {silent: true}); // put the newly formatted recipients in the DOM this._updateTheDom(recipients); }, this); } }, this); // set up collection event handlers for the initial collection (initialized during this.initialize) bindCollectionChange(); // handle the value on the model being changed to something other than the initial collection this.model.on('change:' + this.name, function(model, recipients) { var value = this.model.get(this.name); if (!(value instanceof Backbone.Collection)) { // whoa! someone changed the value to be something other than a collection // stick that new value inside a collection and reset the value, so we're always dealing with a // collection... another change event will be triggered, so we'll end up in the else block right after // this this.model.set(this.name, new Backbone.Collection(value)); } else { // phew! the value is a collection // but it's not the initial collection, so we'll have to set up collection event handlers for this // instance bindCollectionChange(); // you never know what data someone sticks on the field, so we better reset the values in the collection // so that the recipients become formatted as we expect value.reset(recipients.clone().models); } }, this); }, /** * Sets the value of the Select2 element, decorates any invalid recipients, * and rebuilds the tooltips for all recipients. * * @param {Array} recipients the return value for {@link #format}. */ _updateTheDom: function(recipients) { // put the formatted recipients in the DOM this.getFieldElement().select2('data', recipients); this._decorateInvalidRecipients(); if (!this.def.readonly) { this.setDragDropPluginEvents(this.getFieldElement()); } }, /** * Remove events from the field value if it is a collection */ unbindData: function() { var value = this.model.get(this.name); if (value instanceof Backbone.Collection) { value.off(null, null, this); } this._super('unbindData'); }, /** * Render field with select2 widget * * @private */ _render: function() { var $controlsEl; var $recipientsField; if (this.$el) { $controlsEl = this.$el.closest('.controls'); if ($controlsEl.length) { $controlsEl.addClass('controls-one btn-fit'); } } this._super('_render'); $recipientsField = this.getFieldElement(); if ($recipientsField.length > 0) { $recipientsField.select2({ allowClear: true, multiple: true, width: 'off', containerCssClass: 'select2-choices-pills-close', containerCss: {'width': '100%'}, minimumInputLength: 1, query: _.bind(function(query) { this.loadOptions(query); }, this), createSearchChoice: _.bind(this.createOption, this), formatSelection: _.bind(this.formatSelection, this), formatResult: _.bind(this.formatResult, this), formatSearching: _.bind(this.formatSearching, this), formatInputTooShort: _.bind(this.formatInputTooShort, this), selectOnBlur: true }); if (!!this.def.disabled) { $recipientsField.select2('disable'); } if (!this.def.readonly) { this.setDragDropPluginEvents(this.getFieldElement()); } } }, /** * Fetches additional recipients from the server. * * See [Select2 Documentation of `query` parameter](http://ivaynberg.github.io/select2/#doc-query). * * @param {Object} query Possible attributes can be found in select2's * documentation. */ loadOptions: _.debounce(function(query) { var self = this, data = { results: [], // only show one page of results // if more results are needed, then the address book should be used more: false }, options = {}, callbacks = {}, url; // add the search term to the URL params options.q = query.term; // the first 10 results should be enough // if more results are needed, then the address book should be used options.max_num = 10; // build the URL for fetching recipients that match the search term url = app.api.buildURL('Mail', 'recipients/find', null, options); // create the callbacks callbacks.success = function(result) { // the api returns objects formatted such that sidecar can convert them to beans // we need the records to be in a standard object format (@see RecipientsField::format) and the records // need to be converted into beans before we can format them var records = app.data.createMixedBeanCollection(result.records); // format and add the recipients that were found via the select2 callback data.results = self.format(records); }; callbacks.error = function() { // don't add any recipients via the select2 callback data.results = []; }; callbacks.complete = function() { // execute the select2 callback to add any new recipients query.callback(data); }; app.api.call('read', url, null, callbacks); }, 300), /** * Create additional select2 options when loadOptions returns no matches for the search term. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {String} term * @param {Array} data The options in the select2 drop-down after the query callback has been executed. * @return {Object} */ createOption: function(term, data) { if (data.length === 0) { return {id: term, email: term}; } }, /** * Formats a recipient object for displaying selected recipients. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Object} recipient * @return {String} */ formatSelection: function(recipient) { var value = recipient.name || recipient.email, template = app.template.getField(this.type, 'select2-selection', this.module); if (template) { return template({ id: recipient.id, name: value, email: recipient.email, invalid: recipient._invalid }); } return value; }, /** * Formats a recipient object for displaying items in the recipient options list. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Object} recipient * @return {String} */ formatResult: function(recipient) { var format, email = Handlebars.Utils.escapeExpression(recipient.email); if (recipient.name) { format = '"' + Handlebars.Utils.escapeExpression(recipient.name) + '" <' + email + '>'; } else { format = email; } return format; }, /** * Returns the localized message indicating that a search is in progress * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @return {string} */ formatSearching: function() { return app.lang.get('LBL_LOADING', this.module); }, /** * Suppresses the message indicating the number of characters remaining before a search will trigger * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {string} term Search string entered by user. * @param {number} min Minimum required term length. * @return {string} */ formatInputTooShort: function(term, min) { return ''; }, /** * Formats a set of recipients into an array of objects that select2 understands. * * See {@link #_formatRecipient} for the acceptable/expected attributes to * be found on each recipient. * * @param {Mixed} data A Backbone collection, a single Backbone model or standard JavaScript object, or an array of * Backbone models or standard JavaScript objects. * @return {Array} */ format: function(data) { var formattedRecipients = []; // the lowest common denominator of potential inputs is an array of objects // force the parameter to be an array of either objects or Backbone models so that we're always dealing with // one data-structure type if (data instanceof Backbone.Collection) { // get the raw array of models data = data.models; } else if (data instanceof Backbone.Model || (_.isObject(data) && !_.isArray(data))) { // wrap the single model in an array so the code below behaves the same whether it's a model or a collection data = [data]; } if (_.isArray(data)) { _.each(data, function(recipient) { var formattedRecipient; if (!(recipient instanceof Backbone.Model)) { // force the object to be a Backbone.Model to allow for certain assumptions to be made // there is no harm in this because the recipient will not be added to the return value if no email // address is found on the model recipient = new Backbone.Model(recipient); } formattedRecipient = this._formatRecipient(recipient); // only add the recipient if there is an email address if (!_.isEmpty(formattedRecipient.email)) { formattedRecipients.push(formattedRecipient); } }, this); } return formattedRecipients; }, /** * Determine whether or not the recipient pills should be locked. * @return {boolean} */ recipientsLocked: function() { return this.def.readonly || false; }, /** * Synchronize the recipient field value with the model and setup tooltips for email pills. */ bindDomChange: function() { var self = this; this.getFieldElement() .on('change', function(event) { var value = $(this).select2('data'); if (event.removed) { value = _.filter(value, function(d) { return d.id !== event.removed.id; }); } self.model.get(self.name).reset(value); }) .on('select2-selecting', _.bind(this._handleEventOnSelected, this)); }, /** * Event handler for the Select2 "select2-selecting" event. * * @param {Event} event * @return {boolean} * @private */ _handleEventOnSelected: function(event) { // only allow the user to select an option if it is determined to be a valid email address // returning true will select the option; false will prevent the option from being selected var isValidChoice = false; // since this event is fired twice, we only want to perform validation on the first event // event.object is not available on the second event if (event.object) { // the id and email address will not match when the email address came from the database and // we are assuming that email addresses stored in the database have already been validated if (event.object.id == event.object.email) { // this option must be a new email address that the application does not recognize // so mark it as valid and kick off an async validation isValidChoice = true; this._validateEmailAddress(event.object); } else { // the application should recognize the email address, so no need to validate it again // just assume it's a valid choice and we'll deal with the consequences later (server-side) isValidChoice = true; } } return isValidChoice; }, /** * Destroy all select2 and tooltip plugins */ unbindDom: function() { this.getFieldElement().select2('destroy'); this._super('unbindDom'); }, /** * When in edit mode, the field includes an icon button for opening an address book. Clicking the button will * trigger an event to open the address book, which calls this method to do the dirty work. The selected recipients * are added to this field upon closing the address book. * * @private */ _showAddressBook: function() { /** * Callback to add recipients, from a closing drawer, to the target Recipients field. * @param {undefined|Backbone.Collection} recipients */ var addRecipients = _.bind(function(recipients) { if (recipients && recipients.length > 0) { this.model.get(this.name).add(recipients.models); } }, this); app.drawer.open( { layout: 'compose-addressbook', context: { module: 'Emails', mixed: true } }, function(recipients) { addRecipients(recipients); } ); }, /** * update ul.select2-choices data attribute which prevents underrun of pills by * using a css definition for :before {content:''} set to float right * * @param {string} content */ setContentBefore: function(content) { this.$('.select2-choices').attr('data-content-before', content); }, /** * Gets the recipients DOM field * * @return {Object} DOM Element */ getFieldElement: function() { return this.$(this.fieldTag); }, /** * Format a recipient from a Backbone.Model to a standard JavaScript object with id, module, email, and name * attributes. Only id and email are required for the recipient to be considered valid * {@link #format}. * * All attributes are optional. However, if the email attribute is not present, then a primary email address should * exist on the bean. Without an email address that can be resolved, the recipient is considered to be invalid. The * bean attribute must be a Backbone.Model and it is likely to be a Bean. Data found in the bean is considered to be * secondary to the attributes found on its parent model. The bean is a mechanism for collecting additional * information about the recipient that may not have been explicitly set when the recipient was passed in. * @param {Backbone.Model} recipient * @return {Object} * @private */ _formatRecipient: function(recipient) { var formattedRecipient = {}; if (recipient instanceof Backbone.Model) { var bean = recipient.get('bean'); // if there is a bean attribute, then more data can be extracted about the recipient to fill in any holes if // attributes are missing amongst the primary attributes // so follow the trail using recursion if (bean) { formattedRecipient = this._formatRecipient(bean); } // prioritize any values found on recipient over those already extracted from bean formattedRecipient = { id: recipient.get('id') || formattedRecipient.id || recipient.get('email'), module: recipient.get('module') || recipient.module || recipient.get('_module') || formattedRecipient.module, email: recipient.get('email') || formattedRecipient.email, locked: this.recipientsLocked(), name: recipient.get('name') || recipient.get('full_name') || formattedRecipient.name, _invalid: recipient.get('_invalid') }; // don't bother with the recipient unless an id is present if (!_.isEmpty(formattedRecipient.id)) { // extract the primary email address for the recipient if (_.isArray(formattedRecipient.email)) { var primaryEmailAddress = _.findWhere(formattedRecipient.email, {primary_address: true}); if (!_.isUndefined(primaryEmailAddress) && !_.isEmpty(primaryEmailAddress.email_address)) { formattedRecipient.email = primaryEmailAddress.email_address; } } // drop any values that are empty or non-compliant _.each(formattedRecipient, function(val, key) { if ((_.isEmpty(formattedRecipient[key]) || !_.isString(formattedRecipient[key])) && !_.isBoolean(formattedRecipient[key])) { delete formattedRecipient[key]; } }); } else { // drop all values if an id isn't present formattedRecipient = {}; } } return formattedRecipient; }, /** * Validates an email address on the server asynchronously. * * Marks the recipient as invalid if it is not a valid email address. * * @param {Object} recipient * @param {string} recipient.id * @param {string} recipient.email * @private */ _validateEmailAddress: function(recipient) { var callbacks = {}; var url = app.api.buildURL('Mail', 'address/validate'); callbacks.success = _.bind(function(result) { if (!result[recipient.email] && !this.disposed) { this._markRecipientInvalid(recipient.id); } }, this); callbacks.error = _.bind(function() { if (!this.disposed) { this._markRecipientInvalid(recipient.id); } }, this); app.api.call('create', url, [recipient.email], callbacks); }, /** * Mark the given recipient as invalid in the collection and update select2. * * @param {string} recipientId * @private */ _markRecipientInvalid: function(recipientId) { var recipients = this.model.get(this.name); var recipient = recipients.get(recipientId); recipient.set('_invalid', true); this._updateTheDom(this.format(recipients)); }, /** * Decorate any invalid recipients in this field. * @private */ _decorateInvalidRecipients: function() { var self = this; var $invalidRecipients = this.$('.select2-search-choice [data-invalid="true"]'); $invalidRecipients.each(function() { var $choice = $(this).closest('.select2-search-choice'); $choice.addClass('select2-choice-danger'); $(this).attr('data-title', app.lang.get('ERR_INVALID_EMAIL_ADDRESS', self.module)); }); } }) }, "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.Htmleditable_tinymceField * @alias SUGAR.App.view.fields.BaseEmailsHtmleditable_tinymceField * @extends View.Fields.Base.Htmleditable_tinymceField */ ({ // Htmleditable_tinymce FieldTemplate (base) extendsFrom: 'Htmleditable_tinymceField', /** * Force the field to display the correct view even if there is no data to * show. * * @property {boolean} */ showNoData: false, /** * Constant for inserting content above the existing email body. * * @property {string} */ ABOVE_CONTENT: 'above', /** * Constant for inserting content below the existing email body. * * @property {string} */ BELOW_CONTENT: 'below', /** * Constant for inserting content into the email body at the current cursor * location. * * @property {string} */ CURSOR_LOCATION: 'cursor', /** * The tinyMCE button API object for the signature dropdown. * * @private * @property {Object|null} */ _signatureBtnApi: null, /** * List of menu items for fetched signatures * * @private * @property {Array} */ _signatureSubmenu: [], /** * The number of signatures found from the API response. * * @private * @property {number} */ _numSignatures: 0, /** * Track the editor focus/blur state. * * @private * @property {boolean} */ _editorFocused: false, /** * @inheritdoc * * Stores the user's default signature on the context using the attribute * name `current_signature`. This attribute is updated anytime a new * signature is selected. * * Stores the initial signature location for inserting the default * signature. If the context already has `signature_location` attribute, * then that value is used. Otherwise, this attribute is defaulted to * insert the signature below any content. This attribute is updated * anytime a signature is inserted in a different location. * * The default signature is inserted in the initial location if the email * is new. The signature is not inserted if the email is an existing draft * that is being edited. If the initial location is the cursor, then the * signature is inserted after the editor is fully loaded and the cursor * has been placed. * * For new replies, the cursor is placed above the reply content, once the * editor has been loaded. */ initialize: function(options) { var signature; var location; // We insert an empty <p> node in the tinyMCE editor and use that to // focus the cursor to the bottom of the tinyMCE editor. This is // because if the last element in the editor has content // (i.e. <p>Sincercely, John Doe</p>) and we select that element, the // cursor would be placed at the beginning of the content // (in the example, the cursor would be before the "S" in Sincerely). var emptyNode; this._super('initialize', [options]); // Get the default signature and store it on the context. signature = app.user.getPreference('signature_default'); if (!(signature instanceof app.Bean)) { signature = app.data.createBean('UserSignatures', signature); } this.context.set('current_signature', signature); // Determine the initial signature location for inserting the default. location = this.context.get('signature_location'); if (_.isEmpty(location)) { // Default the location. location = this.BELOW_CONTENT; this.context.set('signature_location', location); } // Don't do the following if updating an existing draft. if (this.model.isNew()) { // Insert the default signature. if (location === this.CURSOR_LOCATION) { // Need to wait for the editor before inserting. this.listenToOnce(this.context, 'tinymce:oninit', function() { this._insertSignature(signature, location); }); } else { this._insertSignature(signature, location); } // Focus the editor and place the cursor at the desired location. if (!_.isEmpty(this.context.get('cursor_location'))) { this.listenToOnce(this.context, 'tinymce:oninit', function() { if (this._htmleditor) { this._htmleditor.focus(); // Move the cursor to the bottom of the editor by // inserting an empty node and selecting it. if (this.context.get('cursor_location') == this.BELOW_CONTENT) { emptyNode = this._insertNodeInEditor(); if (emptyNode) { this._htmleditor.selection.setCursorLocation(emptyNode); this._htmleditor.selection.collapse(true); } } } }); } } }, /** * Suppress calling the sidecar _render method in detail view * * @inheritdoc */ _render: function() { if (this._isEditView()) { this._super('_render'); this.$el.toggleClass('detail', false).toggleClass('edit', true); } else { this.destroyTinyMCEEditor(); // Hide the field for now. Once the field loads its contents completely, we will show it. This helps // to prevent a momentary white background/flash in the iframe before it finishes loading in dark mode this.hide(); this._renderView(); this.$el.toggleClass('detail', true).toggleClass('edit', false); } return this; }, /** * Replicate the sidecar render logic for detail view except for * manually appending an iframe instead of invoking the template * * @inheritdoc */ _renderView: function() { var self = this; var iFrame; // sets this.tplName and this.action this._loadTemplate(); if (this.model instanceof Backbone.Model) { this.value = this.getFormattedValue(); } this.dir = _.result(this, 'direction'); if (app.lang.direction === this.dir) { delete this.dir; } this.unbindDom(); // begin custom rendering if (this.$el.find('iframe').length === 0) { iFrame = $('<iframe>', { src: '', class: 'htmleditable w-full' + (this.def.span ? ' span' + this.def.span : ''), frameborder: 0, name: this.name }); // Perform it on load for Firefox. iFrame.appendTo(this.$el).on('load', function() { self._setIframeBaseTarget(iFrame, '_blank'); }); this._setIframeBaseTarget(iFrame, '_blank'); } this.setViewContent(this.value); // end custom rendering if (this.def && this.def.css_class) { this.getFieldElement().addClass(this.def.css_class); } this.$(this.fieldTag).attr('dir', this.dir); this.bindDomChange(); }, /** * @inheritdoc * * Resize the field's container based on the height of the iframe content * for preview. */ _setViewContentInternal: function(editable, value, styleSrc = 'styleguide/assets/css/iframe-sugar.css') { this._super('_setViewContentInternal', [editable, value, styleSrc]); // Only set this field height if it is in the preview or detail pane if (!_.contains(['preview', 'detail'], this.tplName)) { return; } if (!this._iframeHasBody(editable)) { return; } var iframeWaitingTime = 50; _.debounce(_.bind(function() { if (this.disposed) { return; } // Pad this to the final height due to the iframe margins/padding var padding = (this.tplName === 'detail' || this.tplName === 'preview') ? 0 : 25; var contentHeight = 0; contentHeight = this._getContentHeight() + padding; // Only resize the editor when the content is fully loaded if (contentHeight > padding) { // Set the maximum height to 400px if (contentHeight > 400) { contentHeight = 400; } editable.css('height', contentHeight); if (this.view) { this.view.trigger('tinymce:resize'); } } }, this), iframeWaitingTime)(); }, /** * Set iframe base target value * * @param {jQuery} iFrame The iframe element that the target will be added to. * @param {string} targetValue e.g. _self, _blank, _parent, _top or frameName * @private */ _setIframeBaseTarget: function(iFrame, targetValue) { var target = $('<base>', { target: targetValue }); target.appendTo(iFrame.contents().find('head')); }, /** * @inheritdoc * * Adds buttons for uploading a local file and selecting a Sugar Document * to attach to the email. * * Adds a button for selecting and inserting a signature at the cursor. * * Adds a button for selecting and applying a template. * * @fires email_attachments:file on the view when the user elects to attach * a local file. */ addCustomButtons: function(editor) { var attachmentButtons = []; // Attachments can only be added if the user has permission to create // Notes records. Only add the attachment button(s) if the user is // allowed. if (app.acl.hasAccess('create', 'Notes')) { attachmentButtons.push({ text: app.lang.get('LBL_ATTACH_FROM_LOCAL', this.module), type: 'menuitem', onAction: (event) => { // Track click on the file attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_file_button', event); this.view.trigger('email_attachments:file'); }, }); // The user can only select a document to attach if he/she has // permission to view Documents records in the selection list. // Don't add the Documents button if the user can't view and select // documents. if (app.acl.hasAccess('view', 'Documents')) { attachmentButtons.push({ text: app.lang.get('LBL_ATTACH_SUGAR_DOC', this.module), type: 'menuitem', onAction: (event) => { // Track click on the document attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_doc_button', event); this._selectDocument(); }, }); } editor.ui.registry.addMenuButton('sugarattachment', { tooltip: app.lang.get('LBL_ATTACHMENT', this.module), icon: 'plus', onAction: (event) => { // Track click on the attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_button', event); }, fetch: (callback) => { callback(attachmentButtons); }, }); } editor.ui.registry.addMenuButton('sugarsignature', { tooltip: app.lang.get('LBL_SIGNATURE', this.module), icon: 'edit-block', // disable the signature button until they have been loaded onSetup: (btnApi) => { btnApi.setEnabled(false); this._signatureBtnApi = btnApi; // load the users signatures this._getSignatures(); }, onAction: (event) => { // Track click on the signature button. app.analytics.trackEvent('click', 'tinymce_email_signature_button', event); }, fetch: (callback) => { callback(this._signatureSubmenu); }, }); if (app.acl.hasAccess('view', 'EmailTemplates')) { editor.ui.registry.addButton('sugartemplate', { tooltip: app.lang.get('LBL_TEMPLATE', this.module), icon: 'document-properties', onAction: (event) => { // Track click on the template button. app.analytics.trackEvent('click', 'tinymce_email_template_button', event); this._selectEmailTemplate(); }, }); } // Enable the signature button when the editor is focused and the user // has signatures that can be inserted. editor.on('focus', _.bind(function(e) { this._editorFocused = true; this.view.trigger('tinymce:focus'); // the user has at least 1 signature if (this._numSignatures > 0) { // enable the signature button this._signatureBtnApi.setEnabled(true); } }, this)); // Disable the signature button when the editor is blurred and the user // has signatures. Signatures are inserted at the cursor location. If // the button is not disabled when the editor is unfocused, then issues // would arise with the user clicking a signature to insert at the // cursor without a cursor being present. editor.on('blur', _.bind(function(e) { this._editorFocused = false; this.view.trigger('tinymce:blur'); // the user has at least 1 signature if (this._numSignatures > 0) { // disable the signature button this._signatureBtnApi.setEnabled(false); } }, this)); }, /** * Inserts the content into the TinyMCE editor at the specified location. * * @private * @param {string} content * @param {string} [location="cursor"] Whether to insert the new content * above existing content, below existing content, or at the cursor * location. Defaults to being inserted at the cursor position. * @return {string} The updated content. */ _insertInEditor: function(content, location) { var emailBody = this.model.get(this.name) || ''; if (_.isEmpty(content)) { return emailBody; } // Default to the cursor location. location = location || this.CURSOR_LOCATION; // Add empty divs so user can place the cursor on the line before or // after. content = '<div></div>' + content + '<div></div>'; if (location === this.CURSOR_LOCATION) { if (_.isNull(this._htmleditor)) { // Unable to insert content at the cursor without an editor. return emailBody; } this._htmleditor.insertContent(content); // Get the HTML content from the editor. emailBody = this._htmleditor.getContent(); } else if (location === this.BELOW_CONTENT) { emailBody += content; } else if (location === this.ABOVE_CONTENT) { emailBody = content + emailBody; } // Update the model with the new content. this.model.set(this.name, emailBody); return emailBody; }, /** * Inserts a unique element into the TinyMCE editor to the end of the * <body>. * * @private * @return {HTMLElement|boolean} The inserted element or false if an * element can't be inserted. */ _insertNodeInEditor: function() { var body; var uniqueId; if (this._htmleditor) { body = this._htmleditor.getBody(); uniqueId = this._htmleditor.dom.uniqueId(); $('<p id="' + uniqueId + '"><br /></p>').appendTo(body); return this._htmleditor.dom.select('p#' + uniqueId)[0]; } // There is no editor to insert the element into. return false; }, /** * Fetches the signatures for the current user. * * @private */ _getSignatures: function() { var signatures = app.data.createBeanCollection('UserSignatures'); signatures.filterDef = [{ user_id: {$equals: app.user.get('id')} }]; signatures.fetch({ max_num: -1, // Get as many as we can. success: _.bind(this._getSignaturesSuccess, this), error: function() { app.alert.show('server-error', { level: 'error', messages: 'ERR_GENERIC_SERVER_ERROR' }); } }); }, /** * Add each signature as buttons under the signature button. * * @private * @param {Data.BeanCollection} signatures */ _getSignaturesSuccess: function(signatures) { this._signatureSubmenu = []; if (this.disposed === true) { return; } if (!_.isUndefined(signatures) && !_.isUndefined(signatures.models)) { signatures = signatures.models; } else { app.alert.show('server-error', { level: 'error', messages: 'ERR_GENERIC_SERVER_ERROR' }); return; } if (!_.isNull(this._signatureBtnApi)) { // write the signature names to the control dropdown _.each(signatures, _.bind(function(signature) { this._signatureSubmenu.push({ text: signature.get('name'), type: 'menuitem', onAction: (event) => { // Track click on a signature. app.analytics.trackEvent('click', 'email_signature', event); this._insertSignature(signature, this.CURSOR_LOCATION); }, }); }, this)); // Set the number of signatures the user has this._numSignatures = signatures.length; // If the editor is focused before the signatures are returned, enable the signature button if (this._editorFocused) { this._signatureBtnApi.setEnabled(true); } } }, /** * Inserts the signature into the editor. * * @private * @param {Data.Bean} signature * @param {string} [location="cursor"] Whether to insert the new content * above existing content, below existing content, or at the cursor * location. Defaults to being inserted at the cursor position. */ _insertSignature: function(signature, location) { var htmlBodyObj; var emailBody; var signatureHtml; var decodedSignature; var signatureContent; function decodeBrackets(str) { str = str.replace(/</gi, '<'); str = str.replace(/>/gi, '>'); return str; } if (this.disposed === true) { return; } if (!(signature instanceof app.Bean)) { return; } signatureHtml = signature.get('signature_html'); if (_.isEmpty(signatureHtml)) { return; } decodedSignature = decodeBrackets(signatureHtml); signatureContent = '<div class="signature keep">' + decodedSignature + '</div>'; emailBody = this._insertInEditor(signatureContent, location); htmlBodyObj = $('<div>' + this.sanitizeContent(emailBody) + '</div>'); // Mark each signature to either keep or remove. $('div.signature', htmlBodyObj).each(function() { if (!$(this).hasClass('keep')) { // Mark for removal. $(this).addClass('remove'); } else { // If the parent is also a signature, move the node out of the // parent so it isn't removed. if ($(this).parent().hasClass('signature')) { // Move the signature outside of the nested signature. $(this).parent().before(this); } // Remove the "keep" class so if another signature is added it // will remove this one. $(this).removeClass('keep'); } }); // After each signature is marked, perform the removal. htmlBodyObj.find('div.signature.remove').remove(); emailBody = htmlBodyObj.html(); this.model.set(this.name, emailBody); this.context.set('current_signature', signature); this.context.set('signature_location', location || this.CURSOR_LOCATION); }, /** * Allows the user to select a template to apply. * * @private */ _selectEmailTemplate: function() { var def = { layout: 'selection-list', context: { module: 'EmailTemplates', fields: [ 'subject', 'body', 'body_html', 'text_only' ] } }; app.drawer.open(def, _.bind(this._onEmailTemplateDrawerClose, this)); }, /** * Verifies that the user has access to the email template before applying * it. * * @private * @param {Data.Bean} model */ _onEmailTemplateDrawerClose: function(model) { var emailTemplate; if (this.disposed === true) { return; } // This is an edge case where user has List but not View permission. // Search & Select will return only id and name if View permission is // not permitted for this record. Display appropriate error. if (model && _.isUndefined(model.subject)) { app.alert.show('no_access_error', { level: 'error', messages: app.lang.get('ERR_NO_ACCESS', this.module, {name: model.value}) }); } else if (model) { // `value` is not a real attribute. emailTemplate = app.data.createBean('EmailTemplates', _.omit(model, 'value')); this._confirmTemplate(emailTemplate); } }, /** * Confirms that the user wishes to replace all content in the editor. The * template is applied if there is no existing content or if the user * confirms "yes". * * @private * @param {Data.Bean} template */ _confirmTemplate: function(template) { var subject = this.model.get('name') || ''; var text = this.model.get('description') || ''; var html = this.model.get(this.name) || ''; var fullContent = subject + text + html; if (_.isEmpty(fullContent)) { this._applyTemplate(template); } else { app.alert.show('delete_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_EMAILTEMPLATE_MESSAGE_SHOW_MSG', this.module), onConfirm: _.bind(function(event) { // Track click on confirmation button. app.analytics.trackEvent('click', 'email_template_confirm', event); this._applyTemplate(template); }, this), onCancel: function(event) { // Track click on cancel button. app.analytics.trackEvent('click', 'email_template_cancel', event); } }); } }, /** * Inserts the template into the editor. * * The template's subject does not overwrite the existing subject if: * * 1. The email is a forward or reply. * 2. The template does not have a subject. * * @private * @fires email_attachments:template on the view with the selected template * as a parameter. {@link View.Fields.Base.Emails.EmailAttachmentsField} * adds the template's attachments to the email. * @param {Data.Bean} template */ _applyTemplate: function(template) { var body; var replyContent; var forwardContent; var subject; var signature = this.context.get('current_signature'); /** * Check the email body and pull out any forward/reply content from a * draft email. * * @param {string} body The full content to search. * @return {string} The forward/reply content. */ function getForwardReplyContent(body, id) { var content = ''; var $content; if (body) { $content = $('<div>' + body + '</div>').find('div#' + id); if ($content.length > 0) { content = $content[0].outerHTML; } } return content; } if (this.disposed === true) { return; } // Track applying an email template. app.analytics.trackEvent('email_template', 'apply', template); replyContent = getForwardReplyContent(this.model.get(this.name), 'replycontent'); forwardContent = getForwardReplyContent(this.model.get(this.name), 'forwardcontent'); subject = template.get('subject'); // Only use the subject if it's not a forward or reply. if (subject && !(replyContent || forwardContent)) { this.model.set('name', subject); } //TODO: May need to move over replaces special characters. body = template.get('text_only') ? template.get('body') : template.get('body_html'); this.model.set(this.name, body); this.view.trigger('email_attachments:template', template); // The HTML signature is used even when the template is text-only. if (signature) { this._insertSignature(signature, this.BELOW_CONTENT); } // Append the reply content to the end of the email. if (replyContent) { this._insertInEditor(replyContent, this.BELOW_CONTENT); } // Append the forward content to the end of the email. if (forwardContent) { this._insertInEditor(forwardContent, this.BELOW_CONTENT); } }, /** * Allows the user to select a document to attach. * * @private * @fires email_attachments:document on the view with the selected document * as a parameter. {@link View.Fields.Base.EmailAttachmentsField} attaches * the document to the email. */ _selectDocument: function() { var def = { layout: 'selection-list', context: { module: 'Documents' } }; app.drawer.open(def, _.bind(function(model) { var document; if (model) { // `value` is not a real attribute. document = app.data.createBean('Documents', _.omit(model, 'value')); this.view.trigger('email_attachments:document', document); } }, this)); } }) }, "email-recipients": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.EmailRecipientsField * @alias SUGAR.App.view.fields.BaseEmailsEmailRecipientsField * @extends View.Fields.Base.BaseField */ ({ // Email-recipients FieldTemplate (base) /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, /** * The selector for accessing the Select2 field when in edit mode. The * Select2 field is where the recipients are displayed. * * @property {string} */ fieldTag: 'input.select2', /** * @inheritdoc */ initialize: function(options) { var plugins = [ 'CollectionFieldLoadAll', 'EmailParticipants', 'DragdropSelect2', 'ListEditable' ]; this.plugins = _.union(this.plugins || [], plugins); this.events = _.extend({}, this.events, { 'click .btn': '_showAddressBook' }); this._super('initialize', [options]); // Specify the error label for when any recipient's email address is // invalid. app.error.errorName2Keys[this.type] = app.lang.get('ERR_INVALID_RECIPIENTS', this.module); }, /** * @inheritdoc */ bindDataChange: function() { if (this.model) { // Avoids a full re-rendering when editing. The current value of // the field is formatted and passed directly to Select2 when in // edit mode. this.listenTo(this.model, 'change:' + this.name, _.bind(function() { var $el = this.$(this.fieldTag); if (_.isEmpty($el.data('select2'))) { this.render(); } else { $el.select2('data', this.getFormattedValue()); this._decorateRecipients(); this._enableDragDrop(); } }, this)); } }, /** * @inheritdoc */ bindDomChange: function() { var $el = this.$(this.fieldTag); $el.on('select2-selecting', _.bind(function(event) { // Don't add the choice if it duplicates an existing recipient. var duplicate = this.model.get(this.name).find(function(model) { if (event.choice.get('parent_id')) { return event.choice.get('parent_type') === model.get('parent_type') && event.choice.get('parent_id') === model.get('parent_id'); } return event.choice.get('email_address_id') === model.get('email_address_id') || event.choice.get('email_address') === model.get('email_address'); }); if (this.disposed || duplicate) { event.preventDefault(); } }, this)); $el.on('change', _.bind(function(event) { var collection; if (this.model && !this.disposed) { collection = this.model.get(this.name); if (!_.isEmpty(event.added)) { collection.add(event.added); } if (!_.isEmpty(event.removed)) { collection.remove(event.removed); } } }, this)); }, /** * @inheritdoc * * Destroys the Select2 element. */ unbindDom: function() { this.$(this.fieldTag).select2('destroy'); this._super('unbindDom'); }, /** * @inheritdoc */ _render: function() { var $el; var options; this._super('_render'); $el = this.$(this.fieldTag); if ($el.length > 0) { options = this.getSelect2Options(); options = _.extend(options, { allowClear: true, multiple: true, containerCssClass: 'select2-choices-pills-close', /** * Constructs a representation for a selected recipient to be * displayed in the field. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Data.Bean} recipient * @return {string} * @private */ formatSelection: _.bind(function(recipient) { var template = app.template.getField(this.type, 'select2-selection', this.module); var name = recipient.get('parent_name') || ''; var email = recipient.get('email_address') || ''; // The name was erased, so let's use the label. if (_.isEmpty(name) && recipient.nameIsErased) { name = app.lang.get('LBL_VALUE_ERASED', recipient.module); } // The email was erased, so let's use the label. if (_.isEmpty(email) && recipient.emailIsErased) { email = app.lang.get('LBL_VALUE_ERASED', recipient.module); } return template({ cid: recipient.cid, name: name || email, email_address: email, invalid: recipient.invalid, opt_out: !!recipient.get('opt_out'), name_is_erased: recipient.nameIsErased, email_is_erased: recipient.emailIsErased }); }, this), /** * Constructs a representation for the recipient to be * displayed in the dropdown options after a query. * * See [Select2 Documentation](http://ivaynberg.github.io/select2/#documentation). * * @param {Data.Bean} recipient * @return {string} */ formatResult: _.bind(function(recipient) { var template = app.template.getField(this.type, 'select2-result', this.module); return template({ value: recipient.toHeaderString({quote_name: true}), module: recipient.get('parent_type') }); }, this), /** * Don't escape a choice's markup since we built the HTML. * * See [Select2 Documentation](https://select2.github.io/select2/#documentation). * * @param {string} markup * @return {string} */ escapeMarkup: function(markup) { return markup; } }); $el.select2(options).select2('val', []); if (this.isDisabled()) { $el.select2('disable'); } this._decorateRecipients(); this._enableDragDrop(); } }, /** * @inheritdoc * @return {Array} */ format: function(value) { // Reset the tooltip. this.tooltip = ''; if (value instanceof app.BeanCollection) { value = value.map(this.prepareModel, this); // Must wrap the callback in a function or else the collection's // index will be passed, causing the second parameter of // EmailParticipantsPlugin#formatForHeader to unintentionally // receive a value. this.tooltip = _.map(value, function(model) { return model.toHeaderString(); }, this).join(', '); } return value; }, /** * Returns the filter definition for the value of this field. * * @return {Array} */ delegateBuildFilterDefinition: function() { var collection; if (!this.model) { return []; } collection = this.model.get(this.name); if (!collection) { return []; } // Return all of the models in the collection in their current state so // that the _link and parent_name/email_address attributes are stored // in the saved filter definition. This guarantees that the pills will // be displayed correctly when a saved or cached filter is loaded. return collection.map(function(model) { return model.attributes; }); }, /** * Decorates recipients that need it. * * @private */ _decorateRecipients: function() { this._decorateOptedOutRecipients(); this._decorateInvalidRecipients(); }, /** * Decorate any invalid recipients. * * @private */ _decorateInvalidRecipients: function() { var self = this; var $invalidRecipients = this.$('.select2-search-choice [data-invalid="true"]'); if (this.def.decorate_invalid === false) { return; } $invalidRecipients.each(function() { var $choice = $(this).closest('.select2-search-choice'); $choice.addClass('select2-choice-danger'); // Don't change the tooltip if the email address has been erased. if (!$(this).data('email-is-erased')) { $(this).attr('data-title', app.lang.get('ERR_INVALID_EMAIL_ADDRESS', self.module)); } }); }, /** * Decorate any opted out email addresses. * * Email addresses that are opted out and invalid are not decorated by this * method. This preserves the invalid recipient decoration, since users * will need that decoration to correct their email before saving or * sending. * * @private */ _decorateOptedOutRecipients: function() { var self = this; var $optedOutRecipients = this.$('.select2-search-choice [data-optout="true"]:not([data-invalid="true"])'); if (this.def.decorate_opt_out === false) { return; } $optedOutRecipients.each(function() { var $choice = $(this).closest('.select2-search-choice'); $choice.addClass('select2-choice-optout'); $(this).attr('data-title', app.lang.get('LBL_EMAIL_ADDRESS_OPTED_OUT', self.module, { email_address: $choice.data('select2Data').get('email_address') })); }); }, /** * Enable the user to drag and drop recipients between recipient fields. * * @private */ _enableDragDrop: function() { var $el = this.$(this.fieldTag); if (!this.def.readonly) { this.setDragDropPluginEvents($el); } }, /** * When in edit mode, the field includes an icon button for opening an * address book. Clicking the button will trigger an event to open the * address book, which calls this method does. The selected recipients are * added to this field upon closing the address book. * * @private */ _showAddressBook: function() { app.drawer.open( { layout: 'compose-addressbook', context: { module: 'Emails', mixed: true } }, _.bind(function(recipients) { if (recipients && recipients.length > 0) { // Set the correct link for the field where these // recipients are being added. var eps = recipients.map(function(recipient) { recipient.set('_link', this.getLinkName()); return recipient; }, this); this.model.get(this.name).add(eps); } this.view.trigger('address-book-state', 'closed'); }, this) ); this.view.trigger('address-book-state', 'open'); }, /** * Moves the recipients to the target collection. * * @param {Data.BeanCollection} source The collection from which the * recipients are removed. * @param {Data.BeanCollection} target The collection to which the * items are added. * @param {Array} draggedItems The recipients that are to be removed from * the source collection. * @param {Array} droppedItems The recipients that are to be added to * the target collection. */ dropDraggedItems: function(source, target, draggedItems, droppedItems) { source.remove(draggedItems); _.each(droppedItems, function(item) { // The `id` must be unset because we're effectively creating // a brand new model to be linked. item.unset('id'); item.set('_link', this.getLinkName()); }, this); target.add(droppedItems); } }) }, "emailaction-paneltop": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Emails.EmailactionPaneltopField * @alias SUGAR.App.view.fields.BaseEmailsEmailactionPaneltopField * @extends View.Fields.Base.EmailactionField */ ({ // Emailaction-paneltop FieldTemplate (base) extendsFrom: 'EmailactionField', /** * @inheritdoc * Set type to emailaction to get the template */ initialize: function(options) { this._super("initialize", [options]); this.type = 'emailaction'; this.on('emailclient:close', this.handleEmailClientClose, this); }, /** * When email compose is done, refresh the data in the Emails subpanel */ handleEmailClientClose: function() { var context = this.context.parent || this.context; var links = app.utils.getLinksBetweenModules(context.get('module'), this.module); _.each(links, function(link) { context.trigger('panel-top:refresh', link.name); }); app.events.trigger('link:added', this.context.get('parentModel')); }, /** * No additional options are needed from the element in order to launch the * email client. * * @param {jQuery} [$link] The element from which to get options. * @return {Object} * @private * @deprecated Use * View.Fields.Base.Emails.EmailactionPaneltopField#emailOptionTo and * View.Fields.Base.Emails.EmailactionPaneltopField#emailOptionRelated * instead. */ _retrieveEmailOptionsFromLink: function($link) { app.logger.warn('View.Fields.Base.Emails.EmailactionPaneltopField#_retrieveEmailOptionsFromLink is ' + 'deprecated. Use View.Fields.Base.Emails.EmailactionPaneltopField#emailOptionTo and ' + 'View.Fields.Base.Emails.EmailactionPaneltopField#emailOptionRelated instead.'); return {}; }, /** * Returns the recipients to use in the To field of the email. If * `this.def.set_recipient_to_parent` is true, then the model is added to * the email's To field. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model Use this model when identifying the recipients. * @return {undefined|Array} */ emailOptionTo: function(model) { if (this.def.set_recipient_to_parent) { return [{ bean: model }]; } }, /** * Returns the bean to use as the email's related record. If * `this.def.set_related_to_parent` is true, then the model is used. * * @see EmailClientLaunch plugin. * @param {Data.Bean} model This model's parent is used as the email's * related record. * @return {undefined|Data.Bean} */ emailOptionRelated: function(model) { if (this.def.set_related_to_parent) { return model; } } }) } }} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.RecordlistView * @alias SUGAR.App.view.views.BaseEmailsRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc * When record name is empty, return (no subject) */ _getNameForMessage: function(model) { var name = this._super('_getNameForMessage', [model]); if (_.isEmpty(name)) { return app.lang.get('LBL_NO_SUBJECT', this.module); } return name; } }) }, "archive-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ArchiveEmailView * @alias SUGAR.App.view.views.BaseEmailsArchiveEmailView * @extends View.Views.Base.Emails.ComposeView * @deprecated Use {@link View.Views.Base.Emails.CreateView} instead. */ ({ // Archive-email View (base) extendsFrom: 'EmailsComposeView', /** * @inheritdoc * * Add click event handler to archive an email. */ initialize: function(options) { app.logger.warn( 'View.Views.Base.Emails.ArchiveEmailView is deprecated. Use View.Views.Base.Emails.CreateView instead.' ); this.events = _.extend({}, this.events, { 'click [name=archive_button]': 'archive' }); this._super('initialize', [options]); if (!this.model.has('assigned_user_id')) { this.model.set('assigned_user_id', app.user.id); this.model.set('assigned_user_name', app.user.get('full_name')); } }, /** * Set headerpane title. * @private */ _render: function() { var $controls; this._super('_render'); $controls = this.$('.control-group:not(.hide) .control-label'); if ($controls.length) { $controls.last().addClass('end-fieldgroup'); } this.setTitle(app.lang.get('LBL_ARCHIVE_EMAIL', this.module)); }, /** * Archive email if validation passes. */ archive: function(event) { this.setMainButtonsDisabled(true); this.model.doValidate(this.getFieldsToValidate(), _.bind(function(isValid) { if (isValid) { this.archiveEmail(); } else { this.setMainButtonsDisabled(false); } }, this)); }, /** * Get fields that needs to be validated. * @return {Object} */ getFieldsToValidate: function() { var fields = {}; _.each(this.fields, function(field) { fields[field.name] = field.def; }); return fields; }, /** * Call archive api. */ archiveEmail: function() { var archiveUrl = app.api.buildURL('Mail/archive'); var alertKey = 'mail_archive'; var archiveEmailModel = this.initializeSendEmailModel(); app.alert.show(alertKey, {level: 'process', title: app.lang.get('LBL_EMAIL_ARCHIVING', this.module)}); app.api.call('create', archiveUrl, archiveEmailModel, { success: _.bind(function() { app.alert.dismiss(alertKey); app.alert.show(alertKey, { autoClose: true, level: 'success', messages: app.lang.get('LBL_EMAIL_ARCHIVED', this.module) }); app.drawer.close(this.model); }, this), error: function(error) { var msg = {level: 'error'}; if (error && _.isString(error.message)) { msg.messages = error.message; } app.alert.dismiss(alertKey); app.alert.show(alertKey, msg); }, complete: _.bind(function() { if (!this.disposed) { this.setMainButtonsDisabled(false); } }, this) }); }, /** * @inheritdoc */ initializeSendEmailModel: function() { var model = this._super('initializeSendEmailModel'); model.set({ 'date_sent': this.model.get('date_sent'), 'from_address': this.model.get('from_address'), 'status': 'archive', 'state': 'Archived' }); return model; }, /** * Disable/enable archive button. * @param {boolean} disabled */ setMainButtonsDisabled: function(disabled) { this.getField('archive_button').setDisabled(disabled); }, /** * No need to warn of configuration status for archive email because no * email is being sent. */ notifyConfigurationStatus: $.noop }) }, "compose-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeEmailView * @alias SUGAR.App.view.views.BaseEmailsComposeEmailView * @extends View.Views.Base.Emails.CreateView */ ({ // Compose-email View (base) extendsFrom: 'EmailsCreateView', /** * Constant representing the state of an email when it is a draft. * * @property {string} */ STATE_DRAFT: 'Draft', /** * Constant representing the state of an email when it is ready to be sent. * * @property {string} */ STATE_READY: 'Ready', /** * The name of the send button. * * @property {string} */ sendButtonName: 'send_button', /** * Used for determining if an email's content contains variables. * * @property {RegExp} */ _hasVariablesRegex: /\$[a-zA-Z]+_[a-zA-Z0-9_]+/, /** * False when the email client reports a configuration issue. * * @property {boolean} */ _userHasConfiguration: true, /** * The label to be used as the title of the page. * * @property {string} */ _titleLabel: 'LBL_COMPOSE_MODULE_NAME_SINGULAR', /** * @inheritdoc * * Disables the send button if email has not been configured. */ initialize: function(options) { var loadingRequests = 0; this._super('initialize', [options]); if (this.model.isNew()) { this.model.set('state', this.STATE_DRAFT); } this.on('email_not_configured', function() { var sendButton = this.getField('send_button'); if (sendButton) { sendButton.setDisabled(true); } this._userHasConfiguration = false; }, this); this.on('loading_collection_field', function() { loadingRequests++; this.toggleButtons(false); }, this); this.on('loaded_collection_field', function() { loadingRequests--; if (loadingRequests === 0) { this.toggleButtons(true); } }, this); }, /** * @inheritdoc * * Renders the recipients fieldset anytime there are changes to the `to`, * `cc`, or `bcc` fields. * * Disables the send button if the attachments exceed the * max_aggregate_email_attachments_bytes configuration. Enables the send * button if the attachments are under the * max_aggregate_email_attachments_bytes configuration configuration. */ bindDataChange: function() { var self = this; var renderRecipientsField = _.debounce(function() { var field = self.getField('recipients'); if (field) { field.render(); } }, 200); if (this.model) { this.listenTo( this.model, 'change:to_collection change:cc_collection change:bcc_collection', renderRecipientsField ); this.listenTo(this.model, 'attachments_collection:over_max_total_bytes', function() { var sendButton = this.getField(this.sendButtonName); if (sendButton) { sendButton.setDisabled(true); } }); this.listenTo(this.model, 'attachments_collection:under_max_total_bytes', function() { var sendButton = this.getField(this.sendButtonName); if (sendButton) { sendButton.setDisabled(!this._userHasConfiguration); } }); } this._super('bindDataChange'); }, /** * @inheritdoc * * Registers a handler to send the email when the send button is clicked. */ delegateButtonEvents: function() { this._super('delegateButtonEvents'); this.listenTo(this.context, 'button:' + this.sendButtonName + ':click', function() { this.send(); }); }, /** * @inheritdoc * * The send button cannot be enabled if email is not configured for the * user. */ toggleButtons: function(enable) { this._super('toggleButtons', [enable]); if (enable && this.buttons[this.sendButtonName] && !this._userHasConfiguration) { this.buttons[this.sendButtonName].setDisabled(true); } }, /** * @inheritdoc * * Implements the Compose:Send shortcut to send the email. */ registerShortcuts: function() { this._super('registerShortcuts'); app.shortcuts.register({ id: 'Compose:Send', keys: ['mod+shift+s'], component: this, description: 'LBL_SHORTCUT_EMAIL_SEND', callOnFocus: true, handler: function() { var $sendButton = this.$('a[name=' + this.sendButtonName + ']'); if ($sendButton.is(':visible') && !$sendButton.hasClass('disabled')) { $sendButton.get(0).click(); } } }); }, /** * @inheritdoc * * `BaseEmailsCreateView` is used when creating new emails and editing * existing drafts. The model is not new when editing drafts. In those * cases, {@link BaseEmailsRecordView#hasUnsavedChanges} is called to use * logic that checks for unsaved changes for existing records instead of * new records. */ hasUnsavedChanges: function() { if (this.model.isNew()) { return this._super('hasUnsavedChanges'); } return app.view.views.BaseEmailsRecordView.prototype.hasUnsavedChanges.call(this); }, /** * Sends the email. * * Warns the user if the subject and/or body are empty. The user may still * send the email after confirming. * * Alerts the user if the email does not have any recipients. */ send: function() { var confirmationMessages = []; var subject = this.model.get('name') || ''; var text = this.model.get('description') || ''; var html = this.model.get('description_html') || ''; var fullContent = subject + ' ' + text + ' ' + html; var isSubjectEmpty = _.isEmpty(subject.trim()); // When fetching tinyMCE content, convert to jQuery Object // and return only if text is not empty. By wrapping the value // in <div> tags we remove the error if the value contains // no HTML markup var isContentEmpty = _.isEmpty($('<div>' + html + '</div>').text().trim()); var sendEmail = _.bind(function() { this.model.set('state', this.STATE_READY); this.save(); }, this); this.disableButtons(); if (this.model.get('to_collection').length === 0 && this.model.get('cc_collection').length === 0 && this.model.get('bcc_collection').length === 0 ) { this.model.trigger('error:validation:to_collection'); app.alert.show('send_error', { level: 'error', messages: 'LBL_EMAIL_COMPOSE_ERR_NO_RECIPIENTS' }); this.enableButtons(); } else { // to/cc/bcc filled out, check other fields if (isSubjectEmpty && isContentEmpty) { confirmationMessages.push(app.lang.get('LBL_NO_SUBJECT_NO_BODY_SEND_ANYWAYS', this.module)); } else if (isSubjectEmpty) { confirmationMessages.push(app.lang.get('LBL_SEND_ANYWAYS', this.module)); } else if (isContentEmpty) { confirmationMessages.push(app.lang.get('LBL_NO_BODY_SEND_ANYWAYS', this.module)); } if (_.isEmptyValue(this.model.get('parent_id')) && this._hasVariablesRegex.test(fullContent)) { confirmationMessages.push(app.lang.get('LBL_NO_RELATED_TO_WITH_TEMPLATE_SEND_ANYWAYS', this.module)); } if (confirmationMessages.length > 0) { app.alert.show('send_confirmation', { level: 'confirmation', messages: confirmationMessages.join('<br />'), onConfirm: sendEmail, onCancel: _.bind(this.enableButtons, this) }); } else { // All checks pass, send the email sendEmail(); } } }, /** * @inheritdoc * * Builds the appropriate success message based on the state of the email. */ buildSuccessMessage: function() { var successLabel = this.model.get('state') === this.STATE_DRAFT ? 'LBL_DRAFT_SAVED' : 'LBL_EMAIL_SENT'; return app.lang.get(successLabel, this.module); } }) }, "compose-addressbook-recipientscontainer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeAddressbookRecipientscontainerView * @alias SUGAR.App.view.views.BaseEmailsComposeAddressbookRecipientscontainerView * @extends View.Views.Base.RecordView */ ({ // Compose-addressbook-recipientscontainer View (base) extendsFrom: 'RecordView', enableHeaderButtons: false, enableHeaderPane: false, events: {}, /** * Override to remove unwanted functionality. * * @param prefill */ setupDuplicateFields: function(prefill) {}, /** * Override to remove unwanted functionality. */ delegateButtonEvents: function() {}, /** * Override to remove unwanted functionality. */ _initButtons: function() { this.buttons = {}; }, /** * Override to remove unwanted functionality. */ showPreviousNextBtnGroup: function() {}, /** * Override to remove unwanted functionality. */ bindDataChange: function() {}, /** * Override to remove unwanted functionality. * * @param isEdit */ toggleHeaderLabels: function(isEdit) {}, /** * Override to remove unwanted functionality. * * @param field */ toggleLabelByField: function(field) {}, /** * Override to remove unwanted functionality. * * @param e * @param field */ handleKeyDown: function(e, field) {}, /** * Override to remove unwanted functionality. * * @param state */ setButtonStates: function(state) {}, /** * Override to remove unwanted functionality. * * @param title */ setTitle: function(title) {} }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.RecordView * @alias SUGAR.App.view.views.BaseEmailsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * Constant representing the state of an email when it is a draft. * * @property {string} */ STATE_DRAFT: 'Draft', /** * @inheritdoc * * Alerts the user if the email is a draft, so that user can switch to * composing the email instead of simply viewing it. */ initialize: function(options) { var loadingRequests = 0; this._super('initialize', [options]); this._alertUserDraftState(); this.on('loading_collection_field', function() { loadingRequests++; this.toggleButtons(false); }, this); this.on('loaded_collection_field', function() { loadingRequests--; if (loadingRequests === 0) { this.toggleButtons(true); } }, this); }, /** * @inheritdoc * * Alerts the user if the email becomes a draft -- most likely due to * asynchronous data patching -- so that user can switch to composing the * email instead of simply viewing it. * * Renders the recipients fieldset anytime there are changes to the From, * To, CC, or BCC fields. */ bindDataChange: function() { var self = this; /** * Render the specified recipients field. * * @param {string} fieldName */ function renderRecipientsField(fieldName) { var field = self.getField(fieldName); if (field) { field.render(); } } if (this.model) { this.listenTo(this.model, 'change:state', function() { this._alertUserDraftState(); this.setButtonStates(this.getCurrentButtonState()); }); this.listenTo(this.model, 'change:from_collection', function() { renderRecipientsField('from_collection'); }); this.listenTo(this.model, 'change:to_collection', function() { renderRecipientsField('to_collection'); }); this.listenTo(this.model, 'change:cc_collection', function() { renderRecipientsField('cc_collection'); }); this.listenTo(this.model, 'change:bcc_collection', function() { renderRecipientsField('bcc_collection'); }); } this._super('bindDataChange'); }, /** * @inheritdoc * * Adds the view parameter. It must be added to `options.params` because * the `options.view` is only added as a parameter if the request method is * "read". */ getCustomSaveOptions: function(options) { options = options || {}; options.params = options.params || {}; options.params.view = this.name; return options; }, /** * @inheritdoc * * Switches to the email compose route if the email is a draft. */ editClicked: function() { if (this._isEditableDraft()) { this._navigateToEmailCompose(); } else { this._super('editClicked'); } }, /** * @inheritdoc * * Hides the Forward, Reply, and Reply All buttons if the email is a draft. */ setButtonStates: function(state) { var buttons; var buttonsToHideOnDrafts; /** * Some buttons contain other buttons, like ActiondropdownField. This * function recursively finds all buttons starting at the root. * * @param {Array} allButtons The set of buttons that have been found so * far. Begin with an empty array to prime the set. * @param {Object|Array} root A collection of button fields. * @return {Array} */ function getAllButtons(allButtons, root) { var nestedButtons = _.flatten(_.compact(_.pluck(root, 'fields'))); if (nestedButtons.length > 0) { allButtons = allButtons.concat(nestedButtons); allButtons = getAllButtons(allButtons, nestedButtons); } return allButtons; } this._super('setButtonStates', [state]); if (this.model.get('state') === this.STATE_DRAFT) { buttons = getAllButtons([], this.buttons); buttonsToHideOnDrafts = _.filter(buttons, function(field) { return _.contains(['reply_button', 'reply_all_button', 'forward_button'], field.name); }); _.each(buttonsToHideOnDrafts, function(field) { field.hide(); }); } }, /** * Alerts the user if a draft was opened in the record view, so the user * can switch to composing the email instead of simply viewing it. * * @private */ _alertUserDraftState: function() { app.alert.dismiss('email-draft-alert'); if (this._isEditableDraft()) { app.alert.show('email-draft-alert', { level: 'warning', autoClose: false, messages: app.lang.get('LBL_OPEN_DRAFT_ALERT', this.module, {subject: this.model.get('name')}), onLinkClick: _.bind(function(event) { app.alert.dismiss('email-draft-alert'); this._navigateToEmailCompose(); }, this) }); } }, /** * @inheritdoc * * @return {string} Returns (no subject) when the record name is empty. */ _getNameForMessage: function(model) { var name = this._super('_getNameForMessage', [model]); if (_.isEmpty(name)) { return app.lang.get('LBL_NO_SUBJECT', this.module); } return name; }, /** * Determines the email is a draft and the user can edit it. * * @return {boolean} * @private */ _isEditableDraft: function() { return this.model.get('state') === this.STATE_DRAFT && app.acl.hasAccessToModel('edit', this.model); }, /** * Switches to the email compose route for the email. This method should * only be used if the email is a draft. * * @private */ _navigateToEmailCompose: function() { var route; if (this._isEditableDraft()) { route = '#' + app.router.buildRoute(this.model.module, this.model.get('id'), 'compose'); app.router.navigate(route, {trigger: true}); } } }) }, "compose-addressbook-filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeAddressbookFilterView * @alias SUGAR.App.view.views.BaseEmailsComposeAddressbookFilterView * @extends View.View */ ({ // Compose-addressbook-filter View (base) _moduleFilterList: [], _allModulesId: 'All', _selectedModule: null, _currentSearch: '', events: { 'keyup .search-name': 'throttledSearch', 'paste .search-name': 'throttledSearch', 'click .add-on.sicon-close': 'clearInput' }, /** * Converts the input field to a select2 field and adds the module filter for refining the search. * * @private */ _render: function() { app.view.View.prototype._render.call(this); this.buildModuleFilterList(); this.buildFilter(); }, /** * Builds the list of allowed modules to provide the data to the select2 field. */ buildModuleFilterList: function() { var allowedModules = this.collection.allowed_modules; this._moduleFilterList = [ {id: this._allModulesId, text: app.lang.get('LBL_MODULE_ALL')} ]; _.each(allowedModules, function(module) { this._moduleFilterList.push({ id: module, text: app.lang.getModuleName(module, {plural: true}) }); }, this); }, /** * Converts the input field to a select2 field and initializes the selected module. */ buildFilter: function() { var $filter = this.getFilterField(); if ($filter.length > 0) { $filter.select2({ data: this._moduleFilterList, allowClear: false, multiple: false, minimumResultsForSearch: -1, formatSelection: _.bind(this.formatModuleSelection, this), formatResult: _.bind(this.formatModuleChoice, this), dropdownCss: {width: 'auto'}, dropdownCssClass: 'search-filter-dropdown', initSelection: _.bind(this.initSelection, this), escapeMarkup: function(m) { return m; }, width: 'off' }); $filter.off('change'); $filter.on('change', _.bind(this.handleModuleSelection, this)); this._selectedModule = this._selectedModule || this._allModulesId; $filter.select2('val', this._selectedModule); } }, /** * Gets the filter DOM field. * * @return {jQuery} DOM Element */ getFilterField: function() { return this.$('input.select2'); }, /** * Gets the module filter DOM field. * * @return {jQuery} DOM Element */ getModuleFilter: function() { return this.$('span.choice-filter-label'); }, /** * Destroy the select2 plugin. */ unbind: function() { $filter = this.getFilterField(); if ($filter.length > 0) { $filter.off(); $filter.select2('destroy'); } this._super('unbind'); }, /** * Performs a search once the user has entered a term. */ throttledSearch: _.debounce(function(evt) { var newSearch = this.$(evt.currentTarget).val(); if (this._currentSearch !== newSearch) { this._currentSearch = newSearch; this.applyFilter(); } }, 400), /** * Initialize the module selection with the value for all modules. * * @param {jQuery} el * @param {Function} callback */ initSelection: function(el, callback) { if (el.is(this.getFilterField())) { var module = _.findWhere(this._moduleFilterList, {id: el.val()}); callback({id: module.id, text: module.text}); } }, /** * Format the selected module to display its name. * * @param {Object} item * @return {String} */ formatModuleSelection: function(item) { // update the text for the selected module this.getModuleFilter().text(item.text); return '<span class="select2-choice-type">' + app.lang.get('LBL_MODULE') + '<i class="sicon sicon-chevron-down"></i></span>'; }, /** * Format the choices in the module select box. * * @param {Object} option * @return {String} */ formatModuleChoice: function(option) { return '<div><span class="select2-match"></span>' + option.text + '</div>'; }, /** * Handler for when the module filter dropdown value changes, either via a click or manually calling jQuery's * .trigger("change") event. * * @param {Object} evt jQuery Change Event Object * @param {string} overrideVal (optional) ID passed in when manually changing the filter dropdown value */ handleModuleSelection: function(evt, overrideVal) { var module = overrideVal || evt.val || this._selectedModule || this._allModulesId; // only perform a search if the module is in the approved list if (!_.isEmpty(_.findWhere(this._moduleFilterList, {id: module}))) { this._selectedModule = module; this.getFilterField().select2('val', this._selectedModule); this.getModuleFilter().css('cursor', 'pointer'); this.applyFilter(); } }, /** * Triggers an event that makes a call to search the address book and filter the data set. */ applyFilter: function() { var searchAllModules = (this._selectedModule === this._allModulesId), // pass an empty array when all modules are being searched module = searchAllModules ? [] : [this._selectedModule], // determine if the filter is dirty so the "clearQuickSearchIcon" can be added/removed appropriately isDirty = !_.isEmpty(this._currentSearch); this._toggleClearQuickSearchIcon(isDirty); this.context.trigger('compose:addressbook:search', module, this._currentSearch); }, /** * Append or remove an icon to the quicksearch input so the user can clear the search easily. * @param {Boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.add-on.sicon-close')[0]) { this.$('.filter-view.search').append('<i class="add-on sicon sicon-close"></i>'); } else if (!addIt) { this.$('.add-on.sicon-close').remove(); } }, /** * Clear input */ clearInput: function() { var $filter = this.getFilterField(); this._currentSearch = ''; this._selectedModule = this._allModulesId; this.$('.search-name').val(this._currentSearch); if ($filter.length > 0) { $filter.select2('val', this._selectedModule); } this.applyFilter(); } }) }, "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ActivityCardContentView * @alias SUGAR.App.view.views.BaseEmailsActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.initAttachmentDetails('attachments_collection'); this.listenTo(this, 'tinymce:resize', this.toggleShowMore); }, /** * Initializes hbs date variables with date_modified */ initDateDetails: function() { if (!this.activity) { return; } const state = this.activity.get('state'); const activityCard = this.getActivityCardLayout(); let detailDate = app.date(activityCard.getCreatedDate(state)); let dateModified = app.date(this.activity.get('date_modified')); if (detailDate.isValid() && dateModified.isValid()) { detailDate = detailDate.formatUser(); dateModified = dateModified.formatUser(); if (detailDate !== dateModified) { this.dateModified = dateModified; } } }, }) }, "compose-addressbook-list-bottom": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeAddressbookListBottomView * @alias SUGAR.App.view.views.BaseEmailsComposeAddressbookListBottomView * @extends View.Views.Base.ListBottomView */ ({ // Compose-addressbook-list-bottom View (base) extendsFrom: 'ListBottomView', /** * Assign proper label for 'show more' link. * Label should be "More recipients...". */ setShowMoreLabel: function() { this.showMoreLabel = app.lang.get('LBL_SHOW_MORE_RECIPIENTS', this.module); } }) }, "compose-addressbook-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeAddressbookHeaderpaneView * @alias SUGAR.App.view.views.BaseEmailsComposeAddressbookHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // Compose-addressbook-headerpane View (base) extendsFrom: 'HeaderpaneView', events: { 'click [name=done_button]': '_done', 'click [name=cancel_button]': '_cancel' }, /** * The user clicked the Done button so trigger an event to add selected recipients from the address book to the * target field and then close the drawer. * * @private */ _done: function() { var recipients = this.model.get('to_collection'); if (recipients) { app.drawer.close(recipients); } else { this._cancel(); } }, /** * Close the drawer. * * @private */ _cancel: function() { app.drawer.close(); } }) }, "compose": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeView * @alias SUGAR.App.view.views.BaseEmailsComposeView * @extends View.Views.Base.RecordView * @deprecated Use {@link View.Views.Base.Emails.ComposeEmailView} instead. */ ({ // Compose View (base) extendsFrom: 'RecordView', _lastSelectedSignature: null, ATTACH_TYPE_SUGAR_DOCUMENT: 'document', ATTACH_TYPE_TEMPLATE: 'template', MIN_EDITOR_HEIGHT: 300, EDITOR_RESIZE_PADDING: 5, ATTACHMENT_FIELD_HEIGHT: 44, FIELD_PANEL_BODY_SELECTOR: '.row-fluid.panel_body', sendButtonName: 'send_button', cancelButtonName: 'cancel_button', saveAsDraftButtonName: 'draft_button', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Views.Base.Emails.ComposeView is deprecated. Use ' + 'View.Views.Base.Emails.ComposeEmailView instead.'); this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [data-toggle-field]': '_handleSenderOptionClick' }); this.context.on('email_attachments:file', this.launchFilePicker, this); this.context.on('email_attachments:document', this.documentDrawerCallback, this); this.context.on('attachments:updated', this.toggleAttachmentVisibility, this); this.context.on('tinymce:oninit', this.handleTinyMceInit, this); this.on('more-less:toggled', this.handleMoreLessToggled, this); app.drawer.on('drawer:resize', this.resizeEditor, this); this._lastSelectedSignature = app.user.getPreference('signature_default'); }, /** * @inheritdoc */ delegateButtonEvents: function() { this.context.on('button:' + this.sendButtonName + ':click', this.send, this); this.context.on('button:' + this.saveAsDraftButtonName + ':click', this.saveAsDraft, this); this.context.on('button:' + this.cancelButtonName + ':click', this.cancel, this); }, /** * @inheritdoc */ _render: function() { var prepopulateValues; var $controls; this._super('_render'); $controls = this.$('.control-group:not(.hide) .control-label'); if ($controls.length) { $controls.first().addClass('begin-fieldgroup'); $controls.last().addClass('end-fieldgroup'); } this.setTitle(app.lang.get('LBL_COMPOSEEMAIL', this.module)); prepopulateValues = this.context.get('prepopulate'); if (!_.isEmpty(prepopulateValues)) { this.prepopulate(prepopulateValues); } this.addSenderOptions(); this.notifyConfigurationStatus(); }, /** * Notifies the user of configuration issues and disables send button */ notifyConfigurationStatus: function() { var sendButton, emailClientPrefence = app.user.getPreference('email_client_preference'); if (_.isObject(emailClientPrefence) && _.isObject(emailClientPrefence.error)) { app.alert.show('email-client-status', { level: 'warning', messages: app.lang.get(emailClientPrefence.error.message, this.module), autoClose: false, onLinkClick: function() { app.alert.dismiss('email-client-status'); } }); sendButton = this.getField('send_button'); if (sendButton) { sendButton.setDisabled(true); } } }, /** * Prepopulate fields on the email compose screen that are passed in on the context when opening this view * TODO: Refactor once we have custom module specific models * @param {Object} values */ prepopulate: function(values) { var self = this; _.defer(function() { _.each(values, function(value, fieldName) { switch (fieldName) { case 'related': self._populateForModules(value); self.populateRelated(value); break; default: self.model.set(fieldName, value); } }); }); }, /** * Populates email compose with module specific data. * TODO: Refactor once we have custom module specific models * @param {Data.Bean} relatedModel */ _populateForModules: function(relatedModel) { if (relatedModel.module === 'Cases') { this._populateForCases(relatedModel); } }, /** * Populates email compose with cases specific data. * TODO: Refactor once we have custom module specific models * @param {Data.Bean} relatedModel */ _populateForCases: function(relatedModel) { var config = app.metadata.getConfig(), keyMacro = '%1', caseMacro = config.inboundEmailCaseSubjectMacro, subject = caseMacro + ' ' + relatedModel.get('name'); subject = subject.replace(keyMacro, relatedModel.get('case_number')); this.model.set('name', subject); if (!this.isFieldPopulated('to_addresses')) { // no addresses, attempt to populate from contacts relationship var contacts = relatedModel.getRelatedCollection('contacts'); contacts.fetch({ relate: true, success: _.bind(function(data) { var toAddresses = _.map(data.models, function(model) { return {bean: model}; }, this); this.model.set('to_addresses', toAddresses); }, this), fields: ['id', 'full_name', 'email'] }); } }, /** * Populate the parent_name (type: parent) with the related record passed in * * @param {Data.Bean} relatedModel */ populateRelated: function(relatedModel) { var setParent = _.bind(function(model) { var parentNameField = this.getField('parent_name'); if (model.module && parentNameField.isAvailableParentType(model.module)) { model.value = model.get('name'); parentNameField.setValue(model); } }, this); if (!_.isEmpty(relatedModel.get('id')) && !_.isEmpty(relatedModel.get('name'))) { setParent(relatedModel); } else if (!_.isEmpty(relatedModel.get('id'))) { relatedModel.fetch({ showAlerts: false, success: _.bind(function(relatedModel) { setParent(relatedModel); }, this), fields: ['name'] }); } }, /** * Enable/disable the page action dropdown menu based on whether email is sendable * @param {boolean} disabled */ setMainButtonsDisabled: function(disabled) { this.getField('main_dropdown').setDisabled(disabled); }, /** * Add Cc/Bcc toggle buttons * Initialize whether to show/hide fields and toggle show/hide buttons appropriately */ addSenderOptions: function() { this._renderSenderOptions('to_addresses'); this._initSenderOption('cc_addresses'); this._initSenderOption('bcc_addresses'); }, /** * Render the sender option buttons and place them in the given container * * @param {string} container Name of field that will contain the sender option buttons * @private */ _renderSenderOptions: function(container) { var field = this.getField(container), $panelBody, senderOptionTemplate; if (field) { $panelBody = field.$el.closest(this.FIELD_PANEL_BODY_SELECTOR); senderOptionTemplate = app.template.getView('compose-senderoptions', this.module); $(senderOptionTemplate({'module' : this.module})) .insertAfter($panelBody.find('div span.normal')); } }, /** * Check if the given field has a value * Hide the field if there is no value prepopulated * * @param {string} fieldName Name of the field to initialize active state on * @private */ _initSenderOption: function(fieldName) { var fieldValue = this.model.get(fieldName) || []; this.toggleSenderOption(fieldName, (fieldValue.length > 0)); }, /** * Toggle the state of the given field * Sets toggle button state and visibility of the field * * @param {string} fieldName Name of the field to toggle * @param {boolean} [active] Whether toggle button active and field shown */ toggleSenderOption: function(fieldName, active) { var toggleButtonSelector = '[data-toggle-field="' + fieldName + '"]', $toggleButton = this.$(toggleButtonSelector); // if explicit active state not set, toggle to opposite if (_.isUndefined(active)) { active = !$toggleButton.hasClass('active'); } $toggleButton.toggleClass('active', active); this._toggleFieldVisibility(fieldName, active); }, /** * Event Handler for toggling the Cc/Bcc options on the page. * * @param {Event} event click event * @private */ _handleSenderOptionClick: function(event) { var $toggleButton = $(event.currentTarget), fieldName = $toggleButton.data('toggle-field'); this.toggleSenderOption(fieldName); this.resizeEditor(); }, /** * Show/hide a field section on the form * * @param {string} fieldName Name of the field to show/hide * @param {boolean} show Whether to show or hide the field * @private */ _toggleFieldVisibility: function(fieldName, show) { var field = this.getField(fieldName); if (field) { field.$el.closest(this.FIELD_PANEL_BODY_SELECTOR).toggleClass('hide', !show); } }, /** * Cancel and close the drawer */ cancel: function() { app.drawer.close(); }, /** * Get the attachments from the model and format for the API * * @return {Array} array of attachments or empty array if none found */ getAttachmentsForApi: function() { var attachments = this.model.get('attachments') || []; if (!_.isArray(attachments)) { attachments = [attachments]; } return attachments; }, /** * Get the individual related object fields from the model and format for the API * * @return {Object} API related argument as array with appropriate fields set */ getRelatedForApi: function() { var related = {}; var id = this.model.get('parent_id'); var type; if (!_.isUndefined(id)) { id = id.toString(); if (id.length > 0) { related['id'] = id; type = this.model.get('parent_type'); if (!_.isUndefined(type)) { type = type.toString(); } related.type = type; } } return related; }, /** * Get the team information from the model and format for the API * * @return {Object} API teams argument as array with appropriate fields set */ getTeamsForApi: function() { var teamName = this.model.get('team_name') || []; var teams = {}; teams.others = []; if (!_.isArray(teamName)) { teamName = [teamName]; } _.each(teamName, function(team) { if (team.primary) { teams.primary = team.id.toString(); } else if (!_.isUndefined(team.id)) { teams.others.push(team.id.toString()); } }, this); if (teams.others.length == 0) { delete teams.others; } return teams; }, /** * Build a backbone model that will be sent to the Mail API */ initializeSendEmailModel: function() { var sendModel = new Backbone.Model(_.extend({}, this.model.attributes, { to_addresses: this.model.get('to_addresses'), cc_addresses: this.model.get('cc_addresses'), bcc_addresses: this.model.get('bcc_addresses'), subject: this.model.get('name'), html_body: this.model.get('description_html'), attachments: this.getAttachmentsForApi(), related: this.getRelatedForApi(), teams: this.getTeamsForApi() })); return sendModel; }, /** * Save the email as a draft for later sending */ saveAsDraft: function() { this.saveModel( 'draft', app.lang.get('LBL_DRAFT_SAVING', this.module), app.lang.get('LBL_DRAFT_SAVED', this.module), app.lang.get('LBL_ERROR_SAVING_DRAFT', this.module) ); }, /** * Send the email immediately or warn if user did not provide subject or body */ send: function() { var sendEmail = _.bind(function() { this.saveModel( 'ready', app.lang.get('LBL_EMAIL_SENDING', this.module), app.lang.get('LBL_EMAIL_SENT', this.module), app.lang.get('LBL_ERROR_SENDING_EMAIL', this.module) ); }, this); if (!this.isFieldPopulated('to_addresses') && !this.isFieldPopulated('cc_addresses') && !this.isFieldPopulated('bcc_addresses') ) { this.model.trigger('error:validation:to_addresses'); app.alert.show('send_error', { level: 'error', messages: 'LBL_EMAIL_COMPOSE_ERR_NO_RECIPIENTS' }); } else if (!this.isFieldPopulated('name') && !this.isFieldPopulated('description_html')) { app.alert.show('send_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_NO_SUBJECT_NO_BODY_SEND_ANYWAYS', this.module), onConfirm: sendEmail }); } else if (!this.isFieldPopulated('name')) { app.alert.show('send_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_SEND_ANYWAYS', this.module), onConfirm: sendEmail }); } else if (!this.isFieldPopulated('description_html')) { app.alert.show('send_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_NO_BODY_SEND_ANYWAYS', this.module), onConfirm: sendEmail }); } else { sendEmail(); } }, /** * Build the backbone model to be sent to the Mail API with the appropriate status * Also display the appropriate alerts to give user indication of what is happening. * * @param {string} status (draft or ready) * @param {string} pendingMessage message to display while Mail API is being called * @param {string} successMessage message to display when a successful Mail API response has been received * @param {string} errorMessage message to display when Mail API call fails */ saveModel: function(status, pendingMessage, successMessage, errorMessage) { var myURL, sendModel = this.initializeSendEmailModel(); if (this._hasInvalidRecipients(sendModel)) { app.alert.show('mail_invalid_recipients', { level: 'error', messages: app.lang.get('ERR_INVALID_RECIPIENTS', this.module) }); this.setMainButtonsDisabled(false); return; } this.setMainButtonsDisabled(true); app.alert.show('mail_call_status', {level: 'process', title: pendingMessage}); sendModel.set('status', status); myURL = app.api.buildURL('Mail'); app.api.call('create', myURL, sendModel, { success: function() { app.alert.dismiss('mail_call_status'); app.alert.show('mail_call_status', {autoClose: true, level: 'success', messages: successMessage}); app.drawer.close(sendModel); }, error: function(error) { var msg = {level: 'error'}; if (error && _.isString(error.message)) { msg.messages = error.message; } app.alert.dismiss('mail_call_status'); app.alert.show('mail_call_status', msg); }, complete: _.bind(function() { if (!this.disposed) { this.setMainButtonsDisabled(false); } }, this) }); }, /** * Is this field populated? * @param {string} fieldName * @return {boolean} */ isFieldPopulated: function(fieldName) { var value = this.model.get(fieldName) || ''; if (value instanceof Backbone.Collection) { return value.length !== 0; } else { return !_.isEmpty(value.trim()); } }, /** * Check if the recipients in any of the recipient fields are invalid. * * @param {Backbone.Model} model * @return {boolean} Return true if there are invalid recipients in any of * the fields. Return false otherwise. * @private */ _hasInvalidRecipients: function(model) { return _.some(['to_addresses', 'cc_addresses', 'bcc_addresses'], function(fieldName) { var recipients = model.get(fieldName); if (!recipients) { return false; } return _.some(recipients.models, function(recipient) { return recipient.get('_invalid'); }); }, this); }, /** * Open the drawer with the EmailTemplates selection list layout. The callback should take the data passed to it * and replace the existing editor contents with the selected template. */ launchTemplateDrawer: function() { app.drawer.open({ layout: 'selection-list', context: { module: 'EmailTemplates' } }, _.bind(this.templateDrawerCallback, this) ); }, /** * Receives the selected template to insert and begins the process of confirming the operation and inserting the * template into the editor. * * @param {Data.Bean} model */ templateDrawerCallback: function(model) { if (model) { var emailTemplate = app.data.createBean('EmailTemplates', { id: model.id }); emailTemplate.fetch({ success: _.bind(this.confirmTemplate, this), error: _.bind(function(model, error) { this._showServerError(error); }, this) }); } }, /** * Presents the user with a confirmation prompt indicating that inserting the template will replace all content * in the editor. If the user confirms "yes" then the template will inserted. * * @param {Data.Bean} template */ confirmTemplate: function(template) { if (this.disposed === true) return; //if view is already disposed, bail out app.alert.show('delete_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_EMAILTEMPLATE_MESSAGE_SHOW_MSG', this.module), onConfirm: _.bind(this.insertTemplate, this, template) }); }, /** * Inserts the template into the editor. * * @param {Data.Bean} template */ insertTemplate: function(template) { var subject, notes; if (_.isObject(template)) { subject = template.get('subject'); if (subject) { this.model.set('name', subject); } //TODO: May need to move over replaces special characters. if (template.get('text_only') === 1) { this.model.set('description_html', template.get('body')); } else { this.model.set('description_html', template.get('body_html')); } notes = app.data.createBeanCollection('Notes'); notes.fetch({ 'filter': { 'filter': [ //FIXME: email_type should be EmailTemplates {'email_id': {'$equals': template.id}} ] }, success: _.bind(function(data) { if (this.disposed === true) return; //if view is already disposed, bail out if (!_.isEmpty(data.models)) { this.insertTemplateAttachments(data.models); } }, this), error: _.bind(function(collection, error) { this._showServerError(error); }, this) }); // currently adds the html signature even when the template is text-only this._updateEditorWithSignature(this._lastSelectedSignature); } }, /** * Inserts attachments associated with the template by triggering an "add" event for each attachment to add to the * attachments field. * * @param {Array} attachments */ insertTemplateAttachments: function(attachments) { this.context.trigger('attachments:remove-by-tag', 'template'); _.each(attachments, function(attachment) { var filename = attachment.get('filename'); this.context.trigger('attachment:add', { id: attachment.id, name: filename, nameForDisplay: filename, tag: 'template', type: this.ATTACH_TYPE_TEMPLATE }); }, this); }, /** * Launch the file upload picker on the attachments field. */ launchFilePicker: function() { this.context.trigger('attachment:filepicker:launch'); }, /** * Open the drawer with the SugarDocuments attachment selection list layout. The callback should take the data * passed to it and add the document as an attachment. */ launchDocumentDrawer: function() { app.drawer.open({ layout: 'selection-list', context: {module: 'Documents'} }, _.bind(this.documentDrawerCallback, this) ); }, /** * Fetches the selected SugarDocument using its ID and triggers an "add" event to add the attachment to the * attachments field. * * @param {Data.Bean} model */ documentDrawerCallback: function(model) { if (model) { var sugarDocument = app.data.createBean('Documents', { id: model.id }); sugarDocument.fetch({ success: _.bind(function(model) { if (this.disposed === true) return; //if view is already disposed, bail out this.context.trigger('attachment:add', { id: model.id, name: model.get('filename'), nameForDisplay: model.get('filename'), type: this.ATTACH_TYPE_SUGAR_DOCUMENT }); }, this), error: _.bind(function(model, error) { this._showServerError(error); }, this) }); } }, /** * Hide attachment field row if no attachments, show when added * * @param {Array} attachments */ toggleAttachmentVisibility: function(attachments) { var $row = this.$('.attachments').closest('.row-fluid'); if (attachments.length > 0) { $row.removeClass('hidden'); $row.addClass('single'); } else { $row.addClass('hidden'); $row.removeClass('single'); } this.resizeEditor(); }, /** * Open the drawer with the signature selection layout. The callback should take the data passed to it and insert * the signature in the correct place. * * @private */ launchSignatureDrawer: function() { app.drawer.open( { layout: 'selection-list', context: { module: 'UserSignatures' } }, _.bind(this._updateEditorWithSignature, this) ); }, /** * Fetches the signature content using its ID and updates the editor with the content. * * @param {Data.Bean} model */ _updateEditorWithSignature: function(model) { if (model && model.id) { var signature = app.data.createBean('UserSignatures', { id: model.id }); signature.fetch({ success: _.bind(function(model) { if (this.disposed === true) return; //if view is already disposed, bail out if (this._insertSignature(model)) { this._lastSelectedSignature = model; } }, this), error: _.bind(function(model, error) { this._showServerError(error); }, this) }); } }, /** * Inserts the signature into the editor. * * @param {Data.Bean} signature * @return {Boolean} * @private */ _insertSignature: function(signature) { if (_.isObject(signature) && signature.get('signature_html')) { var signatureContent = this._formatSignature(signature.get('signature_html')), emailBody = this.model.get('description_html') || '', signatureOpenTag = '<br class="signature-begin" />', signatureCloseTag = '<br class="signature-end" />', signatureOpenTagForRegex = '(<br\ class=[\'"]signature\-begin[\'"].*?\/?>)', signatureCloseTagForRegex = '(<br\ class=[\'"]signature\-end[\'"].*?\/?>)', signatureOpenTagMatches = emailBody.match(new RegExp(signatureOpenTagForRegex, 'gi')), signatureCloseTagMatches = emailBody.match(new RegExp(signatureCloseTagForRegex, 'gi')), regex = new RegExp(signatureOpenTagForRegex + '[\\s\\S]*?' + signatureCloseTagForRegex, 'g'); if (signatureOpenTagMatches && !signatureCloseTagMatches) { // there is a signature, but no close tag; so the signature runs from open tag until EOF emailBody = this._insertSignatureTag(emailBody, signatureCloseTag, false); // append the close tag } else if (!signatureOpenTagMatches && signatureCloseTagMatches) { // there is a signature, but no open tag; so the signature runs from BOF until close tag emailBody = this._insertSignatureTag(emailBody, signatureOpenTag, true); // prepend the open tag } else if (!signatureOpenTagMatches && !signatureCloseTagMatches) { // there is no signature, so add the tag to the correct location emailBody = this._insertSignatureTag( emailBody, signatureOpenTag + signatureCloseTag, // insert both tags as one (app.user.getPreference('signature_prepend') == 'true')); } this.model.set('description_html', emailBody.replace(regex, '$1' + signatureContent + '$2')); return true; } return false; }, /** * Inserts a tag into the editor to surround the signature so the signature can be identified again. * * @param {string} body * @param {string} tag * @param {string} prepend * @return {string} * @private */ _insertSignatureTag: function(body, tag, prepend) { var preSignature = '', postSignature = ''; prepend = prepend || false; if (prepend) { var bodyOpenTag = '<body>', bodyOpenTagLoc = body.indexOf(bodyOpenTag); if (bodyOpenTagLoc > -1) { preSignature = body.substr(0, bodyOpenTagLoc + bodyOpenTag.length); postSignature = body.substr(bodyOpenTagLoc + bodyOpenTag.length, body.length); } else { postSignature = body; } } else { var bodyCloseTag = '</body>', bodyCloseTagLoc = body.indexOf(bodyCloseTag); if (bodyCloseTagLoc > -1) { preSignature = body.substr(0, bodyCloseTagLoc); postSignature = body.substr(bodyCloseTagLoc, body.length); } else { preSignature = body; } } return preSignature + tag + postSignature; }, /** * Formats HTML signatures to replace select HTML-entities with their true characters. * * @param {string} signature */ _formatSignature: function(signature) { signature = signature.replace(/</gi, '<'); signature = signature.replace(/>/gi, '>'); return signature; }, /** * Show a generic alert for server errors resulting from custom API calls during Email Compose workflows. Logs * the error message for system administrators as well. * * @param {SUGAR.HttpError} error * @private */ _showServerError: function(error) { app.alert.show('server-error', { level: 'error', messages: 'ERR_GENERIC_SERVER_ERROR' }); app.error.handleHttpError(error); }, /** * When toggling to show/hide hidden panel, resize editor accordingly */ handleMoreLessToggled: function() { this.resizeEditor(); }, /** * When TinyMCE has been completely initialized, go ahead and resize the editor */ handleTinyMceInit: function() { this.resizeEditor(); }, _dispose: function() { if (app.drawer) { app.drawer.off(null, null, this); } app.alert.dismiss('email-client-status'); this._super('_dispose'); }, /** * Register keyboard shortcuts. */ registerShortcuts: function() { app.shortcuts.register({ id: 'Compose:Action:More', keys: 'm', component: this, description: 'LBL_SHORTCUT_OPEN_MORE_ACTION', handler: function() { var $primaryDropdown = this.$('.btn-primary[data-bs-toggle=dropdown]'); if ($primaryDropdown.is(':visible') && !$primaryDropdown.hasClass('disabled')) { $primaryDropdown.click(); } } }); this._super('registerShortcuts'); }, /** * Resize the html editor based on height of the drawer it is in * * @param {number} [drawerHeight] current height of the drawer or height the drawer will be after animations */ resizeEditor: function(drawerHeight) { var $editor, headerHeight, recordHeight, showHideHeight, diffHeight, editorHeight, newEditorHeight; $editor = this.$('.mce-stack-layout .mce-stack-layout-item iframe'); //if editor not already rendered, cannot resize if ($editor.length === 0) { return; } drawerHeight = drawerHeight || app.drawer.getHeight(); headerHeight = this.$('.headerpane').outerHeight(true); recordHeight = this.$('.record').outerHeight(true); showHideHeight = this.$('.show-hide-toggle').outerHeight(true); editorHeight = $editor.height(); //calculate the space left to fill - subtracting padding to prevent scrollbar diffHeight = drawerHeight - headerHeight - recordHeight - showHideHeight - this.ATTACHMENT_FIELD_HEIGHT - this.EDITOR_RESIZE_PADDING; //add the space left to fill to the current height of the editor to get a new height newEditorHeight = editorHeight + diffHeight; //maintain min height if (newEditorHeight < this.MIN_EDITOR_HEIGHT) { newEditorHeight = this.MIN_EDITOR_HEIGHT; } //set the new height for the editor $editor.height(newEditorHeight); }, /** * Turn off logic from record view which handles clicking the cancel button * as it causes issues for email compose. * * TODO: Remove this when record view changes to use button events instead * of DOM based events */ cancelClicked: $.noop }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseEmailsActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailview */ ({ // Activity-card-detail View (base) extendsFrom: 'ActivityCardDetailview', /** * The state of the email */ state: null, /** * Initializes hbs date variables with date_entered */ initDateDetails: function() { if (this.activity) { this.state = this.activity.get('state'); const activityCard = this.getActivityCardLayout(); this.setDateDetails(activityCard.getCreatedDate(this.state)); this.detailDateTimeTooltip = (this.state === this.STATE_ARCHIVED) ? 'LBL_DATE_SENT' : 'LBL_DATE_CREATED'; } }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.CreateView * @alias SUGAR.App.view.views.BaseEmailsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ _titleLabel: 'LNK_NEW_ARCHIVE_EMAIL', /** * @inheritdoc * * Add 'TinymceHtmlEditor' plugin for view. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'Tinymce' ]); this._super('initialize', [options]); }, /** * @inheritdoc * * Hides or shows the attachments field based on whether or not there are * attachments when changes to the attachments are detected. * * Disables the save button if the attachments exceed the * max_aggregate_email_attachments_bytes configuration. Alerts the user, as * well. Enables the save button and dismisses the alert if the attachments * are under the max_aggregate_email_attachments_bytes configuration * configuration. */ bindDataChange: function() { if (this.model) { this.listenTo(this.model, 'change:attachments_collection', this._hideOrShowTheAttachmentsField); this.listenTo(this.model, 'attachments_collection:over_max_total_bytes', function(totalBytes, maxBytes) { var readableMax = app.utils.getReadableFileSize(maxBytes); var label = app.lang.get('LBL_TOTAL_ATTACHMENT_MAX_SIZE', this.module); var saveButton = this.getField(this.saveButtonName); app.alert.show('email-attachment-status', { level: 'warning', messages: app.utils.formatString(label, [readableMax]) }); if (saveButton) { saveButton.setDisabled(true); } }); this.listenTo(this.model, 'attachments_collection:under_max_total_bytes', function() { var saveButton = this.getField(this.saveButtonName); app.alert.dismiss('email-attachment-status'); if (saveButton) { saveButton.setDisabled(false); } }); } this._super('bindDataChange'); }, /** * @inheritdoc * * EmailsApi responds with a 500 HTTP status code to report custom errors * related to sending email. Other errors in the 400-499 range are * handled normally in core. */ saveModel: function(success, error) { var onError = _.bind(function(model, e) { if (e && e.status == 520) { // Mark the error as having been handled model.attributes.state = 'Draft'; e.handled = true; this.enableButtons(); app.alert.show(e.error, { level: 'error', autoClose: false, messages: e.message }); } else if (error) { error(model, e); } }, this); this._super('saveModel', [success, onError]); }, /** * @inheritdoc * * Adds the view parameter. It must be added to `options.params` because * the `options.view` is only added as a parameter if the request method is * "read". */ getCustomSaveOptions: function(options) { options = options || {}; options.params = options.params || {}; options.params.view = this.name; return options; }, /** * @inheritdoc * * Sets the title of the page. Hides or shows the attachments field. */ _render: function() { this._super('_render'); this.setTitle(app.lang.get(this._titleLabel, this.module)); this._hideOrShowTheAttachmentsField(); this._resizeEditor(); }, /** * Hides the attachments field if there are no attachments and shows the * field if there are attachments. */ _hideOrShowTheAttachmentsField: function() { var field = this.getField('attachments_collection'); var $el; var $row; if (!field) { return; } $el = field.getFieldElement(); $row = $el.closest('.row-fluid'); if (field.isEmpty()) { $row.addClass('hidden'); $row.removeClass('single'); } else { $row.removeClass('hidden'); $row.addClass('single'); } }, /** * @inheritdoc * * Builds the appropriate success message for saving an archived email. */ buildSuccessMessage: function() { return app.lang.get('LBL_EMAIL_ARCHIVED', this.module); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseEmailsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); const fieldsToDisplay = ['from', 'to', 'cc', 'bcc']; fieldsToDisplay.map((name) => { this[`${name}Field`] = _.find(panel.fields, (field) => field.name === `${name}_collection` ); }); this.hasAvatarUser = !!this.fromField && !!this.toField; } }) }, "compose-addressbook-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Emails.ComposeAddressbookListView * @alias SUGAR.App.view.views.BaseEmailsComposeAddressbookListView * @extends View.Views.Base.FlexListView */ ({ // Compose-addressbook-list View (base) extendsFrom: 'FlexListView', /** * Changed the address book view to use an independent mass collection * * The address book collection is not the same as the list view * collection and therefore we need to preserve the state of the address * book collection through changes to the list view collection. * `independentMassCollection: true` allows us to indicate that the * collections should not be treated as the same so that we are always * adding to the collection instead of resetting with completely new data. * * @property {boolean} */ independentMassCollection: true, /** * @inheritdoc */ initialize: function(options) { var plugins = [ 'ListColumnEllipsis', 'Pagination', 'MassCollection' ]; this.plugins = _.union(this.plugins || [], plugins); this._super('initialize', [options]); }, /** * Removes the event listeners that were added to the mass collection. * * @inheritdoc */ unbindData: function() { var massCollection = this.context.get('mass_collection'); if (massCollection) { this.stopListening(massCollection); } this._super('unbindData'); }, /** * Listens for changes to the list's mass collection. Anytime new rows are * selected or deselected, those models are synchronized in the selected * recipients collection (`this.model.get('to_collection')`). * * When the user enters a search term, the list is re-rendered with the * results of the search. Any recipients in the result set that have * already been selected are checked automatically. * * @inheritdoc */ _render: function() { var massCollection = this.context.get('mass_collection'); var selectedRecipients; var selectedRecipientsInList; /** * Models in the mass collection may be beans for Contacts, Leads, etc. * Those beans need to be converted to EmailParticipants beans when * they are added to the selected recipients collection. * * @param {Data|Bean} model * @return {Data|Bean} */ function convertModelToEmailParticipant(model) { return app.data.createBean('EmailParticipants', { _link: 'to', parent: { _acl: model.get('_acl') || {}, _erased_fields: model.get('_erased_fields') || [], type: model.module, id: model.get('id'), name: model.get('name') }, parent_type: model.module, parent_id: model.get('id'), parent_name: model.get('name'), email_address: _.first(model.get('email')).email_address }); } this._super('_render'); selectedRecipients = this.model.get('to_collection'); if (massCollection) { // Stop listening to changes on the mass collection. Those event // handlers will be recreated. this.stopListening(massCollection); // A single row was checked or all rows were checked. this.listenTo(massCollection, 'add', function(model) { var ep = convertModelToEmailParticipant(model); selectedRecipients.add(ep); }); // A single row was unchecked or all rows were unchecked. this.listenTo(massCollection, 'remove', function(model) { var existingRecipient = selectedRecipients.findWhere({parent_id: model.get('id')}); selectedRecipients.remove(existingRecipient); }); // The mass collection was cleared. Only remove the recipients from // the selected recipients collection that are visible in the list // view. this.listenTo(massCollection, 'reset', function(newCollection, prevCollection) { var resetRecipients; var selectedRecipientsNotInList = _.difference(prevCollection.previousModels, this.collection.models); newCollection.add(selectedRecipientsNotInList); resetRecipients = _.map(newCollection.models, convertModelToEmailParticipant); selectedRecipients.reset(resetRecipients); }); if (selectedRecipients.length > 0) { // Only add, to the mass collection, recipients that are // visible in the list view. selectedRecipientsInList = this.collection.filter(function(model) { return !!selectedRecipients.findWhere({parent_id: model.get('id')}); }); massCollection.add(selectedRecipientsInList); } } } }) } }} , "layouts": { "base": { "archive-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ArchiveEmailLayout * @alias SUGAR.App.view.layouts.BaseEmailsArchiveEmailLayout * @extends View.Layouts.Base.Emails.CreateLayout * @deprecated Use {@link View.Layouts.Base.Emails.CreateLayout} instead. */ ({ // Archive-email Layout (base) extendsFrom: 'EmailsCreateLayout', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Base.Emails.ArchiveEmailLayout is deprecated. ' + 'Use View.Layouts.Base.Emails.CreateLayout instead.'); this._super('initialize', [options]); } }) }, "compose-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ComposeEmailLayout * @alias SUGAR.App.view.layouts.BaseEmailsComposeEmailLayout * @extends View.Layouts.Base.Emails.CreateLayout */ ({ // Compose-email Layout (base) extendsFrom: 'EmailsCreateLayout', /** * @inheritdoc * * Enables the Compose:Send shortcut for views that implement it. */ initialize: function(options) { this.shortcuts = _.union(this.shortcuts || [], ['Compose:Send']); this._super('initialize', [options]); } }) }, "compose-addressbook": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ComposeAddressbookLayout * @alias SUGAR.App.view.layouts.BaseEmailsComposeAddressbookLayout * @extends View.Layout */ ({ // Compose-addressbook Layout (base) /** * @inheritdoc */ initialize: function(options) { app.view.Layout.prototype.initialize.call(this, options); this.collection.sync = this.sync; this.collection.allowed_modules = ['Accounts', 'Contacts', 'Leads', 'Prospects', 'Users']; this.context.on('compose:addressbook:search', this.search, this); }, /** * Calls the custom Mail API endpoint to search for email addresses. * * @param {string} method * @param {Data.Bean} model * @param {Object} options */ sync: function(method, model, options) { var callbacks; var url; var success; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list)) { options.module_list = ['all']; } else { options.module_list = _.intersection(this.allowed_modules, options.module_list); } // this is a hack to make pagination work while trying to minimize the affect on existing configurations // there is a bug that needs to be fixed before the correct approach (config.maxQueryResult vs. options.limit) // can be determined app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; // Is there already a success callback? if (options.success) { success = options.success; } // Map the response so that the email field data is packaged as an // array of objects. The email field component expects the data to be // in that format. options.success = function(data) { if (_.isArray(data)) { data = _.map(data, function(row) { row.email = [{ email_address: row.email, email_address_id: row.email_address_id, opt_out: row.opt_out, // The email address must be seen as the primary email // address to be shown in a list view. primary_address: true }]; // Remove the properties that are now stored in the nested // email array. delete row.opt_out; delete row.email_address_id; return row; }); } // Call the original success callback. if (success) { success(data); } }; options = app.data.parseOptionsForSync(method, model, options); options.params.erased_fields = true; callbacks = app.data.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); url = app.api.buildURL('Mail', 'recipients/find', null, options.params); app.api.call('read', url, null, callbacks); }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {String} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.collection.fetch({query: term, module_list: modules, offset: 0, update: false}); } }) }, "records": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.RecordsLayout * @alias SUGAR.App.view.layouts.BaseEmailsRecordsLayout * @extends View.Layouts.Base.RecordsLayout */ ({ // Records Layout (base) extendsFrom: 'RecordsLayout', /** * @inheritdoc * * Remove shortcuts that do not apply to Emails module list view */ initialize: function(options) { this.shortcuts = _.without( this.shortcuts, 'List:Favorite', 'List:Follow' ); this._super('initialize', [options]); } }) }, "compose": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ComposeLayout * @alias SUGAR.App.view.layouts.BaseEmailsComposeLayout * @extends View.Layouts.Base.Emails.CreateLayout * @deprecated Use {@link View.Layouts.Base.Emails.ComposeEmailLayout} instead. */ ({ // Compose Layout (base) extendsFrom: 'EmailsCreateLayout', /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Base.Emails.ComposeLayout is deprecated. ' + 'Use View.Layouts.Base.Emails.ComposeEmailLayout instead.'); this._super('initialize', [options]); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.CreateLayout * @alias SUGAR.App.view.layouts.BaseEmailsCreateLayout * @extends View.Layouts.Base.CreateLayout */ ({ // Create Layout (base) extendsFrom: 'CreateLayout', /** * @inheritdoc * * Enables the DragdropSelect2:SelectAll shortcut for views that implement * it. */ initialize: function(options) { this.shortcuts = _.union(this.shortcuts || [], ['DragdropSelect2:SelectAll']); this._super('initialize', [options]); } }) }, "activity-card": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ActivityCardLayout * @alias SUGAR.App.view.layouts.BaseEmailsActivityCardLayout * @extends View.Layout.Base.ActivityCardLayout */ ({ // Activity-card Layout (base) extendsFrom: 'ActivityCardLayout', /** * Constant representing the state of an email when it is a draft. * * @property {string} */ STATE_DRAFT: 'Draft', /** * Constant representing the state of an email when it is a draft. * * @property {string} */ STATE_ARCHIVED: 'Archived', /** * @inheritdoc * * Hides the Forward, Reply, and Reply All icons if the email card is a draft. */ setCardMenuVisibilities: function() { // if the email card is a draft if (this.model && this.model.get('state') === this.STATE_DRAFT) { this.$('.cabmenu .activity-card-emailaction').hide(); } }, /** * Returns the created date for the record based on the state of the email * * @param state * @return {string|null} */ getCreatedDate: function(state) { if (state === this.STATE_ARCHIVED) { return this.model.get('date_sent'); } else if (state === this.STATE_DRAFT) { return this.model.get('date_entered'); } return ''; } }) }, "compose-documents": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Emails.ComposeDocumentsLayout * @alias SUGAR.App.view.layouts.BaseEmailsComposeDocumentsLayout * @extends View.Layout * @deprecated Use {@link View.Layouts.Base.SelectionListLayout} instead. */ ({ // Compose-documents Layout (base) /** * @inheritdoc */ initialize: function(options) { app.logger.warn('View.Layouts.Base.Emails.ComposeDocumentsLayout is deprecated.'); this._super('initialize', [options]); } }) } }} , "datas": {} }, "Meetings":{"fieldTemplates": { "base": { "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Enum modifications that are specific to Meeting type field * These modifications are temporary until the can (hopefully) be addressed in * the Enum field refactoring (SC-3481) * * @class View.Fields.Base.Meetings.EnumField * @alias SUGAR.App.view.fields.BaseMeetingsEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) /** * @inheritdoc */ _render: function() { if (this.name === 'type') { this._ensureSelectedValueInItems(); } this._super('_render'); }, /** * Meeting type is a special case where we want to ensure the selected * value is an option in the list. This can happen when User A has * an external meeting integration set up (ie. WebEx) and sets WebEx as * the type. If User B does not have WebEx set up (only needed to create * WebEx meetings, not to join), User B should still see WebEx selected * on existing meetings, but not be able to create a meeting with WebEx. */ _ensureSelectedValueInItems: function() { var value = this.model.get(this.name), meetingTypeLabels; //if we don't have items list yet or no value previously selected - no work to do if (!this.items || _.isEmpty(this.items) || _.isEmpty(value)) { return; } //if selected value is not in the list of items, but is in the list of meeting types... meetingTypeLabels = app.lang.getAppListStrings('eapm_list'); if (_.isEmpty(this.items[value]) && !_.isEmpty(meetingTypeLabels[value])) { //...add it to the list this.items[value] = meetingTypeLabels[value]; } }, /** * @inheritdoc * * Remove options for meeting type field which comes from the vardef - this * will force a retrieval of options from the server. Options is in the * vardef for meeting type to support mobile which does not have the ability * to pull dynamic enum list from the server yet. */ loadEnumOptions: function(fetch, callback) { if (this.name === 'type') { this.def.options = ''; } this._super('loadEnumOptions', [fetch, callback]); } }) }, "label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Meetings.LabelField * @alias SUGAR.App.view.fields.BaseMeetingsLabelField * @extends View.Fields.Base.LabelField */ ({ // Label FieldTemplate (base) /** * @inheritdoc * * Returns the `detail` template for this type of field */ _getFallbackTemplate: function(viewName) { return 'detail'; }, }) }, "record-decor": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Meetings.RecordDecorFields * @alias SUGAR.App.view.fields.BaseMeetingsRecordDecorField * @extends View.Fields.Base.RecordDecorField */ ({ // Record-decor FieldTemplate (base) extendsFrom: 'RecordDecorField', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['RecurringEvents']); this._super('initialize', [options]); } }) }, "launchbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Button to launch an external meeting * * @class View.Fields.Base.Meetings.LaunchbuttonField * @alias SUGAR.App.view.fields.BaseMeetingsLaunchbuttonField * @extends View.Fields.Base.RowactionField */ ({ // Launchbutton FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'rowaction'; this.isHost = (this.def.host === true); }, /** * @inheritdoc * * Hide this button if: * - Status is not Planned * - Type is Sugar (not an external meeting type) * - Host button and user does not have permission to start the meeting */ _render: function() { if (this.model.get('status') !== 'Planned' || this.model.get('type') === 'Sugar' || (this.isHost && !this._hasPermissionToStartMeeting()) ) { this.hide(); } else { this._setLabel(); this._super('_render'); this.show(); } }, /** * Check if the user has permission to host the external meeting * True if assigned user or an admin for Meetings * * @return {boolean} * @private */ _hasPermissionToStartMeeting: function() { return (this.model.get('assigned_user_id') === app.user.id || app.acl.hasAccess('admin', 'Meetings')); }, /** * Set the appropriate label for this field * Use the Start Meeting label for host * Use the Join Meeting label otherwise * * @private */ _setLabel: function() { this.label = (this.isHost) ? this._getLabel('LBL_START_MEETING') : this._getLabel('LBL_JOIN_MEETING'); }, /** * Build the appropriate label based on the meeting type * * @param {string} labelName Meetings module label * @return {string} * @private */ _getLabel: function(labelName) { var meetingTypeStrings = app.lang.getAppListStrings('eapm_list'), meetingType = meetingTypeStrings[this.model.get('type')] || app.lang.get('LBL_MODULE_NAME_SINGULAR', this.module); return app.lang.get(labelName, this.module, {'meetingType': meetingType}); }, /** * Event to trigger the join/start of the meeting * Call the API first to get the host/join URL and determine if user has permission */ rowActionSelect: function() { var url = app.api.buildURL('Meetings', 'external', {id: this.model.id}); app.api.call('read', url, null, { success: _.bind(this._launchMeeting, this), error: function() { app.alert.show('launch_meeting_error', { level: 'error', messages: app.lang.get('LBL_ERROR_LAUNCH_MEETING_GENERAL', this.module) }); } }); }, /** * Given the external meeting info retrieved from the API, launch the meeting * Display an error if user is not permitted to launch the meeting. * * @param {Object} externalInfo * @private */ _launchMeeting: function(externalInfo) { var launchUrl = ''; if (this.disposed) { return; } if (this.isHost && externalInfo.is_host_option_allowed) { launchUrl = externalInfo.host_url; } else if (!this.isHost && externalInfo.is_join_option_allowed) { launchUrl = externalInfo.join_url; } else { // user is not allowed to launch the external meeting app.alert.show('launch_meeting_error', { level: 'error', messages: app.lang.get(this.isHost ? 'LBL_EXTNOSTART_MAIN' : 'LBL_EXTNOT_MAIN', this.module) }); return; } if (!this.isValidExternalUrl(launchUrl)) { app.alert.show('launch_meeting_error', { level: 'error', messages: this._getLabel('LBL_EXTERNAL_MEETING_NO_URL') }); return; } if (!_.isEmpty(launchUrl)) { window.open(launchUrl); } else { app.alert.show('launch_meeting_error', { level: 'error', messages: this._getLabel('LBL_EXTERNAL_MEETING_NO_URL') }); } }, /** * Re-render the join button when the model changes */ bindDataChange: function() { if (this.model) { this.model.on('change', this.render, this); } }, isValidExternalUrl: function(url) { try { const targetUrl = new URL(url); return targetUrl.protocol !== 'javascript:'; } catch (err) { return false; } } }) } }} , "views": { "base": { "dashablerecord": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.DashablerecordView * @alias SUGAR.App.view.views.MeetingsDashablerecordView * @extends View.Views.Base.DashablerecordView */ ({ // Dashablerecord View (base) extendsFrom: 'DashablerecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['RecurringEvents']); this._super('initialize', [options]); }, }) }, "resolve-conflicts-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.ResolveConflictsListView * @alias SUGAR.App.view.views.BaseMeetingsResolveConflictsListView * @extends View.Views.Base.ResolveConflictsListView */ ({ // Resolve-conflicts-list View (base) extendsFrom: 'ResolveConflictsListView', /** * @inheritdoc * * The invitees field should not be displayed on list views. It is removed * before comparing models so that it doesn't get included. */ _buildFieldDefinitions: function(modelToSave, modelInDb) { modelToSave.unset('invitees'); this._super('_buildFieldDefinitions', [modelToSave, modelInDb]); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.RecordView * @alias SUGAR.App.view.views.BaseMeetingsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['EditAllRecurrences', 'AddAsInvitee', 'RecurringEvents']); this._super('initialize', [options]); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.CreateView * @alias SUGAR.App.view.views.MeetingsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee', 'ReminderTimeDefaults']); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseMeetingsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setUsersFields(); }, /** * @inheritdoc * * Do not set user fields as that will be set after activity fetch */ setUsersPanel: function() { this.setUsersTemplate(); }, /** * @inheritdoc */ setUsersFields: function() { this.setInvitees(); } }) }, "create-nodupecheck": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Meetings.CreateNodupecheckView * @alias SUGAR.App.view.views.MeetingsCreateNodupecheckView * @extends View.Views.Base.CreateNodupecheckView */ ({ // Create-nodupecheck View (base) extendsFrom: 'CreateNodupecheckView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee', 'ReminderTimeDefaults']); this._super('initialize', [options]); } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.MeetingsModel * @alias SUGAR.App.model.datas.BaseMeetingsModel * @extends Model.Bean */ ({ // Model Data (base) plugins: ['VirtualCollection'] }) } }} }, "Tasks":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Tasks.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseTasksActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, (field) => field.name === 'assigned_user_name'); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Calendar":{"fieldTemplates": { "base": { "colorpicker": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Calendar.ColorpickerField * @alias SUGAR.App.view.fields.BaseCalendarColorpickerField * @extends View.Fields.Base.ColorpickerField */ ({ // Colorpicker FieldTemplate (base) /** * @override */ initialize: function(options) { this._super('initialize', [options]); this.once('render', function() { this.listenTo(this.model, 'change:color', _.bind(function() { let field = this.$('.hexvar[rel=colorpicker]'); let preview = this.$('.color-preview'); if (this.action == 'edit') { var value = field.val(); preview.css('backgroundColor', value); } }, this)); }, this); }, /** * @override */ _render: function() { this._super('_render'); if (this.action != 'edit') { this.fillIconBackground(); } }, /** * Sets the background color for the colorpicker icon. */ fillIconBackground: function() { if (this.action != 'edit') { if (typeof this.value == 'string' && this.value != '') { this.$('[data-content=color-picker-icon]').css({ 'background-color': this.value }); } } } }) }, "field-enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.FieldEnumField * @alias SUGAR.App.view.fields.BaseFieldEnumField * @extends View.Fields.Base.BaseFieldEnumField */ ({ // Field-enum FieldTemplate (base) extendsFrom: 'BaseEnumField', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.model) { //Check if the record is copied if (typeof this.context.get('copiedFromModelId') === 'string') { this._updateFieldDropdown(); } else { this.listenTo(this.model, 'sync', function() { this._updateFieldDropdown(); }.bind(this)); } this.listenTo(this.model, 'change:calendar_module', _.bind(this._updateFieldDropdown, this)); } }, /** * Update list of items */ _updateFieldDropdown: function() { if (this.name == 'dblclick_event') { var dropdownOptions = {}; dropdownOptions['detail:self:id'] = app.lang.get('LBL_NAVIGATE_TO_RECORD', 'Calendar'), dropdownOptions['detail-newtab:self:id'] = app.lang.get('LBL_NAVIGATE_TO_RECORD_NEW_TAB', 'Calendar'); dropdownOptions['edit:self:id'] = app.lang.get('LBL_OPEN_DRAWER_FOR_EDIT', 'Calendar'); var moduleMetadata = app.metadata.getModule(this.model.get('calendar_module')); if (moduleMetadata) { var fieldsMetadata = moduleMetadata.fields; _.each(fieldsMetadata, _.bind(function(fieldMetadata) { var moduleName = this.model.get('calendar_module'); if (fieldMetadata.type == 'relate' && typeof fieldMetadata.module == 'string' && typeof fieldMetadata.id_name == 'string') { dropdownOptions['detail:' + fieldMetadata.module + ':' + fieldMetadata.id_name] = app.lang.get('LBL_NAVIGATE_TO_RECORD', 'Calendar') + ' (' + app.lang.get(fieldMetadata.vname, moduleName) + ')'; dropdownOptions['detail-newtab:' + fieldMetadata.module + ':' + fieldMetadata.id_name] = app.lang.get('LBL_NAVIGATE_TO_RECORD_NEW_TAB', 'Calendar') + ' (' + app.lang.get(fieldMetadata.vname, moduleName) + ')'; dropdownOptions['edit:' + fieldMetadata.module + ':' + fieldMetadata.id_name] = app.lang.get('LBL_OPEN_DRAWER_FOR_EDIT', 'Calendar') + ' (' + app.lang.get(fieldMetadata.vname, moduleName) + ')'; fieldMetadata.id; } }, this)); } this.items = dropdownOptions; } else { if (!_.isArray(this.fieldDefs.field_types_allowed)) { return; } var fieldsMetadata = []; if (this.model.get('calendar_module') == '' || _.isUndefined(this.model.get('calendar_module'))) { return; } var calendarModule = this.model.get('calendar_module'); var moduleMetadata = app.metadata.getModule(calendarModule); if (moduleMetadata) { fieldsMetadata = moduleMetadata.fields; var dropdownOptions = {}; if (this.name != 'event_start') { dropdownOptions[''] = ''; } _.each(fieldsMetadata, function(fieldMetadata) { if (typeof fieldMetadata == 'object') { var fieldType = fieldMetadata.dbType || fieldMetadata.dbtype || fieldMetadata.type; var fieldSource = fieldMetadata.source || ''; if ( this.fieldDefs.field_types_allowed.indexOf(fieldType) >= 0 && fieldSource != 'non-db' && this.model.denyFields.indexOf(fieldMetadata.name) == -1 ) { dropdownOptions[fieldMetadata.name] = app.lang.get(fieldMetadata.vname, calendarModule); } }; }, this); this.items = dropdownOptions; } } this.render(); }, /** * Load enum options will not be needed on this field type */ loadEnumOptions: function(fetch, callback, error) { } }) }, "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.CalendarHtmleditableTinymceField * @alias SUGAR.App.view.fields.BaseCalendarHtmleditableTinymceField * @extends View.Fields.Base.BaseHtmleditableTinymceField */ ({ // Htmleditable_tinymce FieldTemplate (base) /** * Fields which should not be available to insert in the template */ badFields: [ 'deleted', 'team_count', 'user_name', 'user_hash', 'password', 'is_admin', 'mkto_id', 'parent_type' ], specialFields: [ 'created_by_name', 'modified_by_name', 'primary_contact_name', 'duration_minutes', 'duration_hours', 'entry_source', 'email1' ], /** * Field types which should not be available to insert in the template */ badFieldTypes: [ 'link', 'id', 'collection', 'widget', 'html', 'htmleditable_tinymce', 'image', 'teamset', 'team_list', 'password', 'file' ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // The plugin 'insertfield' needs to be aplied again to show fields of the current module this.listenTo(this.model, 'change:calendar_module', _.bind(this.render, this)); }, /** * Return submenu items * * @param editor * @return {Array} */ getSubmenu: function(editor) { var module = this.model.get('calendar_module'); var fields = app.metadata.getModule(module).fields; fields = _.filter(fields, function(field) { return _.contains(this.specialFields, field.name) || ( !_.isEmpty(field.name) && !_.isEmpty(field.vname) && !_.contains(this.badFields, field.name) && !_.contains(this.badFieldTypes, field.type) && !_.contains(this.badFieldTypes, field.dbType) && field.link_type !== 'relationship_info' && ( _.isUndefined(field.studio) || (_.isObject(field.studio) || field.studio == 'true' || field.studio == true) ) && field.source !== 'non-db' && typeof field.processes == 'undefined' ); }); fields.push({ name: 'event_timestamp', vname: app.lang.getModString('LBL_INSERTFIELD_EVENT_TIMESTAMP', 'Calendar') }); var insertOptions = []; fields = _.sortBy(fields, function sortFieldsAlphabetically(field) { var fieldLabel = app.lang.get(field.vname, module); if (_.isString(fieldLabel)) { return fieldLabel.toLowerCase(); } else { return ''; } }); _.each( fields, function(field) { var fieldLabel = app.lang.get(field.vname, module); var option = { type: 'menuitem', text: fieldLabel, onAction: () => editor.insertContent(`{::${field.name}::}`), }; insertOptions.push(option); }, this ); return insertOptions; }, /** * Add custom button to the UI * * @param editor */ addCustomButtons: function(editor) { if (!app.acl.hasAccess('view', this.model.get('calendar_module'))) { return; } if (_.isEmpty(this.model.get('calendar_module'))) { return; } editor.ui.registry.addMenuButton('insertfield_calendar', { text: app.lang.getModString('LBL_INSERTFIELD', 'Calendar'), onAction: () => {}, fetch: (callback) => callback(this.getSubmenu(editor)), }); }, /** * @override */ getTinyMCEConfig: function() { var getConfig = this._super('getTinyMCEConfig') || {}; getConfig.toolbar += ' insertfield_calendar'; if (this.fieldDefs.name == 'ical_event_template') { getConfig.toolbar = 'insertfield_calendar'; } return getConfig; } }) } }} , "views": { "base": { "add-calendarcontainer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.AddCalendarcontainerView * @alias SUGAR.App.view.views.BaseCalendarAddCalendarcontainerView * @extends View.Views.Base.View */ ({ // Add-calendarcontainer View (base) className: 'calendarAddContainer', /** * @override */ _render: function() { this._super('_render'); let calendarParams = { def: { name: 'calendar', type: 'relate', custom_module: 'Calendar', ext2: 'Calendar', id_name: 'id', module: 'Calendar', quicksearch: 'enabled', required: false, source: 'non-db' }, view: this, viewName: 'edit', model: this.model }; const dashletSource = this.context.get('dashletSource'); const mainCalendarSource = this.context.get('mainCalendarSource'); const calendarModules = [app.controller.context.get('module')]; if (dashletSource !== true && mainCalendarSource !== true) { calendarParams.def = _.extend(calendarParams.def, { initial_filter: 'available_calendars', initial_filter_label: 'LBL_CALENDAR_AVAILABLE_CALENDARS', filter_populate: { 'calendar_module': { $in: calendarModules } } }); } this.calendarField = app.view.createField(calendarParams); this.calendarField.render(); this.$('[data-content=calendar-field]').html(this.calendarField.$el); this.listenTo(this.calendarField.model, 'change:calendar', _.bind(function() { this.context.trigger('calendar:change', this.calendarField.model.get('id')); }, this)); } }) }, "main-panel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.MainPanelView * @alias SUGAR.App.view.views.BaseCalendarMainPanelView * @extends View.Views.Base.View */ ({ // Main-panel View (base) /** * @inheritdoc */ initialize: function(options) { this.events = this.events || {}; this.events['click span[name=addCalendar]'] = 'addCalendar'; this.componentStorageKey = 'main-panel'; this.keyToStoreCalendarConfigurations = app.Calendar.utils.buildUserKeyForStorage(this.componentStorageKey); this.miniCalendar = null; this.delaySelectMyCalendarsTime = 100; this._super('initialize', [options]); this.listenTo(this.context, 'scheduler:view:changed', _.bind(this.updateMiniCalendar, this)); }, /** * Update the mini calendar */ updateMiniCalendar: function() { let schedulerView = this.layout.getComponent('scheduler'); let startDate = schedulerView.scheduler.date(); this.miniCalendar.value(startDate); }, /** * @override */ bindDataChange: function() { this.model.on('change:myCalendars', this.reloadCalendar, this); this.model.on('change:otherCalendars', this.reloadCalendar, this); }, /** * @inheritdoc */ _render: function() { let calendarsSaved = app.user.lastState.get(this.keyToStoreCalendarConfigurations); if (typeof calendarsSaved == 'undefined') { this.selectMyCalendars(); } calendarsSaved = calendarsSaved || {}; const myConfigurations = calendarsSaved.myCalendars || []; const otherConfigurations = calendarsSaved.otherCalendars || []; this.model.set({ myCalendars: myConfigurations, otherCalendars: otherConfigurations }); this._super('_render'); if (typeof kendo == 'object') { this.culturePreferences(); this.loadMiniCalendar(); this.calculateCalendarPosition(); } else { $.getScript('cache/include/javascript/sugar_grp_calendar.js', _.bind(function() { this.culturePreferences(); this.loadMiniCalendar(); this.calculateCalendarPosition(); }, this)); } }, /** * Select my calendars */ selectMyCalendars: function() { if (typeof this.delaySelectingMyCalendars !== 'undefined') { clearTimeout(this.delaySelectingMyCalendars); this.delaySelectingMyCalendars = null; } let myCalendars = this.getField('myCalendars'); if (myCalendars instanceof app.view.Field) { if (myCalendars.$el.find('input[type=checkbox]').length == 0) { this.delaySelectingMyCalendars = _.delay(_.bind(this.selectMyCalendars, this), this.delaySelectMyCalendarsTime); } else { myCalendars.$el.find('input[type=checkbox]').attr('checked', true); myCalendars.trigger('calendars:selectAll'); } } else { this.delaySelectingMyCalendars = _.delay(_.bind(this.selectMyCalendars, this), this.delaySelectMyCalendarsTime); } }, /** * Load mini calendar */ loadMiniCalendar: function() { const userDatePref = app.Calendar.utils.getKendoDateMapping( app.user.getPreference('datepref'), 'fullVerboseMonth' ); this.$('[data-content=mini-calendar]').kendoCalendar({ change: _.bind(function changeMiniCalendar() { let schedulerView = this.layout.getComponent('scheduler'); if (schedulerView.scheduler._selectedView.name === 'month') { schedulerView.scheduler.view('day'); schedulerView.$el.find('.k-dropdown').trigger('change'); } let dateSelected = new Date(moment(this.miniCalendar.current()).format('YYYY/MM/DD')); schedulerView.scheduler.date(dateSelected); schedulerView.scheduler.select({ events: [], start: dateSelected, end: dateSelected, groupIndex: 0 }); let loadedEventsStart = new Date(schedulerView._eventsLoaded.startDate); let loadedEventsEnd = new Date(schedulerView._eventsLoaded.endDate); if (loadedEventsStart > dateSelected || loadedEventsEnd < dateSelected) { schedulerView.trigger('calendar:reload'); } }, this), footer: kendo.format(`{0: dddd, ${userDatePref}}`, new Date()) }); this.$('.k-icon.k-i-arrow-60-right') .removeClass('k-icon') .removeClass('k-i-arrow-60-right') .addClass('sicon') .addClass('sicon-chevron-right'); this.$('.k-icon.k-i-arrow-60-left') .removeClass('k-icon') .removeClass('k-i-arrow-60-left') .addClass('sicon') .addClass('sicon-chevron-left'); this.miniCalendar = this.$('[data-content=mini-calendar]').data('kendoCalendar'); }, /** * Calculate calendar position * * When refresh on this page, we might get into here before kendo css loaded, * so no positioninig possible. * solution is to generate a resonable amount of tries before quit */ calculateCalendarPosition: function() { if (this.$el.css('background-color') == 'rgba(0, 0, 0, 0)') { /** * A count down for number of tries. Wait for slow networks */ let resonableLimit = 50; /** * Try interval */ const timeToWait = 100; const waitForCssToLoad = setInterval(function() { if (this.$el.css('background-color') != 'rgba(0, 0, 0, 0)') { this.positionMiniCalendar(); clearInterval(waitForCssToLoad); } if (resonableLimit == 0) { clearInterval(waitForCssToLoad); } resonableLimit--; }.bind(this), timeToWait); } else { this.positionMiniCalendar(); } }, /** * Position mini calendar */ positionMiniCalendar: function() { this.$('[data-content=mini-calendar]').css('width', '100%'); this.$('[data-content=mini-calendar] .k-calendar-view').css('width', '100%'); }, /** * Add other calendar drawer */ addCalendar: function() { app.drawer.open( { layout: 'add-calendar', context: { module: 'Calendar', mixed: true, mainCalendarSource: true } }, _.bind(function closeCalendarAdd(calendar) { if (typeof calendar == 'undefined') { return; } const calendarsInLS = app.user.lastState.get(this.keyToStoreCalendarConfigurations); if (typeof (calendarsInLS) !== 'undefined' && typeof calendarsInLS.otherCalendars !== 'undefined') { let calendarAlreadyAdded = false; _.each(calendarsInLS.otherCalendars, function searchCalendar(calendarInLS) { if (calendarInLS.calendarId === calendar.calendarId && ((_.isEmpty(calendarInLS.teamId) && calendarInLS.userId === calendar.userId) || (_.isEmpty(calendarInLS.userId) && calendarInLS.teamId === calendar.teamId)) ) { calendarAlreadyAdded = true; } }); if (calendarAlreadyAdded) { app.alert.show('calendar-already-added', { level: 'info', messages: app.lang.getModString('LBL_CALENDAR_CALENDAR_ALREADY_ADDED', 'Calendar'), autoClose: true }); return; } } calendar.selected = true; let otherCalendarsList = this.model.get('otherCalendars'); otherCalendarsList.push(calendar); app.user.lastState.set(this.keyToStoreCalendarConfigurations, { myCalendars: this.model.get('myCalendars'), otherCalendars: this.model.get('otherCalendars') }); this.model.trigger('change:otherCalendars'); const field = this.getField('otherCalendars'); field.render(); }, this) ); }, /** * Reload calendar */ reloadCalendar: function() { let scheduler = this.layout.getComponent('scheduler'); if (scheduler.$el.html() == '') { return; } const myConfigurations = this.model.get('myCalendars') || []; const otherConfigurations = this.model.get('otherCalendars') || []; const otherConfigurationsSelected = _.filter(otherConfigurations, function(configuration) { return configuration.selected; }); const calendarsList = myConfigurations.concat(otherConfigurationsSelected); const calendarsModels = _.map(calendarsList, function(calendarData) { return app.data.createBean('Calendar', calendarData); }); scheduler.calendars = app.data.createBeanCollection('Calendar', calendarsModels); scheduler.trigger('calendar:reconfigure'); }, /** * Setup the culture preference */ culturePreferences: function() { const weekStart = parseInt(app.user.getPreference('first_day_of_week'), 10); kendo.culture('en-US'); if (weekStart) { kendo.culture().calendar.firstDay = weekStart; } }, /** * @inheritdoc */ _dispose: function() { if (this.miniCalendar) { this.miniCalendar.destroy(); this.miniCalendar = null; } this.context.off('scheduler:view:changed'); this._super('_dispose'); } }) }, "add-filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.AddFilterView * @alias SUGAR.App.view.views.BaseCalendarAddFilterView * @extends View.Views.Base.View */ ({ // Add-filter View (base) _moduleFilterList: [], _allModulesId: 'All', _selectedModule: null, _currentSearch: '', events: { 'keyup .search-name': 'throttledSearch', 'paste .search-name': 'throttledSearch', 'click .add-on.sicon-close': 'clearInput' }, /** * Converts the input field to a select2 field and adds the module filter for refining the search. * * @override */ _render: function() { this._super('_render'); this.buildModuleFilterList(); this.buildFilter(); }, /** * Builds the list of allowed modules to provide the data to the select2 field. */ buildModuleFilterList: function() { const allowedModules = this.collection.allowed_modules; this._moduleFilterList = [ {id: this._allModulesId, text: app.lang.get('LBL_MODULE_ALL')} ]; _.each(allowedModules, function(module) { this._moduleFilterList.push({ id: module, text: app.lang.getModuleName(module, {plural: true}) }); }, this); }, /** * Converts the input field to a select2 field and initializes the selected module. */ buildFilter: function() { let $filter = this.getFilterField(); if ($filter.length > 0) { $filter.select2({ data: this._moduleFilterList, allowClear: false, multiple: false, minimumResultsForSearch: -1, formatSelection: _.bind(this.formatModuleSelection, this), formatResult: _.bind(this.formatModuleChoice, this), dropdownCss: {width: 'auto'}, dropdownCssClass: 'search-filter-dropdown', initSelection: _.bind(this.initSelection, this), escapeMarkup: function(m) { return m; }, width: 'off' }); $filter.off('change'); $filter.on('change', _.bind(this.handleModuleSelection, this)); this._selectedModule = this._selectedModule || this._allModulesId; $filter.select2('val', this._selectedModule); } }, /** * Gets the filter DOM field. * * @return {jQuery} DOM Element */ getFilterField: function() { return this.$('input.select2'); }, /** * Gets the module filter DOM field. * * @return {jQuery} DOM Element */ getModuleFilter: function() { return this.$('span.choice-filter-label'); }, /** * Destroy the select2 plugin. */ unbind: function() { let $filter = this.getFilterField(); if ($filter.length > 0) { $filter.off(); $filter.select2('destroy'); } this._super('unbind'); }, /** * Performs a search once the user has entered a term. * * @param {Object} evt */ throttledSearch: _.debounce(function(evt) { const newSearch = this.$(evt.currentTarget).val(); if (this._currentSearch !== newSearch) { this._currentSearch = newSearch; this.applyFilter(); } }, 400), /** * Initialize the module selection with the value for all modules. * * @param {jQuery} el * @param {Function} callback */ initSelection: function(el, callback) { if (el.is(this.getFilterField())) { const module = _.findWhere(this._moduleFilterList, {id: el.val()}); callback({id: module.id, text: module.text}); } }, /** * Format the selected module to display its name. * * @param {Object} item * @return {string} */ formatModuleSelection: function(item) { // update the text for the selected module this.getModuleFilter().text(item.text); return '<span class=\'select2-choice-type\'>' + app.lang.get('LBL_MODULE') + '<i class=\'sicon sicon-caret-down\'></i></span>'; }, /** * Format the choices in the module select box. * * @param {Object} option * @return {string} */ formatModuleChoice: function(option) { return '<div><span class=\'select2-match\'></span>' + option.text + '</div>'; }, /** * Handler for when the module filter dropdown value changes, either via a click or manually calling jQuery's * .trigger('change') event. * * @param {Object} evt jQuery Change Event Object * @param {string} overrideVal (optional) ID passed in when manually changing the filter dropdown value */ handleModuleSelection: function(evt, overrideVal) { const module = overrideVal || evt.val || this._selectedModule || this._allModulesId; // only perform a search if the module is in the approved list if (!_.isEmpty(_.findWhere(this._moduleFilterList, {id: module}))) { this._selectedModule = module; this.getFilterField().select2('val', this._selectedModule); this.getModuleFilter().css('cursor', 'pointer'); this.applyFilter(); } }, /** * Triggers an event that makes a call to search the user/team and filter the data set. */ applyFilter: function() { const searchAllModules = (this._selectedModule === this._allModulesId); // pass an empty array when all modules are being searched const module = searchAllModules ? [] : [this._selectedModule]; // determine if the filter is dirty so the 'clearQuickSearchIcon' can be added/removed appropriately const isDirty = !_.isEmpty(this._currentSearch); this._toggleClearQuickSearchIcon(isDirty); this.context.trigger('calendar:add:search', module, this._currentSearch); }, /** * Append or remove an icon to the quicksearch input so the user can clear the search easily. * * @param {boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.add-on.sicon-close')[0]) { this.$('.filter-view.search').append('<i class=\'add-on sicon sicon-close\'></i>'); } else if (!addIt) { this.$('.add-on.sicon-close').remove(); } }, /** * Clear input */ clearInput: function() { let $filter = this.getFilterField(); this._currentSearch = ''; this._selectedModule = this._allModulesId; this.$('.search-name').val(this._currentSearch); if ($filter.length > 0) { $filter.select2('val', this._selectedModule); } this.applyFilter(); } }) }, "bwc": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @inheritdoc */ ({ // Bwc View (base) convertToSidecarUrl: function(href) { var module = this.moduleRegex.exec(href); module = (_.isArray(module)) ? module[1] : null; if (!module) { return ''; } //Route links for BWC modules through bwc/ route //Remove any './' nonsense in existing hrefs href = href.replace(/^.*\//, ''); return 'bwc/' + href; } }) }, "module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Module-menu View (base) /** * @inheritdoc */ populateMenu: function() { this.populate('calendars'); }, /** * @inheritdoc */ populate: function(tplName, filter, limit) { app.api.call('read', app.api.buildURL('Calendar/modules'), {}, { success: _.bind(function(data) { this.resetCollection(tplName); _.each(data.modules, function(moduleInfo, module) { let collection = this.getCollection(tplName); let createLabel = ''; if (module === 'KBContents') { createLabel = app.lang.getModString('LNK_NEW_KBCONTENT_TEMPLATE', module); } else { let createLabelKey = 'LNK_NEW_' + moduleInfo.objName.toUpperCase(); createLabel = app.lang.get(createLabelKey, module); if (createLabel === createLabelKey) { createLabelKey = 'LNK_NEW_RECORD'; createLabel = app.lang.getModString(createLabelKey, module); } } const moduleItem = { module: module, label: createLabel }; collection.push(moduleItem); }, this); this._renderPartial(tplName); }, this) }); }, /** * Reset collection * * @param {string} tplName The name of the partial template which uses this collection */ resetCollection: function(tplName) { this._collections[tplName] = []; }, /** * @inheritdoc */ getCollection: function(tplName) { if (!this._collections[tplName]) { this._collections[tplName] = []; } return this._collections[tplName]; }, /** * @inheritdoc */ _renderPartial: function(tplName, options) { if (this.disposed || !this.isOpen()) { return; } options = options || {}; let tpl = app.template.getView(this.name + '.' + tplName, this.module) || app.template.getView(this.name + '.' + tplName); const modules = this.getCollection(tplName); let $placeholder = this.$('[data-container="' + tplName + '"]'); let $old = $placeholder.nextUntil('.divider'); //grab the focused element's route (if exists) for later re-focusing const focusedRoute = $old.find(document.activeElement).data('route'); //replace the partial using newly updated modules collection $old.remove(); $placeholder.after(tpl(_.extend({'modules': modules}, options))); //if there was a focused element previously, restore its focus if (focusedRoute) { const $new = $placeholder.nextUntil('.divider'); const focusSelector = '[data-route="' + focusedRoute + '"]'; const $newFocus = $new.find(focusSelector); if ($newFocus.length > 0) { $newFocus.focus(); } } } }) }, "add-calendars-bottom": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.AddCalendarsBottomView * @alias SUGAR.App.view.views.BaseCalendarAddCalendarsBottomView * @extends View.Views.Base.ListBottomView */ ({ // Add-calendars-bottom View (base) extendsFrom: 'ListBottomView', /** * Assign proper label for 'show more' link. * Label should be 'More recipients...'. */ setShowMoreLabel: function() { this.showMoreLabel = app.lang.getModString('LBL_CALENDAR_ADD_SHOW_MORE_RECIPIENTS', this.module); } }) }, "add-calendars": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.AddCalendarcontainerView * @alias SUGAR.App.view.views.BaseCalendarAddCalendarcontainerView * @extends View.Views.Base.FlexListView */ ({ // Add-calendars View (base) extendsFrom: 'FlexListView', /** * @inheritdoc */ initialize: function(options) { var plugins = [ 'ListColumnEllipsis', 'Pagination', ]; this.plugins = _.union(this.plugins || [], plugins); this._super('initialize', [options]); this.context.on('change:selection_model', this._selectUserOrTeamAndClose, this); this.context.on('calendar:change', this._selectCalendarAndClose, this); this.events = { 'click .single': 'triggerCheck' }; this.newCalendar = { calendarId: '', userId: '', teamId: '' }; }, /** * Closes the drawer passing the selected model attributes to the callback if calendar is set * * @param {Object} context * @param {Data.Bean} selectionModel The selected calendar configuration. */ _selectUserOrTeamAndClose: function(context, selectionModel) { var selectedModule = selectionModel.get('_module'); if (selectedModule == 'Users') { this.newCalendar.userId = selectionModel.get('id'); this.newCalendar.teamId = ''; } else if (selectedModule == 'Teams') { this.newCalendar.userId = ''; this.newCalendar.teamId = selectionModel.get('id'); } if (!_.isEmpty(this.newCalendar.calendarId)) { app.drawer.close(this.newCalendar); } }, /** * Select calendar and eventually close the drawer * * @param {string} calendarId */ _selectCalendarAndClose: function(calendarId) { this.newCalendar.calendarId = calendarId; if (!_.isEmpty(this.newCalendar.userId) || !_.isEmpty(this.newCalendar.teamId)) { app.drawer.close(this.newCalendar); } }, /** * Trigger check * * @param {Object} event */ triggerCheck: function(event) { var checkbox = $(event.currentTarget).find('[data-check=one]'); checkbox[0].click(); } }) }, "add-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.CalendarAddHeaderpaneView * @alias SUGAR.App.view.views.BaseCalendarAddHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // Add-headerpane View (base) extendsFrom: 'HeaderpaneView', events: { 'click [name=cancel_button]': '_cancel' }, /** * Close the drawer. * * @private */ _cancel: function() { app.drawer.close(); } }) }, "scheduler": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Calendar.SchedulerView * @alias SUGAR.App.view.views.BaseCalendarSchedulerView * @extends View.Views.Base.View */ ({ // Scheduler View (base) className: 'calendar-scheduler', /** * @inheritdoc */ initialize: function(options) { this._initVars(); this._super('initialize', [options]); this.positionLegend = _.bind(this.positionLegend, this); this.setSchedulerEvents(); }, /** * Initialize parameters */ _initVars: function() { /** * A reference to the Kendo Scheduler component */ this.scheduler = null; /** * Reference to toolbar SubView */ this.toolbarView = null; /** * Reference to popul used to confirm email sending */ this.confirmationPopupView = null; /** * The event focused at a given moment */ this._selectedState = {}; /** * Calendar definitions fetched for calendars in this component. * Contains all fields from db */ this.calendarDefs = []; /** * Raw events got from the last fetch * The start and end dates show the interval of the fetch */ this._eventsLoaded = { startDate: '', endDate: '', events: [] }; /** * Calendar wrapper id. Unique identifier of the scheduler */ this._schedulerCssId = 'scheduler_' + this.cid; /** * Event deleted. Store it at this level to have access to it anywhere needed until it's gone */ this._deletedEvent = {}; /** * Flage to indicate the action in progress. Store it at this level to have access to it everywhere */ this._isDeleteAction = false; /** * Raw views which can be seen in the calendar. */ this._allPossibleViews = ['day', 'workWeek', 'week', 'expandedMonth', 'agenda', 'timeline', 'monthSchedule']; this.DAY_VIEW = 'day'; this.WEEK_VIEW = 'week'; this.WORK_WEEK_VIEW = 'workWeek'; this.EXPANDED_MONTH_VIEW = 'expandedMonth'; this.AGENDA_VIEW = 'agenda'; this.TIMELINE_VIEW = 'timeline'; this.MONTH_SCHEDULE_VIEW = 'monthSchedule'; /** * Calendar's views */ this.views = this.setupCalendarViews(); /** * URI to get events */ this.eventsURL = 'Calendar/getEvents'; /** * Number of next months to load events */ this.nrOfNextMonthsToLoadEvents = 1; /** * Calendars to show on this view */ this.calendars = app.data.createBeanCollection('Calendar', this.options.context.get('calendars')); /** * Location of the scheduler: main, record or records */ this.location = this.options.context.get('location') || ''; /** * Options for kendo scheduler */ this.customKendoOptions = this.options.context.get('customKendoOptions') || {}; /** * Flag whether to load calendars or not */ this.options.skipFetch = this.options.context.get('skipFetch') || false; this.keyToStoreCalendarConfigurations = this.options.context.get('keyToStoreCalendarConfigurations'); this.keyToStoreCalendarView = app.Calendar.utils.buildUserKeyForStorage(this.location); this.keyToStoreCalendarBusinessHours = 'calendarBusinessHours'; this._setBusinessHours(); }, /** * Adds events this component will listen for */ setSchedulerEvents: function() { this.listenTo(app.events, 'calendar:reload', _.bind(this.loadData, this)); this.listenTo(this, 'calendar:reload', _.bind(this.loadData, this)); this.listenTo(this, 'calendar:reconfigure', _.bind(this._reconfigureCalendar, this)); this.listenTo(this.context, 'change:calendars', _.bind(this.updateCalendars, this)); this.listenTo(this.context, 'button:cancel:click', _.bind(this.cancelSave, this)); this.listenTo(this.context, 'button:save:click', _.bind(this.saveRecord, this)); this.listenTo(this.context, 'button:saveAndSendInvites:click', _.bind(this.saveRecordAndSendInvites, this)); this.events = { 'click .previewEvent': '_previewEvent' }; }, /** * Setup calendar views * * @return {Array} */ setupCalendarViews: function() { let views = this.options.context.get('availableViews'); if (typeof views == 'undefined' || views.length == 0) { views = app.utils.deepCopy(this._allPossibleViews); //make sure to use a copy } let defaultView = this.options.context.get('defaultView'); if (typeof defaultView == 'undefined' || defaultView == '') { defaultView = views[0]; } views = _.map(views, _.bind(function(view) { let newView = { type: this.getViewType(view), title: this.getViewTitle(view), selected: false, schedulerViewName: view, showWorkHours: true, }; if (view === this.MONTH_SCHEDULE_VIEW) { newView.showWorkHours = false; } if (defaultView === view) { newView.selected = true; } return newView; }, this)); return views; }, /** * Update Calendars * * Useful when loading the main scheduler. Calendar configurations are set * on the main-scheduler layout so we need to update default vars and reload the scheduler */ updateCalendars: function() { this.location = this.options.context.get('location') || ''; this.customKendoOptions = this.options.context.get('customKendoOptions') || {}; this.options.skipFetch = this.options.context.get('skipFetch') || false; this.views = this.setupCalendarViews(); this.calendars = app.data.createBeanCollection('Calendar', this.options.context.get('calendars')); this.keyToStoreCalendarConfigurations = this.options.context.get('keyToStoreCalendarConfigurations'); this.keyToStoreCalendarView = app.Calendar.utils.buildUserKeyForStorage(this.location); if (_.isNull(this.scheduler)) { this.once('calendar:initialized', _.bind(this._reconfigureCalendar, this)); } else { this._reconfigureCalendar(); } }, /** * Cancel save record */ cancelSave: function() { this.offlineRefreshDataSource(); }, /** * Save record * * @param {Object} clonedEvent Event */ saveRecord: function(clonedEvent) { if (_.isNull(this.eventInChange)) { app.logger.error('Failed to get event in change'); return; } if (clonedEvent) { this.updateEvent(clonedEvent); } }, /** * Save record and send invites * * @param {Object} clonedEvent */ saveRecordAndSendInvites: function(clonedEvent) { if (clonedEvent) { clonedEvent.sendInvites = true; this.saveRecord(clonedEvent); } }, /** * Populate calendar * * @param {Object} res Full response from api */ populateCalendarWithData: function(res) { if (this.disposed) { return; } let kendoEvents = []; this._eventsLoaded.events = []; this._eventsLoaded.startDate = res.startDate; this._eventsLoaded.endDate = res.endDate; this._usersInEventsLoaded = res.users; if (!_.isEmpty(res)) { _.each( res.data, function(row) { let rowData = { recordId: row.id, calendarId: row.calendarId, id: row.calendarId + '_' + row.id, eventUsers: row.eventUsers, name: DOMPurify.sanitize(row.title, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), start: moment(row.start).toDate(), end: moment(row.end).toDate(), title: DOMPurify.sanitize(row.title, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), description: row.description !== undefined ? DOMPurify.sanitize(row.description, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }) : undefined, isAllDay: row.isAllDay, module: row.module, event_tooltip: DOMPurify.sanitize(row.event_tooltip, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), day_event_template: DOMPurify.sanitize(row.day_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), week_event_template: DOMPurify.sanitize(row.week_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), month_event_template: DOMPurify.sanitize(row.month_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), agenda_event_template: DOMPurify.sanitize(row.agenda_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), timeline_event_template: DOMPurify.sanitize(row.timeline_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), schedulermonth_event_template: DOMPurify.sanitize(row.schedulermonth_event_template, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), dbclickRecordId: row.dbclickRecordId, color: _.escape(row.color), assignedUserName: row.assignedUserName, assignedUserId: row.assignedUserId, invitees: row.invitees }; const start = moment(row.start); const end = moment(row.end); const duration = moment.duration(end.diff(start)); if (duration.asDays() >= 1) { rowData.isAllDay = true; } if (start.format() === end.format()) { rowData.isAllDay = true; } //fix kendo not showing events made on date types spanning //on expected cells let schedulerViewType = this.scheduler._selectedView.name; schedulerViewType = this.getViewType(schedulerViewType); if (schedulerViewType === this.EXPANDED_MONTH_VIEW) { let calendarDef = _.find(this.calendarDefs, function(calendarDef) { return calendarDef.calendarId === row.calendarId; }); let calendarModule = calendarDef.module; let moduleMetadata = app.metadata.getModule(calendarModule); let fieldsMetadata = moduleMetadata.fields; let endDef = fieldsMetadata[calendarDef.end_field]; if (typeof endDef != 'undefined') { let endDefFieldType = endDef.dbType || endDef.dbtype || endDef.type; if (endDefFieldType == 'date') { rowData.end = moment(rowData.end).set('minute', 1); rowData.end = new Date(rowData.end); } } } const kendoSchedulerEvent = new kendo.data.SchedulerEvent(rowData); this._eventsLoaded.events.push(app.utils.deepCopy(rowData)); kendoEvents.push(kendoSchedulerEvent); }, this ); } if (this.scheduler) { this._syncedEvents = app.utils.deepCopy(kendoEvents); this.scheduler._selectedView.options.dataSource.data(kendoEvents); this.updateUsersLegend(res); this.positionLegend(); } }, /** * Generates and adds on DOM the list of users * * @param {Object} data Response from the database */ updateUsersLegend: function(data) { let list = ''; _.each(data.users, function(user) { const userColor = app.Calendar.utils.pastelColor(user.id); list += '<li><div><span class="userDot" style="background-color:' + _.escape(userColor) + '"></span> ' + DOMPurify.sanitize(user.name, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }) + '</div></li>'; }); this.$('.usersLegend ul').html(list); }, /** * @inheritdoc */ loadData: function(options) { if (!this.scheduler) { return; } const url = app.api.buildURL(this.eventsURL); let data = {}; if (this.calendars instanceof app.data.beanCollection) { data.calendarConfigurations = this.calendars.compile(); } else { data.calendarConfigurations = this.calendars; } if (typeof this.listFilter !== 'undefined' && !_.isEmpty(this.listFilter)) { data.listFilter = this.listFilter; } if (typeof this.filterModule !== 'undefined' && !_.isEmpty(this.filterModule)) { data.filterModule = this.filterModule; } //show loading alert const visibleAlerts = app.alert.getAll(); if (!visibleAlerts['loading-calendar-events'] && !visibleAlerts['data:sync:process']) { app.alert.show('loading-calendar-events', { level: 'process', messages: app.lang.get('LBL_LOADING') }); } //set start/end dates based on the current view const schedulerView = this.scheduler.view(); if (schedulerView) { data.startDate = moment(schedulerView.startDate()).format(); data.endDate = moment(schedulerView.endDate()).set({ hour: 23, minute: 59, second: 59 }).format(); let retrieveEventsOptions = _.extend({}, options, { success: _.bind(this.populateCalendarWithData, this), error: _.bind(function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); }, this), complete: _.bind(function() { app.alert.dismiss('loading-calendar-events'); if (this.context) { this.context.trigger('calendar:loaded'); } }, this) }); app.api.call('create', url, data, retrieveEventsOptions); } }, /** * Reconfigure the calendar */ _reconfigureCalendar: function() { const params = { calendars: this.calendars.compile() }; if (params.calendars.length == 0) { this.calendarDefs = []; this.loadData(); return; } app.api.call('create', app.api.buildURL('Calendar/getCalendarDefs'), params, { success: _.bind(function(calendarDefs) { if (this.disposed) { return; } this.calendarDefs = calendarDefs; _.each(calendarDefs, function(calendarDef) { if (this.scheduler) { this.scheduler.resources[0].dataSource.add({ text: DOMPurify.sanitize(calendarDef.name, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), value: DOMPurify.sanitize(calendarDef.id, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }), color: _.escape(calendarDef.color) }); } }, this); this.loadData(); }, this), error: _.bind(function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); }, this), }); }, /** * Update templates * * Add circles on each event then put them on DOM */ _updateTemplates: function() { if (this.disposed) { return; } let cells; let viewName = this.scheduler.view().name; const viewType = this.scheduler.view().type; if (viewType === this.EXPANDED_MONTH_VIEW) { viewName = 'Month'; } else if (viewName === this.MONTH_SCHEDULE_VIEW) { viewName = 'Scheduler'; } if (viewName === this.AGENDA_VIEW) { cells = this.$('div.k-task .event-template'); } else { cells = this.$('div[role=gridcell] > .event-template'); } let prependCircle = _.bind(function(htmlContent, event) { //add text color based on backround color if (event.color) { const isWhiteColor = app.Calendar.utils.whiteColor(event.color); let color; if (isWhiteColor) { color = '#000000'; } else { color = '#FFFFFF'; } htmlContent = '<div class="templateHtmlWrapper" style="color:' + _.escape(color) + ';">' + '<div class="calendarEventBody">' + DOMPurify.sanitize(htmlContent, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }) + '</div></div>'; if (viewName === this.AGENDA_VIEW) { this.$('div.k-task[data-uid=' + event.uid + ']').each(function() { $(this).css('background-color', _.escape(event.color)); }); } const assignedUserColor = app.Calendar.utils.pastelColor(event.assignedUserId); htmlContent = $(htmlContent).prepend('<div class="previewEvent" data-module=' + _.escape(event.module) + ' data-record=' + _.escape(event.dbclickRecordId) + ' rel="tooltip" data-bs-placement="bottom"' + ' aria-haspopup="true" aria-expanded="false" data-original-title="' + _.escape(DOMPurify.sanitize(event.assignedUserName, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], })) + '"><span class="userBar" style="background-color:' + _.escape(assignedUserColor) + '"></span></div>'); _.each(event.invitees, function(invitee, idx) { if (invitee.id !== event.assignedUserId && idx < 3) { const inviteeColor = app.Calendar.utils.pastelColor(invitee.id); const inviteeName = invitee.name; htmlContent = $(htmlContent).prepend('<div class="previewEvent" data-module=' + _.escape(event.module) + ' data-record=' + _.escape(event.dbclickRecordId) + ' rel="tooltip" data-bs-placement="bottom"' + ' aria-haspopup="true" aria-expanded="false" data-original-title="' + _.escape(DOMPurify.sanitize(inviteeName, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], })) + '"><span class="userBar" style="background-color:' + _.escape(inviteeColor) + '"></span></div>'); } }, this); } return htmlContent; }, this); _.each(cells, function(cell) { let uid; if (viewName === this.AGENDA_VIEW) { uid = $(cell) .parent() .parent() .data('uid'); } else { uid = $(cell) .parent() .data('uid'); } let event = this.scheduler.occurrenceByUid(uid); switch (viewName) { case this.DAY_VIEW: $(cell).html(prependCircle(event.day_event_template, event)); break; case this.WEEK_VIEW: case this.WORK_WEEK_VIEW: $(cell).html(prependCircle(event.week_event_template, event)); break; case this.EXPANDED_MONTH_VIEW: case 'Month': $(cell).html(prependCircle(event.month_event_template, event)); break; case this.AGENDA_VIEW: $(cell).html(prependCircle(event.agenda_event_template, event)); break; case this.TIMELINE_VIEW: $(cell).html(prependCircle(event.timeline_event_template, event)); break; case this.MONTH_SCHEDULE_VIEW: case 'Scheduler': $(cell).html(prependCircle(event.schedulermonth_event_template, event)); break; } }, this); this.addTooltips(); }, /** * Add a preview of the record when the circle is clicked * * @param {Object} e */ _previewEvent: function(e) { const module = e.currentTarget.dataset.module; const recordId = e.currentTarget.dataset.record; const model = app.data.createBean(module, {id: recordId}); const windowWidth = window.innerWidth; const mainPanelWidth = app.controller.layout.$('.calendar-main-panel').width(); const previewPanelWidth = app.controller.layout.$('.preview-pane').width(); const calendarWidth = windowWidth - mainPanelWidth - previewPanelWidth; const intialCalendarWidth = windowWidth - mainPanelWidth; app.events.trigger('preview:render', model, null); app.controller.layout.$('.preview-pane').removeClass('hide'); app.controller.layout.$('.scheduler-component').css('width', calendarWidth); this.scheduler.resize(); app.controller.layout.$('.closeSubdetail').on('click', _.bind(function() { app.controller.layout.$('.preview-pane').addClass('hide'); app.controller.layout.$('.scheduler-component').css('width', intialCalendarWidth); this.scheduler.resize(); }, this)); }, /** * Add tooltips to the events */ addTooltips: function() { let kendoTooltip = this.$('.k-scheduler-content').data('kendoTooltip'); if (kendoTooltip) { kendoTooltip.hide(); kendoTooltip.destroy(); } const tooltip = app.template.getView('scheduler.tooltip', 'Calendar')(); if (this.scheduler.view().name === this.AGENDA_VIEW) { this.$('.k-scheduler-content').kendoTooltip({ filter: '.k-task', content: tooltip, position: 'left', autoHide: true, showAfter: 500, callout: false, show: _.bind(this._setTooltipContent, this), animation: { close: { duration: 0 } } } ); } else { this.$('.k-scheduler-header, .k-scheduler-content').kendoTooltip({ filter: 'div[role=gridcell]', content: tooltip, position: 'right', autoHide: true, showAfter: 500, callout: false, show: _.bind(this._setTooltipContent, this), width: 200, animation: { close: { duration: 0 } } } ); } }, /** * Set the tooltip content * * @param {Object} tooltip */ _setTooltipContent: function(tooltip) { if (tooltip.sender.target().hasClass('k-event-drag-hint') || this._isDeleteAction) { tooltip.sender.hide(); } else { const target = tooltip.sender.target(); const uid = target.data('uid'); let event = this.scheduler.occurrenceByUid(uid); const assignedUserColor = app.Calendar.utils.pastelColor(event.assignedUserId); let time = '<span>' + moment(event.start).format('ddd, MMMM D'); if (moment(event.start).format('ddd MMMM') != moment(event.end).format('ddd MMMM')) { time += ' - ' + moment(event.end).format('ddd, MMMM D'); } time += '</span><span>' + moment(event.start).format('h:mma') + ' - ' + moment(event.end).format('h:mma') + '</span>'; if (event.module == 'Calls' || event.module == 'Meetings') { tooltip.sender.content.find('.event-tooltip .tooltip-attendees').removeClass('hidden'); tooltip.sender.content.find('.event-tooltip .tooltip-description').removeClass('hideBottomBorder'); const acceptedInvitees = _.filter(event.invitees, function(invitee) { return invitee.acceptStatus == 'accept'; }); const inviteesAcceptedTemplate = app.lang.getModString('LBL_INVITEES_ACCEPTED', 'Calendar', { count: acceptedInvitees.length }); let attendees = inviteesAcceptedTemplate; let moreAttendees = ''; _.each(acceptedInvitees, function(attendee, idx) { const attendeeColor = app.Calendar.utils.pastelColor(attendee.id); const newAttendee = '<div class="attendee"><span class="userDot" style="background-color:' + _.escape(attendeeColor) + '"></span><span>' + DOMPurify.sanitize(attendee.name, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }) + '</span></div>'; if (idx < 2) { attendees += newAttendee; } else { moreAttendees += newAttendee; } }); if (acceptedInvitees.length > 2) { const moreToLoad = acceptedInvitees.length - 2; const moreToLoadLabel = app.lang.getModString('LBL_MORE_INVITEES_TO_LOAD', 'Calendar', { count: moreToLoad }); moreAttendees = '<a data-toggle="collapse" role="button" href="#attendeeCollapse-' + event.id + '">' + moreToLoadLabel + '</a> <div class="collapse" id="attendeeCollapse-' + event.id + '">' + moreAttendees + '</div>'; attendees += moreAttendees; } tooltip.sender.content.find('.event-tooltip .tooltip-attendees .category-container').html(attendees); tooltip.sender.content.find('#attendeeCollapse-' + event.id).on('show.bs.collapse', _.bind(function() { tooltip.sender.content.find('a[data-toggle=collapse]').addClass('hide'); }, this)); } else { tooltip.sender.content.find('.event-tooltip .tooltip-attendees').addClass('hidden'); tooltip.sender.content.find('.event-tooltip .tooltip-description').addClass('hideBottomBorder'); } const sanitizedEventName = DOMPurify.sanitize(event.name, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }); const sanitizedEventTooltip = DOMPurify.sanitize(event.event_tooltip, { FORBID_ATTR: ['style'], FORBID_TAGS: ['style'], }); tooltip.sender.content.find('.event-tooltip .tooltip-header .category-container').html(sanitizedEventName); tooltip.sender.content.find('.event-tooltip .tooltip-time .category-container').html(time); tooltip.sender.content.find('.event-tooltip .tooltip-description .category-container') .html(sanitizedEventTooltip); tooltip.sender.content.find('.event-tooltip .tooltip-header .userDot') .css('background-color', _.escape(assignedUserColor)); tooltip.sender.content.parent().css('box-shadow', 'none'); tooltip.sender.content.parent().parent().css('margin-left', '0px'); } }, /** * Render * * Render this view in DOM * and eventually start initializing Kendo Scheduler * * @inheritdoc */ _render: function() { this._super('_render'); const params = { calendars: this.calendars.compile() }; if (params.calendars.length == 0 || (_.isNull(this.scheduler) && this.options.skipFetch)) { this.calendarDefs = []; this._createCalendar(); return; } app.api.call('create', app.api.buildURL('Calendar/getCalendarDefs'), params, { success: _.bind(function(calendarDefs) { this.calendarDefs = calendarDefs; this._createCalendar(); }, this), error: _.bind(function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); }, this), }); }, /** * Initializes Kendo Scheduler component */ _initializeScheduler: function() { let rawResources = []; _.each(this.calendarDefs, function(calendar) { rawResources.push({ text: calendar.id, value: calendar.id, color: _.escape(calendar.color) }); }); let resourcesDS = new kendo.data.DataSource({ data: rawResources }); //considering the autoBind is set to false, we have to manually fetch resources ds resourcesDS.fetch(); let kendoOptions = _.extend({}, { date: new Date(), timezone: app.user.attributes.preferences.timezone, currentTimeMarker: { useLocalTimezone: false }, startTime: new Date('2000/1/1 00:00 AM'), toolbar: ['pdf'], messages: { pdf: app.lang.getModString('LBL_CALENDAR_PDF_EXPORT', 'Calendar'), showWorkDay: app.lang.getModString('LBL_CALENDAR_SHOW_FULL_DAY', 'Calendar') }, pdf: { fileName: app.lang.getModString('LBL_CALENDAR_CALENDAR_EXPORT', 'Calendar') + '.pdf', landscape: false, calculatePaperSize: true, calendarIdRef: this._schedulerCssId }, pdfExport: _.debounce(this._pdfExport, 0), eventTemplate: '<div class="event-template"></div>', workDays: [1,2,3,4,5], views: this.views, dataSource: new kendo.data.SchedulerDataSource({ data: [] }), autoBind: false, selectable: true, editable: { confirmation: false, //default delete confirmation, resize: true, move: true }, edit: _.bind(this._eventDoubleClickHandler, this), moveStart: _.bind(this._moveResizeStartHandler, this), moveEnd: _.debounce(_.bind(this._moveResizeHandler, this), 0), resizeStart: _.bind(this._moveResizeStartHandler, this), resizeEnd: _.debounce(_.bind(this._moveResizeHandler, this), 0), remove: _.bind(this._deleteHandler, this), dataBound: _.bind(this._updateTemplates, this), navigate: _.bind(this._navigateHandler, this), workDayStart: this._getBusinessHours('start'), workDayEnd: this._getBusinessHours('end'), resources: [ { field: 'calendarId', title: 'calendarId', dataSource: resourcesDS }, ], majorTimeHeaderTemplate: this._getMajorTimeHeaderTemplate(), }, this.customKendoOptions); const selectedView = _.find(this.views, (view) => view.selected).type; if (selectedView !== this.MONTH_SCHEDULE_VIEW) { kendoOptions.dateHeaderTemplate = this._getDateHeaderTemplate( _.find(this.views, (view) => view.selected).type ); } if (selectedView !== this.EXPANDED_MONTH_VIEW) { kendoOptions.selectedDateFormat = this._getSelectedDateFormat(selectedView); } //finally, kendo initialization this.$('#' + this._schedulerCssId).kendoScheduler(kendoOptions); this.scheduler = this.$('#' + this._schedulerCssId) .data('kendoScheduler'); this.toolbarView = app.view.createView({ name: 'scheduler-toolbar', type: 'scheduler-toolbar', module: 'Calendar', }); this.toolbarView.views = this.views; this.toolbarView.formattedDate = this.scheduler._model.formattedDate; this.toolbarView.formattedShortDate = this.scheduler._model.formattedShortDate; this.toolbarView.render(); this.$('.k-scheduler-toolbar.k-toolbar').html(this.toolbarView.$el.html()); this.scheduler._model.bind('change', _.bind(function(e) { if (e.field == 'formattedDate') { this.$('.k-lg-date-format').html(e.sender.source.formattedDate); this.$('.k-sm-date-format').html(e.sender.source.formattedShortDate); } }, this)); this.scheduler.wrapper.on('mousedown.kendoScheduler', _.debounce(_.bind(function(e) { if (e.target.hasAttribute('role') && e.target.getAttribute('role') === 'gridcell') { this.context.trigger('scheduler:view:changed'); } }, this), 0)); //we need to wait until the dashlet is loaded in order to have a context menu _.defer(_.bind(function() { this.$('#' + this._schedulerCssId).find('.k-dropdown').select2({ minimumResultsForSearch: -1, }); $('#context_menu_' + this._schedulerCssId).kendoContextMenu({ filter: '.k-scheduler-table', showOn: 'dblclick', open: _.bind(this._contextMenuOpen, this), select: _.bind(this._contextMenuSelect, this), target: '#' + this._schedulerCssId }); }, this)); this.trigger('calendar:initialized'); }, /** * PDF export * * @param {Object} e */ _pdfExport: function(e) { const calendarFont = app.user.getPreference('sugarpdf_pdf_calendar_font_name_data'); const notoSansFont = 'NotoSansSC-Regular'; const currentFont = this.wrapper.css('font-family'); let cssFontFamily = ''; if (calendarFont === notoSansFont) { cssFontFamily += notoSansFont; } //refreshLayout adds heights to elements that needs to be removed _.defer(_.bind(function() { this.wrapper.find('.k-scheduler-content').css('height', ''); this.wrapper.find('.k-scheduler-times').css('height', ''); this.wrapper.find('.k-scheduler-table').css('height', ''); }, this)); e.sender.wrapper.css('font-family', cssFontFamily); e.promise.done(_.bind(function() { this.element.css('font-family', currentFont); }, this)); }, /** * Returns if the calendar is created * * @return {boolean} */ _calendarIsCreated: function() { if (typeof kendo == 'undefined' || !(this.scheduler instanceof kendo.ui.Scheduler)) { return false; } return true; }, /** * Bind legend position event */ _bindLegendEvent: function() { $(window).on('resize', this.positionLegend); }, /** * Position the legend dropdown */ positionLegend: function() { if (_.isEmpty(this.$el)) { return; } _.each(this.$('.dropdown-menu'), function(dropdownMenu) { const menuWidth = $(dropdownMenu).width(); const parentWidth = $(dropdownMenu).parent().width(); const parentOffset = $(dropdownMenu).parent().offset(); const leftOffSet = 27; const topOffset = 30; $(dropdownMenu).css({ left: parentOffset.left - menuWidth + parentWidth - leftOffSet, top: parentOffset.top + topOffset }); },this); }, /** * Create the Kendo component */ _createCalendar: function() { if (this._calendarIsCreated()) { return; } this.views = this.setupCalendarViews(); if (typeof kendo === 'object') { this.culturePreferences(); this._initializeScheduler(); this._bindExportEvent(); this._bindLegendEvent(); } else { $.getScript('cache/include/javascript/sugar_grp_calendar.js', _.bind(function() { this.culturePreferences(); this._initializeScheduler(); this._bindExportEvent(); this._bindLegendEvent(); }, this)); } }, /** * Open drawer to edit the record * * @param {Object} data */ _editHandlerSuccessCallback: function(data) { const module = data.module; app.drawer.open( { layout: 'create', context: { layoutName: 'create', create: true, module: data.module, model: data } }, _.bind(function() { this.loadData(); if (this.isPageWithSubpanels()) { this.refreshSubpanels(module); } },this) ); }, /** * Trigger a route change to the specified url * * @param {string} url */ _navigate: function(url) { _.defer(function() { app.router.navigate(url, { trigger: true }); }); }, /** * Handler for double click event * * @param {Object} e */ _eventDoubleClickHandler: function(e) { e.preventDefault(); this._selectedState = e.event; let moduleMeta = app.metadata.getModule(e.event.module); let url; let model; if (e.event.isNew()) { return; } let actionOnDbClick = this.getActionOnDbClick(e.event); if (_.isEmpty(e.event.dbclickRecordId)) { const moduleName = app.lang.getModuleName(e.event.module); const relatedModule = app.lang.getModuleName(actionOnDbClick.module); const message = app.lang.getModString('LBL_NAVIGATE_TO_RECORD_WARNING', 'Calendar', { relatedModule: relatedModule.toLowerCase(), moduleName: moduleName.toLowerCase() }); app.alert.show('navigation-record', { level: 'confirmation', messages: message, autoClose: false, onConfirm: _.bind(function() { this.navigateToRecord(e.event); }, this), }); return; } if (actionOnDbClick.action == 'detail') { if (moduleMeta.isBwcEnabled) { url = '#bwc/index.php?module=' + actionOnDbClick.module + '&action=DetailView&record=' + e.event.dbclickRecordId; this._navigate(url); } else { url = '#' + actionOnDbClick.module + '/' + e.event.dbclickRecordId; this._navigate(url); } } else if (actionOnDbClick.action == 'detail-newtab') { if (moduleMeta.isBwcEnabled) { url = app.utils.getSiteUrl() + '#bwc/index.php?module=' + actionOnDbClick.module + '&action=DetailView&record=' + e.event.dbclickRecordId; window.open(url); } else { url = app.utils.getSiteUrl() + '#' + actionOnDbClick.module + '/' + e.event.dbclickRecordId; window.open(url); } } else if (actionOnDbClick.action == 'edit') { moduleMeta = app.metadata.getModule(actionOnDbClick.module); if (this.isAllowed('allow_update', e.event)) { if (moduleMeta.isBwcEnabled) { url = '#bwc/index.php?module=' + actionOnDbClick.module + '&action=EditView&record=' + e.event.dbclickRecordId; this._navigate(url); } else { model = app.data.createBean(actionOnDbClick.module, { id: e.event.dbclickRecordId, module: actionOnDbClick.module }); model.fetch({ params: { view: 'record' }, success: _.bind(this._editHandlerSuccessCallback, this), error: function() { app.alert.show('error-fetch-model', { level: 'error', messages: app.lang.getModString('LBL_CALENDAR_ERROR_FETCH_MODEL', 'Calendar'), autoClose: false }); } }); } } else { app.alert.show('not-allowed', { level: 'warning', messages: app.lang.getModString('LBL_CALENDAR_NOT_ALLOWED_TO_EDIT', 'Calendar'), autoClose: true }); } } }, /** * Get informations about double click action of this event * * @param {Object} event * @return {Object} */ getActionOnDbClick: function(event) { let dblClick; let calendar; const currentUserType = app.user.get('type'); const calendarId = event.calendarId; calendar = _.find(this.calendarDefs, function(calendar) { return calendar.calendarId === calendarId; }); dblClick = calendar.dblclick_event.split(':'); if (dblClick.length == 3) { dblClick = { action: dblClick[0], module: dblClick[1], id: dblClick[2] == 'id' ? event.recordId : event.dbclickRecordId }; } if (dblClick.module == 'self') { dblClick.module = calendar.module; } if (currentUserType == 'user' && dblClick.module == 'Users') { dblClick.module = 'Employees'; } return dblClick; }, /** * Build model with proper default fields * * @param {Object} calendarDef * @param {Object} view * @param {Object} state * @return {Data.Bean} A new instance of a bean. */ buildModelWithProperDefaultFields: function(calendarDef, view, state) { let model; const startField = calendarDef.start_field; let dataPrefill = { module: calendarDef.module }; dataPrefill[startField] = moment(state.start).format(); let endField = calendarDef.end_field; //in Calls & Meetings modules, end date is calculated based on durations if (calendarDef.module == 'Calls' || calendarDef.module == 'Meetings') { dataPrefill.duration_minutes = 30; endField = 'date_end'; } if (!_.isEmpty(endField)) { dataPrefill[endField] = moment(state.start).add(30, 'minutes').format(); } let moduleMetadata = app.metadata.getModule(calendarDef.module); let fieldsMetadata = moduleMetadata.fields; let startDef = fieldsMetadata[calendarDef.start_field]; let endDef = fieldsMetadata[calendarDef.end_field]; if (typeof startDef != 'undefined') { let startDefFieldType = startDef.dbType || startDef.dbtype || startDef.type; if (startDefFieldType == 'date') { dataPrefill[calendarDef.start_field] = moment(state.start).format('YYYY-MM-DD'); } } if (typeof endDef != 'undefined') { let endDefFieldType = endDef.dbType || endDef.dbtype || endDef.type; if (endDefFieldType == 'date') { dataPrefill[calendarDef.end_field] = moment(state.start).format('YYYY-MM-DD'); } } model = app.data.createBean(calendarDef.module, dataPrefill); return model; }, /** * Open list with available calendars * * @param {Object} e */ _contextMenuOpen: function(e) { //on Agenda View edit event is not triggered so we have to trigger it here const schedulerView = this.scheduler.view(); if (schedulerView.name === this.AGENDA_VIEW) { const uid = $(e.event.target) .closest('.k-task') .data('uid'); const event = this.scheduler.occurrenceByUid(uid); this.scheduler.editEvent(event); e.preventDefault(); return; } const state = this._selectedState; if (state && typeof state.isNew == 'function' && state.isNew()) { let menu = e.sender; menu.remove('.contextCalendarItem'); //get all calendar defs of My Calendars in this scheduler. Only one per module let calendars; if (this.location == 'dashboard') { calendars = this.calendarDefs; } else { calendars = this.context.get('myAvailableCalendars'); } const availableCalendars = _.filter(calendars, function(calendar) { return app.acl.hasAccess('view', calendar.module) && calendar.allow_create; }); _.each( availableCalendars, function(calendar) { let text = ''; if (calendar.module === 'KBContents') { text = app.lang.getModString('LNK_NEW_KBCONTENT_TEMPLATE', calendar.module); } else { let createLabel = 'LNK_NEW_' + calendar.objName.toUpperCase(); text = app.lang.get(createLabel, calendar.module); if (text === createLabel) { createLabel = 'LNK_NEW_RECORD'; text = app.lang.getModString(createLabel, calendar.module); } } menu.append([{ text: text, cssClass: 'contextCalendarItem', //used to remove old items uid: calendar.module }]); }, this ); } else { e.preventDefault(); } }, /** * Context menu select * * @param {Object} e */ _contextMenuSelect: function(e) { const state = this._selectedState; const selectedIdx = $(e.item).index(); //get all calendar defs of My Calendars in this scheduler. Only one per module let calendars; if (this.location == 'dashboard') { calendars = this.calendarDefs; } else { calendars = this.context.get('myAvailableCalendars'); } const availableCalendars = _.filter(calendars, function(calendar) { return app.acl.hasAccess('view', calendar.module); }); const calendar = availableCalendars[selectedIdx]; const selectedModule = calendar.module; if (selectedModule === 'Quotes') { app.router.navigate('Quotes/create', {trigger: true}); } else { //open drawer with default values based on module const model = this.buildModelWithProperDefaultFields(calendar, this, state); app.drawer.open({ layout: 'create', context: { layoutName: 'create', create: true, module: selectedModule, model: model }}, _.bind(function(context, model) { if (!(model instanceof app.data.beanModel)) { return; //no record created } this.loadData(); if (this.isPageWithSubpanels()) { this.refreshSubpanels(model.module); } }, this) ); } }, /** * Move event is starting * * @param {Object} e */ _moveResizeStartHandler: function(e) { if (this.isAllowed('allow_update', e.event)) { this.setCssMarkerToHideTooltip(e); } else { let errorMessage = app.lang.getModString('LBL_CALENDAR_NOT_ALLOWED_TO_MOVE', 'Calendar'); if (arguments.callee.name == 'resizeHandler') { errorMessage = app.lang.getModString('LBL_CALENDAR_NOT_ALLOWED_TO_CHANGE', 'Calendar'); } app.alert.show('not-allowed', { level: 'warning', messages: errorMessage, autoClose: true }); e.preventDefault(); } }, /** * Hide tooltip when the event is resized * * @param {Object} e */ setCssMarkerToHideTooltip: function(e) { let element = this.$('[data-uid=' + e.event.uid + ']'); element.addClass('k-event-drag-hint'); }, /** * Move event was done * * @param {Object} e */ _moveResizeHandler: function(e) { this.eventInChange = e; let clonedEvent = app.utils.deepCopy(e.event); let schedulerViewType = this.scheduler._selectedView.name; schedulerViewType = this.getViewType(schedulerViewType); if (schedulerViewType === this.EXPANDED_MONTH_VIEW || schedulerViewType === this.MONTH_SCHEDULE_VIEW) { let originalEvent = this._eventsLoaded.events.find((evt) => evt.recordId === e.event.recordId); if (!originalEvent) { e.preventDefault(); } // When resizing elements in the month/scheduler view, // the end date of the event will come as 00:00 the next day // We will need to turn that back in order to use the correct day of the month let startDate = moment(e.start); let endDate = moment(e.end); let origStartDate = moment(originalEvent.start); let origEndDate = moment(originalEvent.end); let calendarDef = _.find(this.calendarDefs, function(calendarDef) { return calendarDef.calendarId === originalEvent.calendarId; }); let moduleMetadata = app.metadata.getModule(calendarDef.module); let fieldsMetadata = moduleMetadata.fields; let endDef = fieldsMetadata[calendarDef.end_field]; let endDefFieldType = ''; if (typeof endDef != 'undefined') { endDefFieldType = endDef.dbType || endDef.dbtype || endDef.type; } const dateFormat = 'YYYYMMDD'; if (origStartDate.format(dateFormat) === startDate.format(dateFormat)) { if (origStartDate.format(dateFormat) === origEndDate.format(dateFormat) && startDate.format(dateFormat) !== endDate.format(dateFormat) && endDefFieldType != 'date') { endDate.subtract(1, 'days'); } } else if (schedulerViewType === this.MONTH_SCHEDULE_VIEW && originalEvent.isAllDay) { endDate.subtract(1, 'days'); } let newStart = origStartDate.year(startDate.year()).month(startDate.month()).date(startDate.date()); let newEnd = origEndDate.year(endDate.year()).month(endDate.month()).date(endDate.date()); e.event.start = newStart.toDate(); e.event.end = newEnd.toDate(); e.event.dirtyFields.start = true; e.event.dirtyFields.end = true; clonedEvent.start = newStart.toDate(); clonedEvent.end = newEnd.toDate(); } if (clonedEvent.module == 'Calls' || clonedEvent.module == 'Meetings') { this.confirmationPopupView = app.view.createView({ context: this.context, type: 'confirm-invitation', event: clonedEvent, }); $('#alerts').append(this.confirmationPopupView.$el); this.confirmationPopupView.render(); } else { app.alert.show('moveResizeHandlerAlert', { level: 'confirmation', messages: app.lang.getModString('LBL_CALENDAR_CONFIRM_CHANGE_RECORD', 'Calendar'), autoClose: false, onConfirm: _.bind(function() { this.saveRecord(clonedEvent); }, this), onCancel: _.bind(this.cancelSave, this) }); } }, /** * Update event * * @param {Object} eventData */ updateEvent: function(eventData) { let data = { recordId: eventData.recordId, module: eventData.module, calendarId: eventData.calendarId, start: moment(eventData.start).format(), end: moment(eventData.end).format() }; if (typeof eventData.sendInvites == 'boolean') { data.sendInvites = true; } app.alert.show('move_resize_event', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false }); //solve the case when multiple move/resize actions are made quicker then requests this.scheduler.dataSource.trigger('progress'); let url = app.api.buildURL('Calendar/updateRecord') + '/' + this.eventInChange.event.recordId; app.api.call('create', url, data, { success: _.bind(function(updated) { if (this.disposed) { return; } if (updated) { app.alert.show('successUpdateRecordAlert', { level: 'success', messages: app.lang.getModString('LBL_CALENDAR_RECORD_SAVED', 'Calendar'), autoClose: true }); if (this.isPageWithSubpanels()) { this.refreshSubpanels(this.eventInChange.event.module); app.events.trigger('multidateFieldChanged'); } } else { app.alert.show('update-record-restricted', { level: 'error', messages: app.lang.getModString('LBL_CALENDAR_RESTRICT_UPDATE', 'Calendar'), autoClose: true }); } }, this), error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); }, complete: _.bind(function() { app.alert.dismiss('move_resize_event'); this.eventInChange = null; app.events.trigger('calendar:reload'); }, this) }); }, /** * Delete handler * * @param {Object} e */ _deleteHandler: function(e) { e.preventDefault(); if (this.isAllowed('allow_delete', e.event)) { this._deletedEvent = e; this._isDeleteAction = true; app.alert.show('confirmRemoveRecordAlert', { level: 'confirmation', messages: app.lang.getModString('LBL_CALENDAR_CONFIRM_DELETE_RECORD', 'Calendar'), autoClose: false, onConfirm: _.bind(this.deleteEvent, this), onCancel: _.bind(this.cancelDelete, this) }); } else { app.alert.show('not-allowed', { level: 'warning', messages: app.lang.getModString('LBL_CALENDAR_NOT_ALLOWED_TO_DELETE', 'Calendar'), autoClose: true }); } }, /** * Delete event */ deleteEvent: function() { app.alert.show('event-deleted', { level: 'process', messages: app.lang.getModString('LBL_DELETING', 'Calendar') }); app.api.call( 'delete', app.api.buildURL(this._deletedEvent.event.module) + '/' + this._deletedEvent.event.recordId, null, { success: _.bind(this._deleteEventSuccessCallback, this), error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } } ); }, /** * Event delete success callback */ _deleteEventSuccessCallback: function() { app.alert.dismiss('event-deleted'); app.alert.show('deleteAlert', { level: 'success', messages: app.lang.getModString('LBL_CALENDAR_RECORD_DELETED', 'Calendar'), autoClose: true }); app.events.trigger('calendar:reload'); if (this.isPageWithSubpanels()) { this.refreshSubpanels(this._deletedEvent.event.module); } this._isDeleteAction = false; }, /** * Mark delete action */ cancelDelete: function() { this._isDeleteAction = false; }, /** * Offline refresh data source * * Loads events from last fetch (cached on this view), on the calendar component */ offlineRefreshDataSource: function() { // Need to patch end/start dates this._syncedEvents.forEach((evt) => { evt.start = moment(evt.start).toDate(); evt.end = moment(evt.end).toDate(); }); this.scheduler._selectedView.options.dataSource.data(this._syncedEvents); }, /** * Refresh collections on all subpanels of the given module * * @param {string} module */ refreshSubpanels: function(module) { const filterpanel = app.controller.layout .getComponent('sidebar') .getComponent('main-pane') .getComponent('filterpanel'); const subpanels = filterpanel.componentsList.subpanels; const targetSubpanels = _.filter(subpanels._components, function(subpanel) { return subpanel.module === module; }); if (targetSubpanels.length > 0) { _.each(targetSubpanels, function(subpanel) { subpanel.collection.fetch(); }); } }, /** * Returns whether the current page contains subpanels * * @return {boolean} */ isPageWithSubpanels: function() { let filterPanel; const sidebar = app.controller.layout.getComponent('sidebar'); if (sidebar) { const mainPane = sidebar.getComponent('main-pane'); if (mainPane) { filterPanel = mainPane.getComponent('filterpanel'); } } if (filterPanel instanceof app.view.Layout) { if (filterPanel.componentsList.subpanels) { return true; } } return false; }, /** * Navigate handler * * @param {Object} options */ _navigateHandler: function(options) { if (options.action === 'changeView') { if (options.view === this.MONTH_SCHEDULE_VIEW) { delete this.scheduler.options.dateHeaderTemplate; } else { this.scheduler.options.dateHeaderTemplate = this._getDateHeaderTemplate(options.view); } if (options.view === this.EXPANDED_MONTH_VIEW) { delete this.scheduler.options.selectedDateFormat; } else { this.scheduler.options.selectedDateFormat = this._getSelectedDateFormat(options.view); } } this._navigationLoadData(options); }, /** * Load data after navigation * * Wait for UI navigation to complete, then load data if not already loaded * * @param {Object} options */ _navigationLoadData: _.debounce(function(options) { let schedulerView = this.scheduler.view(); const intervalDates = { start: moment(schedulerView.startDate()).format('YYYY-MM-DD'), end: moment(schedulerView.endDate()).format('YYYY-MM-DD') }; let selectedDate = new Date(options.date.getTime());; schedulerView = this.scheduler.view(); schedulerView.select({ events: [], start: selectedDate, end: selectedDate, groupIndex: 0 }); this.context.trigger('scheduler:view:changed'); let intervalStart = moment(intervalDates.start).format('YYYY-MM-DD'); let intervalEnd = moment(intervalDates.end).format('YYYY-MM-DD'); if ( intervalStart != moment(this._eventsLoaded.startDate).format('YYYY-MM-DD') || intervalEnd != moment(this._eventsLoaded.endDate).format('YYYY-MM-DD') ) { this.loadData(); } const viewName = schedulerView.options.schedulerViewName; app.cache.set(this.keyToStoreCalendarView, viewName); }, 0), /** * Returns whether an action like create/update/delete is allowed for a calendar event * * @param {string} action * @param {Object} event * @return {boolean} */ isAllowed: function(action, event) { let calendar = _.find(this.calendarDefs, function(calendar) { return calendar.id === event.calendarId; }); if (typeof calendar == 'undefined') { return false; } else { return calendar[action] || false; } }, /** * Transforms a View Name into an accepted View Name format * * @param {string} viewName * @return {string} */ getViewType: function(viewName) { let type; switch (viewName) { case this.DAY_VIEW: case 'DayView': type = this.DAY_VIEW; break; case this.WEEK_VIEW: case 'WeekView': type = this.WEEK_VIEW; break; case this.WORK_WEEK_VIEW: case 'WorkWeekView': type = this.WORK_WEEK_VIEW; break; case 'month': case this.EXPANDED_MONTH_VIEW: case 'Month': type = this.EXPANDED_MONTH_VIEW; break; case this.AGENDA_VIEW: type = this.AGENDA_VIEW; break; case this.TIMELINE_VIEW: case 'TimelineView': type = this.TIMELINE_VIEW; break; case this.MONTH_SCHEDULE_VIEW: case 'Scheduler': type = this.MONTH_SCHEDULE_VIEW; break; } return type; }, /** * Transforms a view title into an accepted View Title format * * @param {string} viewName * @return {string} */ getViewTitle: function(viewName) { let title; switch (viewName) { case this.DAY_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_DAY', 'Calendar'); break; case this.WEEK_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_WEEK', 'Calendar'); break; case this.WORK_WEEK_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_WORKWEEK', 'Calendar'); break; case 'month': case 'Month': case this.EXPANDED_MONTH_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_MONTH', 'Calendar'); break; case this.AGENDA_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_AGENDA', 'Calendar'); break; case this.TIMELINE_VIEW: title = app.lang.getModString('LBL_CALENDAR_VIEW_TIMELINE', 'Calendar'); break; case this.MONTH_SCHEDULE_VIEW: case 'Scheduler': title = app.lang.getModString('LBL_CALENDAR_VIEW_SCHEDULERMONTH', 'Calendar'); break; } return title; }, /** * Bind export events * * After creating the scheduler, and it's on DOM, we need to attach the handlers for Export, Publish and Settings */ _bindExportEvent: function() { this.$('.export_icalendar') .on( 'click', _.bind(this._getICalIdentifier, this) ); this.$('.publish_icalendar') .on( 'click', _.bind(this._publishCalendar, this) ); this.$('.userSettings') .on( 'click', _.bind(this._userSettings, this) ); }, /** * Get iCal id * * Get the ICal id where we store calendar definitions */ _getICalIdentifier: function() { const postData = { calendarConfigurations: this.calendars.compile() }; app.api.call('create', app.api.buildURL('Calendar/getICalConfigurationsUID'), postData, { success: _.bind(function successSaveCalendarConfigurations(data) { if (this.disposed) { return; } if (_.isEmpty(data.key)) { app.alert.show('set-publish-key', { level: 'info', messages: app.lang.getModString('LBL_CALENDAR_CONFIGURE_PUBLISH_KEY', 'Calendar'), autoClose: false }); return; } this._exportCalendar(data); }, this), error: function errorSaveCalendarConfigurations(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }); }, /** * Export ICal calendar * * @param {Object} options */ _exportCalendar: function(options) { let sugarLocation = app.utils.getSiteUrl(); sugarLocation = sugarLocation .replace(/#$/, '') .replace(/(index\.php)$/, '') .replace(/\/$/, ''); const url = sugarLocation + '/' + app.config.serverUrl + '/Calendar/getICalData?type=ics&user_id=' + app.user.id + '&key=' + options.key + '&calendarsUID=' + options.calendarConfigurationUID + '&export=1'; window.open(url); }, /** * Publish ICal calendar */ _publishCalendar: function() { this.addPublishModalElementOnDom(); const postData = { calendarConfigurations: this.calendars.compile() }; //obtain a publish key app.api.call('create', app.api.buildURL('Calendar/getICalPublishUrl'), postData, { success: function(data) { if (this.disposed) { return; } if (data === 'empty_publish_key') { app.alert.show('set-publish-key', { level: 'info', messages: app.lang.getModString('LBL_CALENDAR_CONFIGURE_PUBLISH_KEY', 'Calendar'), autoClose: false }); return; } //update modal $('[data-content=publish-icalendar-modal]') .find('input') .attr('value', data); //open modal $('[data-content=publish-icalendar-modal]').modal('show'); //on close, remove it from body in order to not mess with other modals $('[data-content=publish-icalendar-modal]') .on('hidden.bs.modal', function modalCloseHandler() { $('[data-content=publish-icalendar-modal]').remove(); }); }, error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }); }, /** * User settings * * Added possibility to set business hours */ _userSettings: function() { this.addBusinessHoursElementOnDom(); //update modal const calendarBusinessHours = app.user.lastState.get(this.keyToStoreCalendarBusinessHours); $('[name=startTime]').timepicker({ timeFormat: app.user.getPreference('timepref'), disableTextInput: true }); $('[name=endTime]').timepicker({ timeFormat: app.user.getPreference('timepref'), disableTextInput: true }); if (calendarBusinessHours) { $('[data-content=business-hours-modal]') .find('[name=startTime]') .timepicker('setTime', calendarBusinessHours.start); $('[data-content=business-hours-modal]') .find('[name=endTime]') .timepicker('setTime', calendarBusinessHours.end); } //open modal $('[data-content=business-hours-modal]').modal('show'); //on close, remove it from body in order to not mess with other modals $('[data-content=business-hours-modal]') .on('hidden.bs.modal', function modalCloseHandler() { $('[data-content=business-hours-modal]').remove(); }); $('[name=saveHours]').on('click', _.bind(function() { this._saveBusinessHours(); }, this)); }, /** * Add publish modal element on DOM */ addPublishModalElementOnDom: function() { const modal = app.template.getView('scheduler.publish-modal', 'Calendar'); $('body').append(modal()); }, /** * Add business hours modal element on DOM */ addBusinessHoursElementOnDom: function() { const modal = app.template.getView('scheduler.business-hours', 'Calendar'); $('body').append(modal()); }, /** * Setup the culture preference */ culturePreferences: function() { const weekStart = parseInt(app.user.getPreference('first_day_of_week'), 10); kendo.culture('en-US'); if (weekStart) { kendo.culture().calendar.firstDay = weekStart; } }, /** * Save business hours to local storage */ _saveBusinessHours: function() { let startTime = $('[name=startTime]').val(); let endTime = $('[name=endTime]').val(); if (startTime && endTime) { startTime = app.date(startTime, app.date.convertFormat(app.user.getPreference('timepref'))); endTime = app.date(endTime, app.date.convertFormat(app.user.getPreference('timepref'))); const businessHours = { start: startTime.format('HH:mm'), end: endTime.format('HH:mm') }; app.user.lastState.set(this.keyToStoreCalendarBusinessHours, businessHours); $('[data-content=business-hours-modal]').remove(); $('.modal-backdrop').remove(); //need to remove the old calendar and recreate it with the new options if (this.toolbarView) { this.toolbarView.dispose(); this.$('.k-scheduler-toolbar.k-toolbar').remove(); } if (this.confirmationPopupView) { this.confirmationPopupView.dispose(); } this.stopListening(this.context, 'change:calendars'); this.$('#' + this._schedulerCssId).find('.k-scheduler-layout').remove(); this.$('#' + this._schedulerCssId).find('.k-scheduler-footer').remove(); this.cleanupScheduler(); this._createCalendar(); app.events.trigger('calendar:reload'); } }, /** * Set business hours */ _setBusinessHours: function() { let businessHours = app.user.lastState.get(this.keyToStoreCalendarBusinessHours); if (!businessHours) { businessHours = { start: '09:00', end: '17:00' }; app.user.lastState.set(this.keyToStoreCalendarBusinessHours, businessHours); } }, /** * Get business hours * * @param {string} momentIdentifier * @return {Object} */ _getBusinessHours: function(momentIdentifier) { const businessHours = app.user.lastState.get(this.keyToStoreCalendarBusinessHours); return new Date(`2000-01-01 ${businessHours[momentIdentifier]}`); }, /** * Get date header template * * @param {string} view * @return {string} */ _getDateHeaderTemplate: function(view) { let dataHeaderTemplate = ''; let kendoDateFormat = ''; if (view === this.TIMELINE_VIEW) { kendoDateFormat = app.Calendar.utils.getKendoDateMapping( app.user.getPreference('datepref'), 'dayAndVerboseMonth' ); dataHeaderTemplate = kendo.template( `<span class='k-link k-nav-day'>#=kendo.format('{0:${kendoDateFormat}}', date)#</span>` ); } else if (view === this.MONTH_SCHEDULE_VIEW) { kendoDateFormat = app.Calendar.utils.getKendoDateMapping(app.user.getPreference('datepref'), 'dayAndMonth'); dataHeaderTemplate = kendo.template( `<span class='k-link k-nav-day'>#=kendo.format('{0:m}', date)#</span>` ); } else { kendoDateFormat = app.Calendar.utils.getKendoDateMapping(app.user.getPreference('datepref'), 'dayAndMonth'); dataHeaderTemplate = kendo.template(`#var dateString = isMobile ? kendo.toString(date,'ddd')[0] : kendo.toString(date,'ddd ${kendoDateFormat}'); #<span class='k-link k-nav-day'>#=dateString#</span>`); } return dataHeaderTemplate; }, /** * Get major time header template * * @return {string} */ _getMajorTimeHeaderTemplate: function() { let kendoTimeFormat = app.Calendar.utils.getKendoTimeMapping(app.user.getPreference('timepref')); return `#=kendo.toString(date, '${kendoTimeFormat}')#`; }, /** * Get selected date format * * @param {string} view * @return {string} */ _getSelectedDateFormat: function(view) { const userDatePref = app.Calendar.utils.getKendoDateMapping( app.user.getPreference('datepref'), 'fullVerboseMonth' ); let selectedDateFormat = ''; switch (view) { case this.DAY_VIEW: case this.TIMELINE_VIEW: selectedDateFormat = `{0:dddd, ${userDatePref}}`; break; case this.WORK_WEEK_VIEW: case this.WEEK_VIEW: case this.AGENDA_VIEW: case this.MONTH_SCHEDULE_VIEW: selectedDateFormat = `{0:dddd, ${userDatePref}} - {1:dddd, ${userDatePref}}`; break; } return selectedDateFormat; }, /** * Navigate to record view * * @param {Object} event */ navigateToRecord: function(event) { let url; const moduleMeta = app.metadata.getModule(event.module); if (moduleMeta.isBwcEnabled) { url = '#bwc/index.php?module=' + event.module + '&action=DetailView&record=' + event.recordId; this._navigate(url); } else { url = '#' + event.module + '/' + event.recordId; this._navigate(url); } }, /** * Destroy Kendo Scheduler and remove legend positioning event */ cleanupScheduler: function() { if (typeof kendo != 'undefined' && this.scheduler instanceof kendo.ui.Scheduler) { this.scheduler.destroy(); this.scheduler = null; } }, /** * @inheritdoc */ _dispose: function() { if (this.toolbarView) { this.toolbarView.dispose(); } if (this.confirmationPopupView) { this.confirmationPopupView.dispose(); } this.stopListening(this.context); this.cleanupScheduler(); this._super('_dispose'); } }) }, "confirm-invitation": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Confirm-invitation View (base) className: 'alert-wrapper calendar-alert', extendsFrom: 'AlertView', /** * @override */ initialize: function(options) { this.events = _.extend({}, this.events, { 'click [data-action=confirm]': 'saveClicked', 'click [data-action=saveAndSendEmails]': 'saveAndSendEmailsClicked' }); this.context = options.context; this._super('initialize', [options]); this.name = 'confirm-invitation'; }, /** * @override */ _getAlertTemplate: function(options, templateOptions) { options = options || {}; let template = app.template.getView('confirm-invitation.email', this.options.module); return template(); }, /** * @override */ cancelClicked: function(event) { $('.calendar-alert').remove(); this.context.trigger('button:cancel:click'); }, /** * @override */ saveClicked: function(event) { $('.calendar-alert').remove(); this.context.trigger('button:save:click', this.options.event); }, /** * Save and send emails handler */ saveAndSendEmailsClicked: function() { $('.calendar-alert').remove(); this.context.trigger('button:saveAndSendInvites:click', this.options.event); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "main-scheduler": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Calendar.MainSchedulerLayout * @alias SUGAR.App.view.layouts.BaseCalendarMainSchedulerLayout * @extends View.Layouts.Base.BaseLayout */ ({ // Main-scheduler Layout (base) /** * @inheritdoc */ initialize: function(options) { this.initVars(); options.context.set('skipFetch', true); this._super('initialize', [options]); this.listenTo(this.context, 'calendars:cache:force-refresh', _.bind(this.forceCalendarRefresh, this)); }, /** * Initialize variables */ initVars: function() { /** * Identifier of the type of calendar we are showing in this component */ this.componentStorageKey = 'main-panel'; /** * Scheduler key used to store calendar configurations * Each location where user can modify the calendar on the fly, has it's own key * ie: main / lists / subpanels */ this.keyToStoreCalendarConfigurations = app.Calendar.utils.buildUserKeyForStorage(this.componentStorageKey); }, /** * Force Calendar Refresh * * Setup context parameters like calendars, location... then update context */ forceCalendarRefresh: function() { let schedulerContext = this.getContextSettingsForScheduler(); this.context.set(schedulerContext); }, /** * @override */ _render: function() { let schedulerContext = this.getContextSettingsForScheduler(); this.context.set(schedulerContext); this._super('_render'); }, /** * Get context settings for scheduler component * * @return {Object} */ getContextSettingsForScheduler: function() { let calendarConfigurations = app.Calendar.utils.getConfigurationsByKey(this.keyToStoreCalendarConfigurations); calendarConfigurations.otherCalendars = _.filter(calendarConfigurations.otherCalendars, function filterCalendars(calendar) { return calendar.selected; } ); calendarConfigurations = calendarConfigurations.myCalendars.concat(calendarConfigurations.otherCalendars); const keyToStoreCalendarView = app.Calendar.utils.buildUserKeyForStorage('main'); const defaultView = app.cache.get(keyToStoreCalendarView) || 'expandedMonth'; return { module: this.module, calendars: calendarConfigurations, defaultView: defaultView, visibleRelationsInContextList: [], location: 'main', keyToStoreCalendarConfigurations: this.keyToStoreCalendarConfigurations, customKendoOptions: { listExportButtons: true } }; }, /** * @override */ _dispose: function() { this.context.off('calendars:cache:force-refresh'); this._super('_dispose'); } }) }, "sidebar-nav-flyout-module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.CalendarSidebarNavFlyoutModuleMenuLayout * @alias SUGAR.App.view.layouts.BaseCalendarSidebarNavFlyoutModuleMenuLayout * @extends View.Layout */ ({ // Sidebar-nav-flyout-module-menu Layout (base) extendsFrom: 'SidebarNavFlyoutModuleMenuLayout', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.listenTo(this.layout, 'popover:opened', this._populateCalendarModules); }, /** * Adds the calendar modules to the menu components. * * @private */ _getMenuComponents: function() { let menuComponents = this._super('_getMenuComponents'); menuComponents.splice(1,0 ,{ view: { type: 'sidebar-nav-flyout-actions', name: 'calendar-modules', actions: [] } }); return menuComponents; }, /** * Calls the the Calendar Modules api to get the list of calendar modules. * * @private */ _populateCalendarModules: function() { app.api.call('read', app.api.buildURL('Calendar/modules'), {}, { success: _.bind(this._populateCalendarModulesSucceess, this) }); }, /** * Populates the calendar modules componenet and renders the actions. * @param data * @private */ _populateCalendarModulesSucceess: function(data) { let actions = []; let calendarModules = this.getComponent('calendar-modules'); _.each(data.modules, function(moduleInfo, module) { let createLabel = ''; if (module === 'KBContents') { createLabel = app.lang.getModString('LNK_NEW_KBCONTENT_TEMPLATE', module); } else { let createLabelKey = 'LNK_NEW_' + moduleInfo.objName.toUpperCase(); createLabel = app.lang.get(createLabelKey, module); if (createLabel === createLabelKey) { createLabelKey = 'LNK_NEW_RECORD'; createLabel = app.lang.getModString(createLabelKey, module); } } actions.push({ acl_action: 'create', acl_module: module, icon: app.metadata.getModule(module).icon || '', label: createLabel, route: `#${module}/create` }); }, this); calendarModules.updateActions(actions); } }) }, "add-calendar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Calendar.AddCalendarLayout * @alias SUGAR.App.view.layouts.BaseAddCalendarLayout * @extends View.Layouts.Base.BaseLayout */ ({ // Add-calendar Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.collection.sync = this.sync; this.collection.getSyncCallbacks = this.getSyncCallbacks; this.collection.allowed_modules = ['Users', 'Teams']; this.context.on('calendar:add:search', this.search, this); }, /** * Collection's sync method * * @param {string} method * @param {Data.Bean} model * @param {Object} options */ sync: function(method, model, options) { let callbacks; let url; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list)) { options.module_list = ['all']; } else { options.module_list = _.intersection(this.allowed_modules, options.module_list); } app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; options = app.data.parseOptionsForSync(method, model, options); callbacks = this.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); url = app.api.buildURL('Calendar', 'usersAndTeams', null, options.params); app.api.call('read', url, null, callbacks); }, /** * @override * Customizations needed just for teams_offset flag we send from server */ getSyncCallbacks: function(method, model, options) { return { success: app.data.getSyncSuccessCallback(method, model, options), error: app.data.getSyncErrorCallback(method, model, options), complete: app.data.getSyncCompleteCallback(method, model, options), abort: app.data.getSyncAbortCallback(method, model, options) }; }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {string} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.collection.fetch({ query: term, module_list: modules, offset: 0, update: false }); }, /** * @override */ _dispose: function() { this.context.off('calendar:add:search'); this._super('_dispose'); } }) } }} , "datas": { "base": { "collection": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /* * @class Data.Base.CalendarBeanCollection * @extends Data.BeanCollection */ ({ // Collection Data (base) /** * Returns all calendars formatted for events retrival * * @return {Array} */ compile: function() { const calendarConfigurations = _.map(this.models, function(calendarConfiguration) { return calendarConfiguration.compile(); }); return calendarConfigurations; }, }) }, "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.CalendarModel * @alias SUGAR.App.model.datas.BaseCalendarModel * @extends Data.Bean */ ({ // Model Data (base) initialize: function(options) { app.Bean.prototype.initialize.call(this, options); if (this.isNew()) { this.listenTo(this, 'change:calendar_module', _.debounce(_.bind(this.prefillDefaultFields, this), 0)); } else { this.once('sync', function() { this.listenTo(this, 'change:calendar_module', _.debounce(_.bind(this.prefillDefaultFields, this), 0)); }, this); } this.addValidationTask('dates_exists', _.bind(this._validateDateFields, this)); this.addValidationTask('fields_required', _.bind(this.validateRequiredFields, this)); this.denyFields = ['date_entered', 'date_modified']; }, /** * Returns data of a calendar configuration * * @return {Object} */ compile: function() { return { id: this.get('id') || this.get('calendarId'), calendarId: this.get('calendarId'), userId: this.get('userId'), teamId: this.get('teamId'), }; }, /** * Prefill default fields * * @param {Object} model * @param {string} module * @param {Object} options */ prefillDefaultFields: function(model, module, options) { if (options.revert) { return; } const calendarModule = this.get('calendar_module'); const moduleMeta = app.metadata.getModule(calendarModule); if (moduleMeta) { const fieldsMetadata = moduleMeta.fields; const fieldsToCalculate = { 'subject': { fieldTypes: ['varchar', 'name', 'fullname'], keys: ['subject', 'name'] }, 'event_start': { fieldTypes: ['datetime', 'datetimecombo'], keys: ['startdate', 'datestart', 'start', 'dateentered'] }, 'event_end': { fieldTypes: ['datetime', 'datetimecombo'], keys: ['enddate', 'dateend', 'end', 'dateentered'] }, }; _.each(fieldsToCalculate, function(fieldToCalculate, fieldName) { let bestScore = 0; let bestScoreField = ''; _.each(fieldToCalculate.keys, function(keyToSearch) { _.each(fieldsMetadata, function(fieldDef, fieldDefName) { if (this.denyFields.indexOf(fieldDef.name) == -1 && fieldToCalculate.fieldTypes.indexOf(fieldDef.type) >= 0) { const fieldScore = this.calculateMatchScore(keyToSearch, fieldDef); if (fieldScore > bestScore) { bestScore = fieldScore; bestScoreField = fieldDefName; } } }, this); }, this); if (bestScore > 0) { this.set(fieldName, bestScoreField); } else { this.unset(fieldName); } }, this); const defaultColor = this.getDefaultBackgroundColor(this.get('calendar_module')); let defaultTemplate = ''; if (_.isArray(fieldsMetadata.name.db_concat_fields)) { _.each(fieldsMetadata.name.db_concat_fields, function(field) { defaultTemplate += ' {::' + field + '::}'; }); } else { defaultTemplate += '{::name::}'; } defaultTemplate += ' {::description::}'; let defaultParams = { duration_minutes: '', duration_hours: '', duration_days: '', color: defaultColor, dblclick_event: 'detail:self:id', allow_create: true, allow_update: true, allow_delete: true, event_tooltip_template: defaultTemplate, day_event_template: defaultTemplate, week_event_template: defaultTemplate, month_event_template: defaultTemplate, agenda_event_template: defaultTemplate, timeline_event_template: defaultTemplate, schedulermonth_event_template: defaultTemplate, ical_event_template: defaultTemplate, }; //Calls and Meetings needs durations instead of event_end if (calendarModule == 'Calls' || calendarModule == 'Meetings') { defaultParams = _.extend(defaultParams, { event_end: '', duration_minutes: 'duration_minutes', duration_hours: 'duration_hours', }); } this.set(defaultParams); } }, /** * Validate date fields * * Validate there is start date and an end or some duration * * @param {Object} fields The list of field names and their definitions. * @param {Object} errors The list of field names and their errors. * @param {Function} callback Async.js waterfall callback. * @private */ _validateDateFields: function(fields, errors, callback) { const eventStartGiven = !_.isEmpty(this.get('event_start')); const eventEndGiven = !_.isEmpty(this.get('event_end')); const durationMinutesGiven = !_.isEmpty(this.get('duration_minutes')); const durationHoursGiven = !_.isEmpty(this.get('duration_hours')); const durationDaysGiven = !_.isEmpty(this.get('duration_days')); if (!eventStartGiven) { errors.event_start = app.lang.get('LBL_EVENT_START_ERROR', 'Calendar'); } if (!eventEndGiven && !durationMinutesGiven && !durationHoursGiven && !durationDaysGiven) { errors.event_end = app.lang.get('LBL_EVENT_END_ERROR', 'Calendar'); } callback(null, fields, errors); }, /** * It will validate required fields. * * @param {Array} fields The list of fields to be validated. * @param {Object} errors A list of error messages. * @param {Function} callback Callback to be called at the end of the validation. */ validateRequiredFields: function(fields, errors, callback) { if (!app.acl.hasAccess('view', this.get('calendar_module'))) { this.set({ calendar_module: '', event_start: '', event_end: '', duration_minutes: '', duration_hours: '', duration_days: '' }); } _.each(fields, function(field) { if (_.has(field, 'required') && field.required) { var key = field.name; if (!this.get(key)) { errors[key] = errors[key] || {}; errors[key].required = true; } } }, this); callback(null, fields, errors); }, /** * Calculates a value representing how close a field is to a given concept (start date / end date...) * * @param {string} text Concept key to search for * @param {Object} fieldDef A field definition * @return {number} */ calculateMatchScore: function(text, fieldDef) { if (typeof text != 'string' || typeof fieldDef != 'object') { return 0; } const fieldModule = this.get('calendar_module'); let fieldName = fieldDef.name; if (fieldName.substr(fieldName.length - 2) === '_c') { fieldName = fieldName.substr(0, fieldName.length - 2); } fieldName = fieldName.replace(/_/g, ''); let fieldLabel = fieldDef.vname || ''; if (!_.isEmpty(fieldLabel)) { fieldLabel = app.lang.get(fieldLabel, fieldModule); fieldLabel = fieldLabel.toLowerCase().replace(/\s/g, ''); } let score = 0; if (fieldName.indexOf(text) >= 0) { score += 1; //prioritize 'startdate' above 'bigstartfieldname' for given 'startdate' if (fieldName.length > text.length) { score -= fieldName.length / text.length; } else if (fieldName.length < text.length) { score -= text.length / fieldName.length; } } if (fieldLabel.indexOf(text) >= 0) { score += 1; //prioritize 'startdate' above 'bigstartfieldname' for given 'startdate' if (fieldName.length > text.length) { score -= fieldLabel.length / text.length; } else if (fieldName.length < text.length) { score -= text.length / fieldLabel.length; } } return score; }, /** * Get default color of a module * * @param {string} module * @return {string} Hex format */ getDefaultBackgroundColor: function(module) { const defaultStyleSheet = _.find(document.styleSheets, function(styleSheet) { return styleSheet.href.indexOf('cache/themes/clients/base/default') > 0; }); const labelRule = _.find(defaultStyleSheet.rules, function(rule) { return rule.selectorText == '.label-' + module; }); if (typeof labelRule == 'object') { let rgbColor = labelRule.style['background-color']; rgbColor = rgbColor.match(/\d+/g); rgbColor = _.map(rgbColor, function(color) { return parseInt(color); }); return this.rgbToHex(rgbColor); } return ''; }, /** * Convert rgb color to hex * * @param {int} comp * @return {string} */ componentToHex: function(comp) { let hex = comp.toString(16); return hex.length == 1 ? '0' + hex : hex; }, /** * Return hex from rgb * * @param {Array} rgb * @return {string} */ rgbToHex: function(rgb) { return '#' + this.componentToHex(rgb[0]) + this.componentToHex(rgb[1]) + this.componentToHex(rgb[2]); }, }) } }} }, "Leads":{"fieldTemplates": { "base": { "badge": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Leads.BadgeField * @alias SUGAR.App.view.fields.BaseLeadsBadgeField * @extends View.Fields.Base.BadgeField */ ({ // Badge FieldTemplate (base) /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, events: { 'click [data-action=convert]': 'convertLead' }, /** * @inheritdoc * * The badge is always a readonly field. */ initialize: function(options) { options.def.readonly = true; app.view.Field.prototype.initialize.call(this, options); }, /** * @inheritdoc */ isHidden: function() { return false; }, /** * Kick off convert lead process. */ convertLead: function() { var model = app.data.createBean(this.model.module); model.set(app.utils.deepCopy(this.model.attributes)); app.drawer.open({ layout : 'convert', context: { forceNew: true, skipFetch: true, module: this.model.module, leadsModel: model } }); } }) }, "convertbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Leads.ConvertbuttonField * @alias SUGAR.App.view.fields.BaseLeadsConvertbuttonField * @extends View.Fields.Base.RowactionField */ ({ // Convertbutton FieldTemplate (base) extendsFrom: 'RowactionField', initialize: function (options) { this._super("initialize", [options]); this.type = 'rowaction'; // Fix for when the convert button is in the dashablerecord view if (this.view.layout && this.view.layout.type === 'dashlet-grid-wrapper') { this.model = this.view.layout.getComponent("dashablerecord").model; } }, _render: function () { var convertMeta = app.metadata.getLayout('Leads', 'convert-main'); var missingRequiredAccess = _.some(convertMeta.modules, function (moduleMeta) { return (moduleMeta.required === true && !app.acl.hasAccess('create', moduleMeta.module)); }, this); if (this.model.get('converted') || missingRequiredAccess) { this.hide(); } else { this._super("_render"); } }, /** * Event to trigger the convert lead process for the lead */ rowActionSelect: function() { let model = app.data.createBean(this.model.module); model.set(app.utils.deepCopy(this.model.attributes)); let isOnDashlet = this.view.name === 'dashlet-toolbar'; app.drawer.open({ layout : "convert", context: { forceNew: true, skipFetch: true, module: 'Leads', leadsModel: model, doRedirect: !isOnDashlet } }, success => { if (success && isOnDashlet) { let dashlet = this.view.layout.getComponent('dashablerecord'); dashlet._updateAllowedButtons(); } }); }, bindDataChange: function () { if (this.model) { this.model.on("change", this.render, this); } }, /** * @inheritdoc */ isAllowedDropdownButton: function() { // Filter logic for when its on a dashlet if (this.view.name === 'dashlet-toolbar') { if (this.module === 'Leads') { var model = this.context.parent.get('model'); return model && !model.get('converted') && !_.isUndefined(model.get('converted')); } return false; } return true; } }) }, "status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Leads.StatusField * @alias SUGAR.App.view.fields.BaseLeadsStatusField * @extends View.Fields.Base.EnumField */ ({ // Status FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'enum'; }, /** * @inheritdoc * * Filter out the Converted option if the Lead is not already converted. */ _filterOptions: function(options) { var status = this.model.get('status'); var filteredOptions = this._super('_filterOptions', [options]); return (!_.isUndefined(status) && status !== 'Converted') ? _.omit(filteredOptions, 'Converted') : filteredOptions; } }) } }} , "views": { "base": { "convert-results": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-results View (base) extendsFrom: 'ConvertResultsView', /** * Build a collection of associated models and re-render the view */ populateResults: function() { var model; //only show related records if lead is converted if (!this.model.get('converted')) { return; } this.associatedModels.reset(); model = this.buildAssociatedModel('Contacts', 'contact_id', 'contact_name'); if (model) { this.associatedModels.push(model); } model = this.buildAssociatedModel('Accounts', 'account_id', 'account_name'); if (model) { this.associatedModels.push(model); } model = this.buildAssociatedModel('Opportunities', 'opportunity_id', 'converted_opp_name'); if (model) { this.associatedModels.push(model); } app.view.View.prototype.render.call(this); }, /** * Build an associated model based on given id & name fields on the Lead record * * @param {String} moduleName * @param {String} idField * @param {String} nameField * @return {*} model or false if id field is not set on the lead */ buildAssociatedModel: function(moduleName, idField, nameField) { var moduleSingular = app.lang.getAppListStrings('moduleListSingular'), model; if (_.isEmpty(this.model.get(idField))) { return false; } model = app.data.createBean(moduleName, { id: this.model.get(idField), name: this.model.get(nameField), row_title: moduleSingular[moduleName], _module: moduleName, target_module: moduleName }); model.module = moduleName; return model; } }) }, "pipeline-recordlist-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Leads.PipelineRecordlistContentView * @alias App.view.views.BaseLeadsPipelineRecordlistContentView * @extends View.Views.Base.PipelineRecordlistContentView */ ({ // Pipeline-recordlist-content View (base) extendsFrom: 'PipelineRecordlistContentView', /** * Overrides the base function to account for the lead conversion functionality * @override */ saveModel: function(model, pipelineData) { if (this.headerField === 'status') { // If the lead has already been converted, don't allow the user to change // its status. If the lead status is being changed to from non-converted // to converted, open the lead conversion layout in a drawer instead of // the normal change saving process if (model.get('converted')) { this._postChange(model, true, pipelineData); var moduleName = app.lang.getModuleName(this.module, {plural: false}); app.alert.show('error_converted', { level: 'error', messages: app.lang.get('LBL_PIPELINE_ERR_CONVERTED', this.module, {moduleSingular: moduleName}) }); return; } else if (_.isObject(pipelineData.newCollection) && pipelineData.newCollection.headerKey === 'Converted') { app.drawer.open({ layout: 'convert', context: { forceNew: true, skipFetch: true, module: 'Leads', leadsModel: model } }, _.bind(function(success) { this._callWithTileModel(model, '_postChange', [!success, pipelineData]); }, this)); return; } } this._super('saveModel', [model, pipelineData]); } }) }, "convert-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-headerpane View (base) extendsFrom: 'HeaderpaneView', events: { 'click [name=save_button]:not(".disabled")': 'initiateSave', 'click [name=cancel_button]': 'initiateCancel' }, /** * @inheritdoc */ initialize: function(options) { this._super("initialize", [options]); this.context.on('lead:convert-save:toggle', this.toggleSaveButton, this); }, /** * @override * * Grabs the lead's name and format the title such as `Convert: <name>`. */ _formatTitle: function(title) { var leadsModel = this.context.get('leadsModel'), name = !_.isUndefined(leadsModel.get('name')) ? leadsModel.get('name') : leadsModel.get('first_name') + ' ' + leadsModel.get('last_name'); return app.lang.get(title, this.module) + ': ' + name; }, /** * When finish button is clicked, send this event down to the convert layout to wrap up */ initiateSave: function() { this.context.trigger('lead:convert:save'); }, /** * When cancel clicked, hide the drawer */ initiateCancel : function() { app.drawer.close(); }, /** * Enable/disable the Save button * * @param enable true to enable, false to disable */ toggleSaveButton: function(enable) { this.$('[name=save_button]').toggleClass('disabled', !enable); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this._super('initialize', [options]); this.listenTo(this.context, 'button:custom_sf_iframebutton:click', this.viewInMarket); }, /** * Remove id, status and converted fields * (including associations created during conversion) when duplicating a Lead * @param prefill */ setupDuplicateFields: function(prefill){ // Clear sugar predict fields const predictFields = [ 'ai_icp_fit_score_classification', 'ai_icp_fit_score_classification_c', 'ai_conv_score_classification', 'ai_conv_score_classification_c' ]; predictFields.forEach(fieldName => prefill.unset(fieldName)); var duplicateBlackList = ['id', 'status', 'converted', 'account_id', 'opportunity_id', 'contact_id']; _.each(duplicateBlackList, function(field){ if(field && prefill.has(field)){ //set blacklist field to the default value if exists if (!_.isUndefined(prefill.fields[field]) && !_.isUndefined(prefill.fields[field].default)) { prefill.set(field, prefill.fields[field].default); } else { prefill.unset(field); } } }); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Leads.CreateView * @alias SUGAR.App.view.views.LeadsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', getCustomSaveOptions: function(){ var options = {}; if(this.context.get('prospect_id')) { options.params = {}; // Needed for populating the relationship options.params.relate_to = 'Prospects'; options.params.relate_id = this.context.get('prospect_id'); this.context.unset('prospect_id'); } return options; } }) }, "convert-options": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-options View (base) /** * @inheritdoc * * Prevent render if transfer activities action is not move. */ _render: function() { var transferActivitiesAction = app.metadata.getConfig().leadConvActivityOpt; if (transferActivitiesAction === 'move') { this.model.setDefault('transfer_activities', true); this._super('_render'); } } }) }, "convert-panel-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-panel-header View (base) events: { 'click .toggle-link': 'handleToggleClick' }, /** * @inheritdoc */ initialize: function(options) { options.meta.buttons = this.getButtons(options); app.view.View.prototype.initialize.call(this, options); this.layout.on('toggle:change', this.handleToggleChange, this); this.layout.on('lead:convert-dupecheck:pending', this.setDupeCheckPending, this); this.layout.on('lead:convert-dupecheck:complete', this.setDupeCheckResults, this); this.layout.on('lead:convert-panel:complete', this.handlePanelComplete, this); this.layout.on('lead:convert-panel:reset', this.handlePanelReset, this); this.layout.on('lead:convert:duplicate-selection:change', this.setAssociateButtonState, this); this.context.on('lead:convert:' + this.meta.module + ':shown', this.handlePanelShown, this); this.context.on('lead:convert:' + this.meta.module + ':hidden', this.handlePanelHidden, this); this.initializeSubTemplates(); }, /** * Return the metadata for the Associate/Reset buttons to be added to the * convert panel header * * @param {Object} options * @return {Array} */ getButtons: function(options) { return [ { name: 'associate_button', type: 'button', label: this.getLabel( 'LBL_CONVERT_CREATE_MODULE', {'moduleName': options.meta.moduleSingular} ), css_class: 'btn-primary disabled' }, { name: 'reset_button', type: 'button', label: 'LBL_CONVERT_RESET_PANEL', css_class: 'btn-invisible btn-link' } ]; }, /** * Initialize the Reset button to be hidden on render * @inheritdoc */ _render: function() { app.view.View.prototype._render.call(this); this.getField('reset_button').hide(); }, /** * Compile data from the convert panel layout with some of the metadata to * be used when rendering sub-templates * * @return {Object} */ getCurrentState: function() { var currentState = _.extend({}, this.layout.currentState, { create: (this.layout.currentToggle === this.layout.TOGGLE_CREATE_LAYOUT), labelModule: this.module, moduleInfo: {'moduleName': this.meta.moduleSingular}, required: this.meta.required }); if (_.isNumber(currentState.dupeCount)) { currentState.duplicateCheckResult = {'duplicateCount': currentState.dupeCount}; } return currentState; }, /** * Pull in the sub-templates to be used to render & re-render pieces of the convert header * Pieces of the convert header change based on various states the panel is in */ initializeSubTemplates: function() { this.tpls = {}; this.initial = {}; this.tpls.title = app.template.getView(this.name + '.title', this.module); this.initial.title = this.tpls.title(this.getCurrentState()); this.tpls.dupecheckPending = app.template.getView(this.name + '.dupecheck-pending', this.module); this.tpls.dupecheckResults = app.template.getView(this.name + '.dupecheck-results', this.module); }, /** * Toggle the subviews based on which link was clicked * * @param {Event} event The click event on the toggle link */ handleToggleClick: function(event) { var nextToggle = this.$(event.target).data('next-toggle'); this.layout.trigger('toggle:showcomponent', nextToggle); event.preventDefault(); event.stopPropagation(); }, /** * When switching between sub-views, change the appropriate header components: * - Title changes to reflect New vs. Select (showing New ModuleName or just ModuleName) * - Dupe check results are shown/hidden based on whether dupe view is shown * - Change the toggle link to allow the user to toggle back to the other one * - Enable Associate button when on create view - Enable/Disable button based * on whether dupe selected on dupe view * * @param {string} toggle Which view is now being displayed */ handleToggleChange: function(toggle) { this.renderTitle(); this.toggleDupeCheckResults(toggle === this.layout.TOGGLE_DUPECHECK); this.setSubViewToggle(toggle); this.setAssociateButtonState(); }, /** * When opening a panel, change the appropriate header components: * - Activate the header * - Display the subview toggle link * - Enable Associate button when on create view - Enable/Disable button * based on whether dupe selected on dupe view * - Mark active indicator pointing up */ handlePanelShown: function() { this.$('.accordion-heading').addClass('active'); this.toggleSubViewToggle(true); this.setAssociateButtonState(); this.toggleActiveIndicator(true); }, /** * When hiding a panel, change the appropriate header components: * - Deactivate the header * - Hide the subview toggle link * - Disable the Associate button * - Mark active indicator pointing down */ handlePanelHidden: function() { this.$('.accordion-heading').removeClass('active'); this.toggleSubViewToggle(false); this.setAssociateButtonState(false); this.toggleActiveIndicator(false); }, /** * When a panel has been marked complete, change the appropriate header components: * - Mark the step circle as check box * - Title changes to show the record associated * - Hide duplicate check results * - Hide the subview toggle link * - Switch to Reset button */ handlePanelComplete: function() { this.setStepCircle(true); this.renderTitle(); this.toggleDupeCheckResults(false); this.toggleSubViewToggle(false); this.toggleButtons(true); }, /** * When a panel has been reset, change the appropriate header components: * - Mark the step circle back to step number * - Title changes back to incomplete (showing New ModuleName or just ModuleName) * - Show duplicate check count (if any found) * - Switch to back to Associate button * - Enable Associate button when on create view - Enable/Disable button * based on whether dupe selected on dupe view */ handlePanelReset: function() { this.setStepCircle(false); this.renderTitle(); this.toggleDupeCheckResults(true); this.toggleButtons(false); this.setAssociateButtonState(); }, /** * Switch between check mark and step number * * @param {boolean} complete Whether to mark panel completed */ setStepCircle: function(complete) { var $stepCircle = this.$('.step-circle'); if (complete) { $stepCircle.addClass('complete'); } else { $stepCircle.removeClass('complete'); } }, /** * Render the title based on current state Create vs DupeCheck and * Complete vs. Incomplete */ renderTitle: function() { this.$('.title').html(this.tpls.title(this.getCurrentState())); }, /** * Put up "Searching for duplicates" message */ setDupeCheckPending: function() { this.renderDupeCheckResults('pending'); }, /** * Display duplicate results (if any found) or hide subview links if none found * * @param {number} duplicateCount Number of duplicates found */ setDupeCheckResults: function(duplicateCount) { if (duplicateCount > 0) { this.renderDupeCheckResults('results'); } else { this.renderDupeCheckResults('clear'); } this.setSubViewToggleLabels(duplicateCount); }, /** * Render either dupe check results or pending (or empty if no dupes found) * * @param {string} type Which message to show - `results` or `pending` */ renderDupeCheckResults: function(type) { var results = ''; if (type === 'results') { results = this.tpls.dupecheckResults(this.getCurrentState()); } else if (type === 'pending') { results = this.tpls.dupecheckPending(this.getCurrentState()); } this.$('.dupecheck-results').text(results); }, /** * Show/hide dupe check results * If duplicate already selected, results will not be shown * * @param {boolean} show Whether to show the duplicate check results */ toggleDupeCheckResults: function(show) { // if we are trying to show this, but we already have a dupeSelected, change the show to false if (show && this.layout.currentState.dupeSelected) { show = false; } this.$('.dupecheck-results').toggle(show); }, /** * Show/hide the subview toggle links altogether * If panel is complete, the subview toggle will not be shown * * @param {boolean} show Whether to show the subview toggle */ toggleSubViewToggle: function(show) { if (this.layout.currentState.complete) { show = false; } this.$('.subview-toggle').toggleClass('hide', !show); }, /** * Show/hide appropriate toggle link for the subview being displayed * * @param {string} nextToggle Css class labeling the next toggle */ setSubViewToggle: function(nextToggle) { _.each(['dupecheck', 'create-layout'], function(currentToggle) { this.toggleSubViewLink(currentToggle, (nextToggle === currentToggle)); }, this); }, /** * Show/hide a single subview toggle link * * @param {string} currentToggle Css class labeling the current toggle * @param {boolean} show Whether to show the toggle link */ toggleSubViewLink: function(currentToggle, show) { this.$('.subview-toggle .' + currentToggle).toggle(show); }, /** * Switch subview toggle labels based on whether duplicates were found or not * * @param {number} duplicateCount */ setSubViewToggleLabels: function(duplicateCount) { if (duplicateCount > 0) { this.setSubViewToggleLabel('dupecheck', 'LBL_CONVERT_IGNORE_DUPLICATES'); this.setSubViewToggleLabel('create-layout', 'LBL_CONVERT_BACK_TO_DUPLICATES'); } else { this.setSubViewToggleLabel('dupecheck', 'LBL_CONVERT_SWITCH_TO_CREATE'); this.setSubViewToggleLabel('create-layout', 'LBL_CONVERT_SWITCH_TO_SEARCH'); } }, /** * Set label for given subview toggle * * @param {string} currentToggle Css class labeling the current toggle * @param {string} label Label to replace the toggle text with */ setSubViewToggleLabel: function(currentToggle, label) { this.$('.subview-toggle .' + currentToggle).text(this.getLabel(label)); }, /** * Toggle between Associate and Reset buttons * * @param {boolean} complete */ toggleButtons: function(complete) { var associateButton = 'associate_button', resetButton = 'reset_button'; if (complete) { this.getField(associateButton).hide(); this.getField(resetButton).show(); } else { this.getField(associateButton).show(); this.getField(resetButton).hide(); } }, /** * Activate/Deactivate the Associate button based on which subview is active * and whether the panel itself is active (keep disabled when panel not active) * * @param {boolean} [activate] */ setAssociateButtonState: function(activate) { let shouldActivate = _.isUndefined(activate) ? null : false; const $associateButton = this.$('[name="associate_button"]'); const panelActive = this.$('.accordion-heading').hasClass('active'); //use current state to determine activate if not explicit in call if (shouldActivate === null) { if (this.layout.currentToggle === this.layout.TOGGLE_CREATE_LAYOUT) { shouldActivate = true; } else { shouldActivate = this.layout.currentState.dupeSelected; } } this.setAssociateButtonLabel(this.layout.currentToggle === this.layout.TOGGLE_CREATE_LAYOUT); //only activate if current panel is active if (shouldActivate && panelActive) { $associateButton.removeClass('disabled'); } else { $associateButton.addClass('disabled'); } }, /** * Set the label for the Associate Button * * @param {boolean} isCreate */ setAssociateButtonLabel: function(isCreate) { var label = 'LBL_CONVERT_SELECT_MODULE'; if (isCreate) { label = 'LBL_CONVERT_CREATE_MODULE'; } this.$('[name="associate_button"]').html(this.getLabel(label, {'moduleName': this.meta.moduleSingular})); }, /** * Toggle the active indicator up/down * * @param {boolean} active */ toggleActiveIndicator: function(active) { var $activeIndicator = this.$('.active-indicator i'); $activeIndicator.toggleClass('sicon-chevron-down', active); $activeIndicator.toggleClass('sicon-chevron-right', !active); }, /** * Get translated strings from the Leads module language file * * @param {string} key The app/mod string * @param {Object} [context] Any placeholder data to populate in the string * @return {string} The translated string */ getLabel: function(key, context) { context = context || {}; return app.lang.get(key, 'Leads', context); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Leads.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseLeadsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "convert-panel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-panel Layout (base) extendsFrom: 'ToggleLayout', TOGGLE_DUPECHECK: 'dupecheck', TOGGLE_CREATE: 'create', TOGGLE_CREATE_LAYOUT: 'create-layout', availableToggles: { 'dupecheck': {}, 'create': {}, 'create-layout': {} }, //selectors accordionHeading: '.accordion-heading', accordionBody: '.accordion-body', //turned on, but could be turned into a setting later autoCompleteEnabled: true, /** * @inheritdoc */ initialize: function(options) { var convertPanelEvents; this.meta = options.meta; this._setModuleSpecificValues(); convertPanelEvents = {}; convertPanelEvents['click .accordion-heading.enabled'] = 'togglePanel'; convertPanelEvents['click [name="associate_button"]'] = 'handleAssociateClick'; convertPanelEvents['click [name="reset_button"]'] = 'handleResetClick'; this.events = _.extend({}, this.events, convertPanelEvents); this.plugins = _.union(this.plugins || [], [ 'FindDuplicates' ]); this.currentState = { complete: false, dupeSelected: false }; this.toggledOffDupes = false; this._super('initialize', [options]); this._initSubpanelsData(); this.addSubComponents(); this.context.on('lead:convert:populate', this.handlePopulateRecords, this); this.context.on('lead:convert:' + this.meta.module + ':enable', this.handleEnablePanel, this); this.context.on('lead:convert:' + this.meta.moduleNumber + ':open', this.handleOpenRequest, this); this.context.on('lead:convert:exit', this.turnOffUnsavedChanges, this); this.context.on('lead:convert:' + this.meta.module + ':shown', this.handleShowComponent, this); //if this panel is dependent on others - listen for changes and react accordingly this.addDependencyListeners(); //open the first module upon the first autocomplete check completion if (this.meta.moduleNumber === 1) { this.once('lead:autocomplete-check:complete', this.handleOpenRequest, this); } }, /** * Initializes the metadata that drives which create subpanels are included * on this convert panel * * @private */ _initSubpanelsData: function() { this.subpanelsMeta = this.meta.subpanels || []; // Check for Opps+RLI subpanel settings and add the RLI create subpanel // to the subpanels meta if needed if (this.meta.module === 'Opportunities' && this.meta.enableRlis) { this.subpanelsMeta.push({ layout: 'subpanel-create', label: 'LBL_RLI_SUBPANEL_TITLE', override_subpanel_list_view: 'subpanel-for-opportunities-create', context: { link: 'revenuelineitems' }, settings: { allowEmpty: !this.meta.requireRlis, copyData: this.meta.copyDataToRlis } }); } }, /** * Retrieve module specific values (like modular singular name and whether * dupe check is enabled at a module level). * @private */ _setModuleSpecificValues: function() { var module = this.meta.module; this.meta.modulePlural = app.lang.getAppListStrings('moduleList')[module] || module; this.meta.moduleSingular = app.lang.getAppListStrings('moduleListSingular')[module] || this.meta.modulePlural; //enable or disable duplicate check var moduleMetadata = app.metadata.getModule(module); this.meta.enableDuplicateCheck = (moduleMetadata && moduleMetadata.dupCheckEnabled) || this.meta.enableDuplicateCheck || false; this.meta.duplicateCheckOnStart = this.meta.enableDuplicateCheck && this.meta.duplicateCheckOnStart; }, /** * Used by toggle layout to determine where to place sub-components. * * @param {Object} component * @return {jQuery} */ getContainer: function(component) { if (component.name === 'convert-panel-header') { return this.$('[data-container="header"]'); } else { return this.$('[data-container="inner"]'); } }, /** * Add all sub-components of the panel. */ addSubComponents: function() { this.addHeaderComponent(); this.addDupeCheckComponent(); this.addRecordCreateComponent(); }, /** * Add the panel header view. */ addHeaderComponent: function() { var header = app.view.createView({ context: this.context, type: 'convert-panel-header', layout: this, meta: this.meta }); this.addComponent(header); }, /** * Add the duplicate check layout along with events to listen for changes to * the duplicate view. */ addDupeCheckComponent: function() { var leadsModel = this.context.get('leadsModel'), context = this.context.getChildContext({ 'module': this.meta.module, 'forceNew': true, 'skipFetch': true, 'dupelisttype': 'dupecheck-list-select', 'collection': this.createDuplicateCollection(leadsModel, this.meta.module), 'layoutName': 'records', 'dataView': 'selection-list' }); context.prepare(); this.duplicateView = app.view.createLayout({ context: context, type: this.TOGGLE_DUPECHECK, layout: this, module: context.get('module') }); this.duplicateView.context.on('change:selection_model', this.handleDupeSelectedChange, this); this.duplicateView.collection.on('reset', this.dupeCheckComplete, this); this.addComponent(this.duplicateView); }, /** * Add the create toggle layout, including the create view and any * applicable create subpanels */ addRecordCreateComponent: function() { // Create the context for all the record create components let context = this._buildRecordCreateContext(); // Create a single layout that will contain all the create subcomponents. // This is done so that both the record create view and create subpanels // will be wrapped into one component that can be toggled this.toggleLayout = this._buildRecordCreateToggleLayout(context); // Add the record create view component to the toggle layout this.createView = this._buildRecordCreateView(context); this.toggleLayout.addComponent(this.createView); // Add the create subpanels component to the toggle layout if needed if (!_.isEmpty(this.subpanelsMeta)) { this.createSubpanelsLayout = this._buildRecordCreateSubpanelsLayout(context); this.toggleLayout.addComponent(this.createSubpanelsLayout); } // Finally, add the toggle-able layout to this toggle layout this.addComponent(this.toggleLayout); }, /** * Builds the context needed for the create components * * @return {Context} the record create context * @private */ _buildRecordCreateContext: function() { let context = this.context.getChildContext({ module: this.meta.module, forceNew: true, create: true }); context.prepare(); return context; }, /** * Builds the layout that will be used as the toggle-able layout * containing all the components related to creating a record * * @param {Context} context the context to use for the toggle-able layout * @return {Layout} the toggle-able layout * @private */ _buildRecordCreateToggleLayout: function(context) { return app.view.createLayout({ context: context, type: 'base', name: this.TOGGLE_CREATE_LAYOUT, module: context.get('module'), layout: this }); }, /** * Builds the create view that will be used to create the panel's record * * @param {Context} context the context to use for the create view * @return {View} the create view * @private */ _buildRecordCreateView: function(context) { let createView = app.view.createView({ context: context, type: this.TOGGLE_CREATE, module: context.get('module'), layout: this.toggleLayout }); createView.meta = this.removeFieldsFromMeta(createView.meta, this.meta); createView.enableHeaderButtons = false; return createView; }, /** * Builds the subpanels layout that will be used to create records related * to the panel's record * * @param {Context} context the context to use for the subpanels layout * @return {Layout} the subpanels layout * @private */ _buildRecordCreateSubpanelsLayout: function(context) { let createSubpanelsLayout = app.view.createLayout({ context: context, type: 'subpanels-create', layout: this.toggleLayout }); createSubpanelsLayout.initComponents(this.subpanelsMeta); this._initSubpanelListeners(createSubpanelsLayout); return createSubpanelsLayout; }, /** * Initializes listeners for any create subpanels as needed * * @private */ _initSubpanelListeners: function(subpanelsLayout) { // Add listeners to the subpanels as needed _.each(subpanelsLayout.context.children, function(childContext) { if (childContext.get('isCreateSubpanel')) { // Product Catalog listeners for Revenue Line Items subpanels if (childContext.get('module') === 'RevenueLineItems') { let convertComponent = this.closestComponent('convert'); // When we open the Product Catalog/Quick Picks previews, // show the "Add" button only if this panel is currently // enabled and open convertComponent.before('productcatalog:preview:add:disable', function() { return !(this.isPanelEnabled() && this.isPanelOpen()); }, this); // When we click a Product on the Product Catalog/Quick Picks // dashlets, add it to this subpanel only if this panel is // currently enabled and opened convertComponent.before('productCatalogDashlet:add:allow', function() { return this.isPanelEnabled() && this.isPanelOpen(); }, this); } // If this subpanel is set to copy data, add listeners to copy // Lead data to new rows let subpanelSettings = childContext.get('settings'); if (subpanelSettings && subpanelSettings.copyData) { // If the subpanel was initialized with any models, make // sure the Lead data is copied once the Lead is fetched let leadsModel = this.context.get('leadsModel'); leadsModel.once('sync', function() { _.each(childContext.get('collection').models, function(subpanelModel) { this.populateSubpanelRecord(subpanelModel); }, this); }, this); // Whenever subsequent subpanel models are created, copy // data from the Lead as needed this.listenTo(childContext, 'subpanel-list-create:row:added', this.populateSubpanelRecord); } } }, this); }, /** * Copies attributes from the Lead model to the newly created subpanel model * @param {Bean} subpanelModel the newly created subpanel model */ populateSubpanelRecord(subpanelModel) { // Get the Lead model we are copying from let leadsModel = this.context.get('leadsModel'); // Get the list of fields that can be safely copied let copyableAttrs = this._getCopyableAttrs(leadsModel, subpanelModel); // Copy the fields from the Lead to the subpanel model let attrs = {}; _.each(copyableAttrs, function(value, key) { if (leadsModel.has(key) && subpanelModel.get(key) !== value) { subpanelModel.setDefault(key, value); attrs[key] = value; } }, this); subpanelModel.set(attrs); }, /** * Filters and returns the attributes of the fromModel that are valied to be copied to the toModel * @param {Bean} fromModel the source model to copy attributes from * @param {Bean} toModel the destination model to copy attributes to * @return {Object} the subset of the source model's attributes that can be copied to the destination model * @private */ _getCopyableAttrs(fromModel, toModel) { let fromModule = fromModel.module || fromModel.get('_module'); let toModule = toModel.module || toModel.get('_module'); let fromFieldMeta = app.metadata.getModule(fromModule, 'fields'); let toFieldMeta = app.metadata.getModule(toModule, 'fields'); return _.pick(fromModel.attributes, function(value, field) { return app.acl.hasAccessToModel('edit', toModel, field) && fromFieldMeta[field] && toFieldMeta[field] && fromFieldMeta[field].type === toFieldMeta[field].type && (_.isUndefined(fromFieldMeta[field].duplicate_on_record_copy) || fromFieldMeta[field].duplicate_on_record_copy !== 'no') && this.shouldSourceValueBeCopied(value); }, this); }, /** * Sets the listeners for changes to the dependent modules. */ addDependencyListeners: function() { _.each(this.meta.dependentModules, function(details, module) { this.context.on('lead:convert:' + module + ':complete', this.updateFromDependentModuleChanges, this); this.context.on('lead:convert:' + module + ':reset', this.resetFromDependentModuleChanges, this); }, this); }, /** * When duplicate results are received (or dupe check did not need to be * run) toggle to the appropriate view. * * If duplicates were found for a required module, auto select the first * duplicate. */ dupeCheckComplete: function() { if (this.disposed) { return; } this.currentState.dupeCount = this.duplicateView.collection.length; this.runAutoCompleteCheck(); if (this.currentState.dupeCount !== 0) { this.showComponent(this.TOGGLE_DUPECHECK); if (this.meta.required) { this.selectFirstDuplicate(); } } else if (!this.toggledOffDupes) { this.showComponent(this.TOGGLE_CREATE_LAYOUT); } this.toggledOffDupes = true; //flag so we only toggle once this.trigger('lead:convert-dupecheck:complete', this.currentState.dupeCount); }, /** * Check to see if the panel should be automatically marked as complete * * Required panels are marked complete when there are no duplicates and * the create form passes validation. */ runAutoCompleteCheck: function() { //Bail out if we've already completed the check if (this.autoCompleteCheckComplete) { return; } if (this.autoCompleteEnabled && this.meta.required && this.currentState.dupeCount === 0) { this.createView.once('render', this.runAutoCompleteValidation, this); } else { this.markAutoCompleteCheckComplete(); } }, /** * Run validation, mark panel complete if valid without any alerts */ runAutoCompleteValidation: function() { var view = this.createView, model = view.model; model.isValidAsync(view.getFields(view.module), _.bind(function(isValid) { if (isValid) { this.markPanelComplete(model); } this.markAutoCompleteCheckComplete(); }, this)); }, /** * Set autocomplete check complete flag and trigger event */ markAutoCompleteCheckComplete: function() { this.autoCompleteCheckComplete = true; this.trigger('lead:autocomplete-check:complete'); }, /** * Select the first item in the duplicate check list. */ selectFirstDuplicate: function() { var list = this.duplicateView.getComponent('dupecheck-list-select'); if (list) { list.once('render', function() { var radio = this.$('input[type=radio]:first'); if (radio) { radio.prop('checked', true); radio.click(); } }, this); } }, /** * Removes fields from the meta and replaces with empty html container * based on the modules config option - hiddenFields. * * Example: Account name drop-down should not be available on contact * and opportunity module. * * @param {Object} meta The original metadata * @param {Object} moduleMeta Metadata defining fields to hide * @return {Object} The metadata after hidden fields removed */ removeFieldsFromMeta: function(meta, moduleMeta) { if (moduleMeta.hiddenFields) { _.each(meta.panels, function(panel) { _.each(panel.fields, function(field, index, list) { if (_.isString(field)) { field = {name: field}; } if (moduleMeta.hiddenFields[field.name]) { field.readonly = true; field.required = false; list[index] = field; } }); }, this); } return meta; }, /** * Toggle the accordion body for this panel. */ togglePanel: function() { this.$(this.accordionBody).collapse('toggle'); }, /** * When one panel is completed it notifies the next panel to open * This function handles that request and will... * - wait for auto complete check to finish before doing anything * - pass along request to the next if already complete or not enabled * - open the panel otherwise */ handleOpenRequest: function() { if (this.autoCompleteCheckComplete !== true) { this.once('lead:autocomplete-check:complete', this.handleOpenRequest, this); } else { if (this.currentState.complete || !this.isPanelEnabled()) { this.requestNextPanelOpen(); } else { this.openPanel(); } } }, /** * Check if the the current panel is enabled. * * @return {boolean} */ isPanelEnabled: function() { return this.$(this.accordionHeading).hasClass('enabled'); }, /** * Check if the current panel is open. * * @return {boolean} */ isPanelOpen: function() { return this.$(this.accordionBody).hasClass('show'); }, /** * Open the body of the panel if enabled (and not already open). */ openPanel: function() { if (this.isPanelEnabled()) { if (this.isPanelOpen()) { this.context.trigger('lead:convert:' + this.meta.module + ':shown'); } else { this.$(this.accordionBody).collapse('show'); } } }, /** * When showing create view, render the view, trigger duplication * of fields with special handling (like image fields). * * @inheritdoc */ showComponent: function(name) { this._super('showComponent', [name]); if (this.currentToggle === this.TOGGLE_CREATE_LAYOUT) { this.createViewRendered = true; } this.handleShowComponent(); }, /** * Render the create view. */ handleShowComponent: function() { if (this.currentToggle === this.TOGGLE_CREATE_LAYOUT && this.createView.meta.useTabsAndPanels && !this.createViewRendered) { this.createView.render(); this.createViewRendered = true; } }, /** * Close the body of the panel (if not already closed) */ closePanel: function() { this.$(this.accordionBody).collapse('hide'); }, /** * Handle click of Associate button - running validation if on create view * or marking complete if on dupe view. * * @param {Event} event */ handleAssociateClick: function(event) { //ignore clicks if button is disabled if (!$(event.currentTarget).hasClass('disabled')) { if (this.currentToggle === this.TOGGLE_CREATE_LAYOUT) { this.runCreateValidation({ valid: _.bind(this.markPanelComplete, this), invalid: _.bind(this.resetPanel, this) }); } else { this.markPanelComplete(this.duplicateView.context.get('selection_model')); } } event.stopPropagation(); }, /** * Run validation on the create model and perform specified callbacks based * on the validity of the model. * * @param {Object} callbacks Callbacks to be run after validation is performed. * @param {Function} callbacks.valid Run if model is valid. * @param {Function} callbacks.invalid Run if model is invalid. */ runCreateValidation: function(callbacks) { var view = this.createView, model = view.model; // Validate both the model and any related models in create subpanels, if they exist async.parallel([ _.bind(view.validateModelWaterfall, view), _.bind(view.validateSubpanelModelsWaterfall, view) ], _.bind(function(hasError) { if (hasError) { model.trigger('error:validation'); callbacks.invalid(model); } else { callbacks.valid(model); } }, this)); }, /** * Mark the panel as complete, close the panel body, and tell the next panel * to open. * * @param {Data.Bean} model */ markPanelComplete: function(model) { this.currentState.associatedName = app.utils.getRecordName(model); this.currentState.complete = true; this.context.trigger('lead:convert-panel:complete', this.meta.module, model); this.trigger('lead:convert-panel:complete', this.currentState.associatedName); app.alert.dismissAll('error'); //re-run validation if create model changes after completion if (!model.id) { model.on('change', this.runPostCompletionValidation, this); } //if this panel was open, close & tell the next panel to open if (this.isPanelOpen()) { this.closePanel(); this.requestNextPanelOpen(); } }, /** * Re-run create model validation after a panel has been marked completed */ runPostCompletionValidation: function() { this.runCreateValidation({ valid: $.noop, invalid: _.bind(this.resetPanel, this) }); }, /** * Trigger event to open the next panel in the list */ requestNextPanelOpen: function() { this.context.trigger('lead:convert:' + (this.meta.moduleNumber + 1) + ':open'); }, /** * When reset button is clicked - reset this panel and open it * @param {Event} event */ handleResetClick: function(event) { this.resetPanel(); this.openPanel(); event.stopPropagation(); }, /** * Reset the panel back to a state the user can modify associated values */ resetPanel: function() { this.createView.model.off('change', this.runPostCompletionValidation, this); this.currentState.complete = false; this.context.trigger('lead:convert-panel:reset', this.meta.module); this.trigger('lead:convert-panel:reset'); }, /** * Track when a duplicate has been selected and notify the panel so it can * enable the Associate button */ handleDupeSelectedChange: function() { this.currentState.dupeSelected = this.duplicateView.context.has('selection_model'); this.trigger('lead:convert:duplicate-selection:change'); }, /** * Wrapper to check whether to fire the duplicate check event */ triggerDuplicateCheck: function() { if (this.shouldDupeCheckBePerformed(this.createView.model)) { this.trigger('lead:convert-dupecheck:pending'); this.duplicateView.context.trigger('dupecheck:fetch:fire', this.createView.model, { //Show alerts for this request showAlerts: true }); } else { this.dupeCheckComplete(); } }, /** * Check if duplicate check should be performed * dependent on enableDuplicateCheck setting and required dupe check fields * @param {Object} model */ shouldDupeCheckBePerformed: function(model) { var performDuplicateCheck = this.meta.enableDuplicateCheck; if (this.meta.duplicateCheckRequiredFields) { _.each(this.meta.duplicateCheckRequiredFields, function(field) { if (_.isEmpty(model.get(field))) { performDuplicateCheck = false; } }); } return performDuplicateCheck; }, /** * Populates the record view from the passed in model and then kick off the * dupe check * * @param {Object} model */ handlePopulateRecords: function(model) { var fieldMapping = {}; // if copyData is not set or false, no need to run duplicate check, bail out if (!this.meta.copyData) { this.dupeCheckComplete(); return; } if (!_.isEmpty(this.meta.fieldMapping)) { fieldMapping = app.utils.deepCopy(this.meta.fieldMapping); } var sourceFields = app.metadata.getModule(model.attributes._module, 'fields'); var targetFields = app.metadata.getModule(this.meta.module, 'fields'); _.each(model.attributes, function(fieldValue, fieldName) { if (app.acl.hasAccessToModel('edit', this.createView.model, fieldName) && !_.isUndefined(sourceFields[fieldName]) && !_.isUndefined(targetFields[fieldName]) && sourceFields[fieldName].type === targetFields[fieldName].type && (_.isUndefined(sourceFields[fieldName]['duplicate_on_record_copy']) || sourceFields[fieldName]['duplicate_on_record_copy'] !== 'no') && model.has(fieldName) && model.get(fieldName) !== this.createView.model.get(fieldName) && _.isUndefined(fieldMapping[fieldName])) { fieldMapping[fieldName] = fieldName; } }, this); this.populateRecords(model, fieldMapping); if (this.meta.duplicateCheckOnStart) { this.triggerDuplicateCheck(); } else if (!this.meta.dependentModules || this.meta.dependentModules.length == 0) { //not waiting on other modules before running dupe check, so mark as complete this.dupeCheckComplete(); } }, /** * Use the convert metadata to determine how to map the lead fields to * module fields * * @param {Object} model * @param {Object} fieldMapping * @return {boolean} whether the create view model has changed */ populateRecords: function(model, fieldMapping) { var hasChanged = false; _.each(fieldMapping, function(sourceField, targetField) { if (model.has(sourceField) && this.shouldSourceValueBeCopied(model.get(sourceField)) && model.get(sourceField) !== this.createView.model.get(targetField)) { this.createView.model.setDefault(targetField, model.get(sourceField)); this.createView.model.set(targetField, model.get(sourceField)); hasChanged = true; } }, this); //mark the model as copied so that the currency field doesn't set currency_id to user's default value if (hasChanged) { this.createView.once('render', function() { this.createView.model.trigger('duplicate:field', model); }, this); if (model.has('currency_id')) { this.createView.model.isCopied = true; } } return hasChanged; }, /** * Enable the panel * * @param {boolean} isEnabled add/remove the enabled flag on the header */ handleEnablePanel: function(isEnabled) { var $header = this.$(this.accordionHeading); if (isEnabled) { if (!this.currentState.complete) { this.triggerDuplicateCheck(); } $header.addClass('enabled'); } else { $header.removeClass('enabled'); } }, /** * Updates the attributes on the model based on the changes from dependent * modules duplicate view. * Uses dependentModules property - fieldMappings * * @param {string} moduleName * @param {Object} model */ updateFromDependentModuleChanges: function(moduleName, model) { var dependencies = this.meta.dependentModules, modelChanged = false; if (dependencies && dependencies[moduleName] && dependencies[moduleName].fieldMapping) { modelChanged = this.populateRecords(model, dependencies[moduleName].fieldMapping); if (modelChanged) { this.triggerDuplicateCheck(); } } }, /** * Resets the state of the panel based on a dependent module being reset */ resetFromDependentModuleChanges: function(moduleName) { var dependencies = this.meta.dependentModules; if (dependencies && dependencies[moduleName]) { //if dupe check has already been run, reset but don't run again yet - just update status if (this.currentState.dupeCount && this.currentState.dupeCount > 0) { this.duplicateView.collection.reset(); this.currentState.dupeCount = 0; } //undo any dependency field mapping that was done previously if (dependencies && dependencies[moduleName] && dependencies[moduleName].fieldMapping) { _.each(dependencies[moduleName].fieldMapping, function(sourceField, targetField) { this.createView.model.unset(targetField); }, this); } //make sure if we re-trigger dupe check again we handle as if it never happened before this.toggledOffDupes = false; this.resetPanel(); } }, /** * Resets the model to the default values so that unsaved warning prompt * will not be displayed. */ turnOffUnsavedChanges: function() { var defaults = _.extend({}, this.createView.model._defaults, this.createView.model.getDefault()); this.createView.model.attributes = defaults; }, /** * Determine whether to copy the the supplied value when it appears in the Source module during conversion */ shouldSourceValueBeCopied: function(val) { return _.isNumber(val) || _.isBoolean(val) || !_.isEmpty(val); }, /** * Stop listening to events on duplicate view collection * @inheritdoc */ _dispose: function() { this.createView.model.off('change', this.runPostCompletionValidation, this); this.createView.off(null, null, this); this.duplicateView.off(null, null, this); this.duplicateView.context.off(null, null, this); this.duplicateView.collection.off(null, null, this); this._super('_dispose'); } }) }, "convert-main": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-main Layout (base) /** * @inheritdoc */ initialize: function(options) { this.convertPanels = {}; this.associatedModels = {}; this.dependentModules = {}; this.noAccessRequiredModules = []; app.view.Layout.prototype.initialize.call(this, options); this.meta.modules = this.filterModulesByACL(this.meta.modules); this.initializeOptions(this.meta.modules); //create and place all the accordion panels this.initializePanels(this.meta.modules); //listen for panel status updates this.context.on('lead:convert-panel:complete', this.handlePanelComplete, this); this.context.on('lead:convert-panel:reset', this.handlePanelReset, this); //listen for Save button click in headerpane this.context.on('lead:convert:save', this.handleSave, this); this.before('render', this.checkRequiredAccess); // Indicates that this view is being opened from Tile View this.fromPipeline = this.context.parent && this.context.parent.get('layout') === 'pipeline-records'; }, /** * Create a new object with only modules the user has create access to and * build list of required modules the user does not have create access to. * * @param {Object} modulesMetadata * @return {Object} */ filterModulesByACL: function(modulesMetadata) { var filteredModulesMetadata = {}; _.each(modulesMetadata, function(moduleMeta, key) { //strip out modules that user does not have create access to if (app.acl.hasAccess('create', moduleMeta.module)) { filteredModulesMetadata[key] = moduleMeta; } else if (moduleMeta.required === true) { this.noAccessRequiredModules.push(moduleMeta.module); } }, this); return filteredModulesMetadata; }, /** * Create an options section on top of convert panels that presents options * when converting a lead (specifically, which modules to copy/move * activities to). * * @param {Object} modulesMetadata */ initializeOptions: function(modulesMetadata) { var view, convertModuleList = []; _.each(modulesMetadata, function(moduleMeta) { let moduleSingular = this.getModuleSingular(moduleMeta.module); let moduleDetails = { id: moduleMeta.module, text: moduleSingular, required: moduleMeta.required }; moduleDetails = Object.assign(moduleDetails, { enableRlis: moduleMeta.enableRlis, requireRlis: moduleMeta.requireRlis, }); convertModuleList.push(moduleDetails); }, this); this.context.set('convertModuleList', convertModuleList); view = app.view.createView({ context: this.context, layout: this, type: 'convert-options', platform: this.options.platform }); this.addComponent(view); }, /** * Iterate over the modules defined in convert-main.php * Create a convert panel for each module defined there * * @param {Object} modulesMetadata */ initializePanels: function(modulesMetadata) { var moduleNumber = 1; _.each(modulesMetadata, function(moduleMeta) { moduleMeta.moduleNumber = moduleNumber++; var view = app.view.createLayout({ context: this.context, type: 'convert-panel', layout: this, meta: moduleMeta, platform: this.options.platform }); view.initComponents(); //This is because backbone injects a wrapper element. view.$el.addClass('accordion-group'); view.$el.data('module', moduleMeta.module); this.addComponent(view); this.convertPanels[moduleMeta.module] = view; if (moduleMeta.dependentModules) { this.dependentModules[moduleMeta.module] = moduleMeta.dependentModules; } }, this); }, /** * Check if user is missing access to any required modules * @return {boolean} */ checkRequiredAccess: function() { //user is missing access to required modules - kick them out if (this.noAccessRequiredModules.length > 0) { this.denyUserAccess(this.noAccessRequiredModules); return false; } return true; }, /** * Close lead convert and notify the user that they are missing required access * @param {Array} noAccessRequiredModules */ denyUserAccess: function(noAccessRequiredModules) { var translatedModuleNames = []; _.each(noAccessRequiredModules, function(module) { translatedModuleNames.push(this.getModuleSingular(module)); }, this); app.alert.show('convert_access_denied', { level: 'error', messages: app.lang.get( 'LBL_CONVERT_ACCESS_DENIED', this.module, {requiredModulesMissing: translatedModuleNames.join(', ')} ) }); app.drawer.close(); }, /** * Retrieve the translated module name * @param {string} module * @return {string} */ getModuleSingular: function(module) { var modulePlural = app.lang.getAppListStrings('moduleList')[module] || module; return (app.lang.getAppListStrings('moduleListSingular')[module] || modulePlural); }, _render: function() { app.view.Layout.prototype._render.call(this); //This is because backbone injects a wrapper element. this.$el.addClass('accordion'); this.$el.attr('id', 'convert-accordion'); //apply the accordion to this layout this.$('.collapse').collapse({toggle: false, parent: '#convert-accordion'}); //copy lead data down to each module when we get the lead data this.context.get('leadsModel').fetch({ success: _.bind(function(model) { if (this.context) { this.context.trigger('lead:convert:populate', model); } this._applyAccorditionEvents(); }, this) }); }, /** * Apply accordition events */ _applyAccorditionEvents: function() { this.$('.collapse').on('shown.bs.collapse hidden.bs.collapse', _.bind(this.handlePanelCollapseEvent, this)); }, /** * Catch collapse shown/hidden events and notify the panels via the context * @param {Event} event */ handlePanelCollapseEvent: function(event) { //only respond to the events directly on the collapse (was getting events from tooltip propagated up if (event.target !== event.currentTarget) { return; } var module = $(event.currentTarget).data('module'); this.context.trigger('lead:convert:' + module + ':' + event.type); }, /** * When a panel is complete, add the model to the associatedModels array and notify any dependent modules * @param {string} module that was completed * @param {Data.Bean} model */ handlePanelComplete: function(module, model) { this.associatedModels[module] = model; this.handlePanelUpdate(); this.context.trigger('lead:convert:' + module + ':complete', module, model); }, /** * When a panel is reset, remove the model from the associatedModels array and notify any dependent modules * @param {string} module */ handlePanelReset: function(module) { delete this.associatedModels[module]; this.handlePanelUpdate(); this.context.trigger('lead:convert:' + module + ':reset', module); }, /** * When a panel has been updated, check if any module's dependencies are met * and/or if all required modules have been completed */ handlePanelUpdate: function() { this.checkDependentModules(); this.checkRequired(); }, /** * Check if each module's dependencies are met and enable the panel if they are. * Dependencies are defined in the convert-main.php */ checkDependentModules: function() { _.each(this.dependentModules, function(dependencies, dependentModuleName) { var isEnabled = _.all(dependencies, function(module, moduleName) { return (this.associatedModels[moduleName]); }, this); this.context.trigger('lead:convert:' + dependentModuleName + ':enable', isEnabled); }, this); }, /** * Checks if all required modules have been completed * Enables the Save button if all are complete */ checkRequired: function() { var showSave = _.all(this.meta.modules, function(module) { if (module.required) { if (!this.associatedModels[module.module]) { return false; } } return true; }, this); this.context.trigger('lead:convert-save:toggle', showSave); }, /** * When save button is clicked, call the Lead Convert API */ handleSave: function() { // Disable the save button to prevent double click this.context.trigger('lead:convert-save:toggle', false); // Before building the convert model, make sure that if any of the module panels have create subpanels, // their subpanel models are added to the save correctly _.each(this.convertPanels, function(panel, module) { if (panel.createView) { panel.createView.addSubpanelCreateModels(); } }, this); // Build the convert model that will be sent into the convert API let convertModel = new Backbone.Model(_.extend( { 'modules': this.parseEditableFields(this.associatedModels), 'allowBatching': true }, this.getTransferActivitiesAttributes() )); // Set field_duplicateBeanId for fields implementing FieldDuplicate _.each(this.convertPanels, function(view, module) { if (view && view.createView && convertModel.get('modules')[module]) { view.createView.model.trigger('duplicate:field:prepare:save', convertModel.get('modules')[module]); } }, this); const leadsModel = this.context.get('leadsModel'); const convertLeadsCallback = (view) => { this.cjFormBatch = view; // if form batching has to be performed then don't show saving alert if (_.isUndefined(this.cjFormBatch)) { app.alert.show('processing_convert', {level: 'process', title: app.lang.get('LBL_SAVING')}); } // Call the convert API with the convert model let myURL = app.api.buildURL('Leads', 'convert', {id: leadsModel.id}); app.api.call('create', myURL, convertModel, { success: _.bind(this.convertSuccess, this), error: _.bind(this.convertError, this) }); }; app.CJBaseHelper.fetchActiveSmartGuideCount(this.context, this, this.module, leadsModel.id, convertLeadsCallback ); }, /** * Retrieve the attributes to be added to the convert model to support the * transfer activities functionality. * * @return {Object} */ getTransferActivitiesAttributes: function() { var action = app.metadata.getConfig().leadConvActivityOpt, optedInToTransfer = this.model.get('transfer_activities'); return { transfer_activities_action: (action === 'move' && optedInToTransfer) ? 'move' : 'donothing' }; }, /** * Returns only the fields for the models that the user is allowed to edit. * This method is run in the sync method of data-manager for creating records. * * @param {Object} models to get fields from. * @return {Object} Hash of models with editable fields. */ parseEditableFields: function(models) { var filteredModels = {}; _.each(models, function(associatedModel, associatedModule) { filteredModels[associatedModule] = app.data.getEditableFields(associatedModel); }, this); return filteredModels; }, /** * Lead was successfully converted */ convertSuccess: function() { if (!_.isUndefined(this.cjFormBatch)) { const params = { record: this.context.get('leadsModel').id, module: this.module, }; this.cjFormBatch.startBatchingProcess(params); this.cjFormBatch = undefined; } else { this.convertComplete('success', 'LBL_CONVERTLEAD_SUCCESS', true); } }, /** * There was a problem converting the lead */ convertError: function() { this.convertComplete('error', 'LBL_CONVERTLEAD_ERROR', false); if (!_.isUndefined(this.cjFormBatch)) { this.cjFormBatch.endBatchingProcess(false, false); this.cjFormBatch = undefined; } if (!this.disposed) { this.context.trigger('lead:convert-save:toggle', true); } }, /** * Based on success of lead conversion, display the appropriate messages and optionally close the drawer * @param {string} level * @param {string} message * @param {boolean} doClose */ convertComplete: function(level, message, doClose) { var leadsModel = this.context.get('leadsModel'); app.alert.dismiss('processing_convert'); app.alert.show('convert_complete', { level: level, messages: app.lang.get(message, this.module, {leadName: app.utils.getRecordName(leadsModel)}), autoClose: (level === 'success') }); if (!this.disposed && doClose) { this.context.trigger('lead:convert:exit'); if (this.fromPipeline) { app.drawer.close(level === 'success'); } else { app.drawer.close(level === 'success'); if (this.context.get('doRedirect') !== false) { app.router.record('Leads', leadsModel.id); } } } }, /** * Clean up the jquery events that were added * @private */ _dispose: function() { this.$('.collapse').off(); app.view.Layout.prototype._dispose.call(this); } }) } }} , "datas": {} }, "Currencies":{"fieldTemplates": { "base": { "actionmenu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Actionmenu FieldTemplate (base) extendsFrom: 'ActionmenuField', /** * Binds mass collection events to a record row checkbox. * * @private */ _bindModelChangeEvents: function() { this._super('_bindModelChangeEvents'); this.massCollection.on('reset', function() { // force any disabled field to be unchecked var field = this.$(this.fieldTag); if (field.prop('disabled')) { field.attr('checked', false); } }, this); }, /** * @inheritdoc **/ _onMassCollectionRemoveResetAll: function() { // if default currency exists in collection, remove it _.each(this.massCollection.models, function(model, index) { if (model.id === '-99') { this.massCollection.remove(this.massCollection.models[index], {silent: true}); } }, this); // force entire property to allow the selected row count alert to display if (this.massCollection.length > 0) { this.massCollection.entire = true; } else { this.massCollection.entire = false; } this._super('_onMassCollectionRemoveResetAll'); }, }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * Overriding because Currencies cannot be unlinked nor deleted * * @inheritdoc * @override */ getCustomSaveOptions: function(options) { options.complete = function() {}; return options; } }) }, "text": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.CurrenciesTextField * @alias App.view.fields.BaseCurrenciesTextField * @extends View.Fields.Base.TextField */ ({ // Text FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // If the ISO 4217 field is changed to a valid code, automatically fill the symbol if (this.name === 'symbol') { this.model.on('change:iso4217', (model, iso4217) => { if (this.action === 'edit' && iso4217 !== '' && model.get('id') !== '-99' && app.lang.getAppListKeys('iso_currency_symbol').includes(iso4217) ) { this.model.set(this.name, app.lang.getAppListStrings('iso_currency_symbol')[iso4217]); } }); } } }) }, "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.CurrenciesNameField * @alias App.view.fields.BaseCurrenciesNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // If the ISO 4217 field is changed to a valid code, automatically fill the currency name this.model.on('change:iso4217', (model, iso4217) => { if (this.action === 'edit' && iso4217 !== '' && model.get('id') !== '-99' && app.lang.getAppListKeys('iso_currency_name').includes(iso4217) ) { this.model.set(this.name, app.lang.getAppListStrings('iso_currency_name')[iso4217]); } }); // Set the system currency name this.listenTo(this.model, 'change:name', (model, name) => { if (name !== '' && model.get('id') === '-99') { model.set(this.name, app.lang.get('LBL_CURRENCY_DEFAULT', 'Currencies')); } }); }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); this._super('_dispose'); } }) } }} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Currencies Record List. * * @class View.Views.Base.Currencies.RecordlistView * @alias SUGAR.App.view.views.BaseCurrenciesRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc **/ bindDataChange: function() { this.collection.on('data:sync:complete', function() { this.collection.each(function(model) { if (model.get('id') == app.currency.getBaseCurrencyId()) { model.isDefault = true; let defaultCurrencyName = app.lang.get('LBL_CURRENCY_DEFAULT', 'Currencies'); if (defaultCurrencyName) { model.set('name', defaultCurrencyName); } } }, this); this.render(); }, this); // call the parent this._super('bindDataChange'); }, /** * Disable double click to edit for the base currency * @inheritdoc */ doubleClickEdit: function(event) { let row = this.$(event.target).parents('tr'); if (row.attr('name') !== 'Currencies_-99') { this._super('doubleClickEdit', [event]); } }, /** * @inheritdoc **/ _render: function() { this._super('_render'); let $tableRow = this.$('tr[name="Currencies_-99"]'); let $rowCheckBox = $tableRow.find('input[name="check"]'); let $rowActionDropdown = $tableRow.find('[data-bs-toggle="dropdown"'); let $defaultCurrencyLabel = $tableRow.find('[data-type="name"] div.ellipsis_inline'); // Add the default currency class to the default currency row if ($defaultCurrencyLabel.length) { $defaultCurrencyLabel.addClass('defaultCurrencyLabel'); } // disable the checkbox if ($rowCheckBox.length) { $rowCheckBox.prop('disabled', true); } // remove actions if ($rowActionDropdown.length) { $rowActionDropdown.closest('span.overflow-visible').css('justify-content', 'left').css('left', '0'); $rowActionDropdown.remove(); } } }) }, "dashablerecord": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Currencies.DashablerecordView * @alias SUGAR.App.view.views.BaseCurrenciesDashablerecordView * @extends View.Views.Base.DashablerecordView */ ({ // Dashablerecord View (base) extendsFrom: 'DashablerecordView', isBase: false, /** * @inheritdoc */ _setReadonlyFields: function() { this.isBaseCurrency(this.model); this._super('_setReadonlyFields'); }, /** * Checks to see if the model is the base currency * @param model */ isBaseCurrency: function(model) { if (model && model.get('id') === app.currency.getBaseCurrencyId()) { this.isBase = true; } else { this.isBase = false; } } }) }, "preview-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Currencies.PreviewHeaderView * @alias SUGAR.App.view.views.BaseCurrenciesPreviewHeaderView * @extends View.Views.Base.PreviewHeaderView */ ({ // Preview-header View (base) extends: 'PreviewHeaderView', isBase: false, /** * @inheritdoc * @override */ triggerEdit: function() { //If this isn't the base currency, go ahead and display the edit view if (!this.isBase) { this._super('triggerEdit'); } }, /** * Checks to see if the model is the base currency * @param model */ isBaseCurrency: function(model) { if (model && _.isFunction(model.get) && model.get('id') === app.currency.getBaseCurrencyId()) { this.isBase = true; } else { this.isBase = false; } }, /** * @inheritdoc */ _render: function() { this.isBaseCurrency(this.context.get('model')); this._super('_render'); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this._checkIfBaseCurrency(options); this._super('initialize', [options]); }, /** * Checks to see if the currency is the base currency * @param options * @private */ _checkIfBaseCurrency: function(options) { if (options.context.get('modelId') == app.currency.getBaseCurrencyId()) { var mainDropdownBtn = this._findButton(options.meta.buttons, 'main_dropdown'); //disable edit if (mainDropdownBtn) { // disable the edit button var editBtn = this._findButton(mainDropdownBtn.buttons, 'edit_button'); if (editBtn) { editBtn.css_class = editBtn.css_class || ''; editBtn.css_class += ' disabled'; } } //set fields to read only. _.each(options.meta.panels, function(panel) { _.each(panel.fields, function(field) { field.readonly = true; }, this); }, this); } }, /** * Finds buttons of a given type * * @param buttons * @param name * @return {*} * @private */ _findButton: function(buttons, name) { return _.find(buttons, function(btn) { return btn.name === name; }); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Currencies.PreviewView * @alias SUGAR.App.view.views.BaseCurrenciesPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * @inheritdoc */ hasUnsavedChanges: function() { if (!_.isUndefined(this.model) && this.model.get('id') === app.currency.getBaseCurrencyId()) { return false; } return this._super('hasUnsavedChanges'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "filterpanel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filterpanel Layout (base) extendsFrom: 'FilterpanelLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if (this.context.get('layout') === 'record') { this.before('render', function() { return false; }, this); this.template = app.template.empty; this.$el.html(this.template()); } } }) } }} , "datas": {} }, "Contracts":{"fieldTemplates": {} , "views": { "base": { "filter-filter-dropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-filter-dropdown View (base) extendsFrom: 'FilterFilterDropdownView', /** * @inheritdoc */ getFilterList: function() { var list = this._super('getFilterList').filter(function(obj) { if (obj.id == 'favorites') { return false; } return true; }); return list; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Contracts.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseContractsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Quotes":{"fieldTemplates": { "base": { "badge": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.BadgeField * @alias SUGAR.App.view.fields.BaseBadgeField * @extends View.Fields.Base.BaseField */ ({ // Badge FieldTemplate (base) /** * Hash map of the possible labels for the badge */ badgeLabelMap: undefined, /** * Hash map of the possible CSS Classes for the badge */ cssClassMap: undefined, /** * The current CSS Class to add to the badge */ currentCSSClass: undefined, /** * The current Label to use for the badge */ currentLabel: undefined, /** * The field name to check for the badge */ badgeFieldName: undefined, /** * The current state of the field */ state: undefined, /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, /** * @inheritdoc * * The badge is always a readonly field. */ initialize: function(options) { options.def.readonly = true; this._initOptionMaps(options); this._super('initialize', [options]); this._setState(); }, /** * Sets up any class hashes defined in metadata * * @param {Object} options The field def options from metadata * @private */ _initOptionMaps: function(options) { this.cssClassMap = options.def.css_class_map; this.badgeLabelMap = options.def.badge_label_map; }, /** * Sets the state of the field, field name, label, css classes, etc * * @private */ _setState: function() { this.badgeFieldName = this.def.related_fields && _.first(this.def.related_fields) || this.name; var val = this.model.get(this.badgeFieldName); switch (this.def.badge_compare.comparison) { case 'notEq': this.state = val != this.def.badge_compare.value; break; case 'eq': this.state = val == this.def.badge_compare.value; break; case 'notEmpty': this.state = !_.isUndefined(val) && !_.isEmpty(val.toString()); break; case 'empty': this.state = !_.isUndefined(val) && _.isEmpty(val.toString()); break; } this.currentLabel = app.lang.get(this.badgeLabelMap[this.state], this.module); this.currentCSSClass = this.cssClassMap[this.state]; }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:' + this.badgeFieldName, function() { if (!this.disposed) { this._setState(); this.render(); } }, this); } }) }, "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.CurrencyField * @alias SUGAR.App.view.fields.BaseQuotesCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) extendsFrom: 'CurrencyField', /** * The field's value in Percent */ valuePercent: undefined, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.name === 'deal_tot' && this.view.name === 'quote-data-grand-totals-header') { this.model.on('change:deal_tot_discount_percentage', function() { this._updateDiscountPercent(); }, this); if (this.context.get('create')) { // if this is deal_tot and on the create view, update the discount percent this._updateDiscountPercent(); } } }, /** * Needed to override loadTemplate to check field permissions for Quotes header and footer views * * @inheritdoc */ _loadTemplate: function() { var viewName = this.view.name; if ((viewName === 'quote-data-grand-totals-header' || viewName === 'quote-data-grand-totals-footer') && !this._checkAccessToAction('list')) { // set the action to noaccess so the field template will get the right class this.action = 'noaccess'; // if this is a header or footer currency field and there's no access, show noaccess this.tplName = 'noaccess-' + viewName; this.template = app.template.getField('currency', this.tplName, this.module); } else { this._super('_loadTemplate'); } }, /** * Updates `this.valuePercent` for the deal_tot field in the quote-data-grand-totals-header view. * * @private */ _updateDiscountPercent: function() { var percent = this.model.get('deal_tot_discount_percentage'); if (!_.isUndefined(percent)) { //clean up precision percent = app.utils.formatNumber( percent, false, app.user.getPreference('decimal_precision'), app.user.getPreference('number_grouping_separator'), app.user.getPreference('decimal_separator') ); if (app.lang.direction === 'rtl') { this.valuePercent = '%' + percent; } else { this.valuePercent = percent + '%'; } // re-render after update this.render(); } }, /** * Check is transaction value is equal to value and returns answer. * * @return {boolean} True if the values are equal or false if not * @private */ isTransactionValueEqualToValue: function() { return this.transactionValue === this.value; }, }) }, "quote-data-actiondropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.QuoteDataActiondropdownField * @alias SUGAR.App.view.fields.BaseQuotesQuoteDataActiondropdownField * @extends View.Fields.Base.BaseActiondropdownField */ ({ // Quote-data-actiondropdown FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'BaseActiondropdownField', /** * @inheritdoc */ className: 'quote-data-actiondropdown' }) }, "quote-footer-currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.QuoteFooterCurrency * @alias SUGAR.App.view.fields.BaseQuotesQuoteFooterCurrency * @extends View.Fields.Base.CurrencyField */ ({ // Quote-footer-currency FieldTemplate (base) extendsFrom: 'CurrencyField', /** * @inheritdoc */ initialize: function(options) { var isCreate = options.context.isCreate(); options.viewName = isCreate ? 'edit' : 'detail'; this._super('initialize', [options]); if (!isCreate) { // only add this event on record view this.events = _.extend({ 'click .currency-field': '_toggleFieldToEdit' }, this.events); } this.model.addValidationTask( 'isNumeric_validator_' + this.cid, _.bind(this._doValidateIsNumeric, this) ); this.action = isCreate ? 'edit' : 'detail'; this.context.trigger('quotes:editableFields:add', this); }, /** * Needed to override loadTemplate to check field permissions for Quotes footer views * * @inheritdoc */ _loadTemplate: function() { if (!this._checkAccessToAction('list')) { // set the action to noaccess so the field template will get the right class this.action = 'noaccess'; // if this is a header or footer currency field and there's no access, show noaccess this.tplName = 'noaccess'; this.template = app.template.getField('quote-footer-currency', this.tplName, this.module); } else { this._super('_loadTemplate'); } }, /** * Toggles the field to edit if it not in edit * * @param {jQuery.Event} evt jQuery click event * @private */ _toggleFieldToEdit: function(evt) { var record; if (!this.$el.hasClass('edit')) { this.action = 'edit'; this.tplName = 'detail'; // if this isn't already in edit, toggle to edit record = this.closestComponent('record'); if (record) { record.context.trigger('editable:handleEdit', evt); } } }, /** * Validation function to check to see if a value is numeric. * * @param {Array} fields * @param {Array} errors * @param {Function} callback * @private */ _doValidateIsNumeric: function(fields, errors, callback) { var value = this.model.get(this.name); if (!$.isNumeric(value)) { errors[this.name] = app.lang.get('ERROR_NUMBER'); } callback(null, fields, errors); }, /** * Extending to remove the custom validation task for this field * * @inheritdoc * @private */ _dispose: function() { this.model.removeValidationTask('isNumeric_validator_' + this.cid); this._super('_dispose'); } }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseQuotesEditablelistbuttonField * @extends View.Fields.EditablelistbuttonField */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['QuotesConversionRateLocking']); this._super('initialize', [options]); }, /** * @inheritdoc */ _save: function() { this.checkConversionRateLock(() => { this._super('_save'); }); } }) }, "currency-type-dropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.CurrencyTypeDropdownField * @alias SUGAR.App.view.fields.BaseQuotesCurrencyTypeDropdownField * @extends View.Fields.Base.EnumField */ ({ // Currency-type-dropdown FieldTemplate (base) extendsFrom: 'EnumField', /** * Holds the compiled currencies templates with symbol/iso by currencyID key * @type {Object} */ currenciesTpls: undefined, /** * The currency ID field name to use on the model when changing currency ID * Defaults to 'currency_id' if no currency_field exists in metadata * @type {string} */ currencyIdFieldName: undefined, /** * The base rate field name to use on the model * Defaults to 'base_rate' if no base_rate_field exists in metadata * @type {string} */ baseRateFieldName: undefined, /** * The last known record currency id * @type {string} */ _lastCurrencyId: undefined, /** * @inheritdoc */ initialize: function(options) { // get the currencies and run them through the template this.currenciesTpls = app.currency.getCurrenciesSelector(Handlebars.compile('{{symbol}} ({{iso4217}})')); // Type should be enum to use the enum templates options.def.type = 'enum'; // update options defs the currencies templates options.def.options = options.def.options || this.currenciesTpls; // get the default field names from metadata this.currencyIdFieldName = options.def.currency_field || 'currency_id'; this.baseRateFieldName = options.def.base_rate_field || 'base_rate'; this._super('initialize', [options]); // check to make sure this is a new model or currency_id has not been set, and the model is not a copy // so we don't overwrite the models previously entered values if ((this.model.isNew() && !this.model.isCopy())) { var currencyFieldValue = app.user.getPreference('currency_id'); var baseRateFieldValue = app.metadata.getCurrency(currencyFieldValue).conversion_rate; // set the currency_id to the user's preferred currency this.model.set(this.currencyIdFieldName, currencyFieldValue); // set the base_rate to the preferred currency conversion_rate this.model.set(this.baseRateFieldName, baseRateFieldValue); // if this.name is not the same as the currency ID field, also set this.name on the model if (this.name !== this.currencyIdFieldName) { this.model.set(this.name, currencyFieldValue); } // Modules such as `Forecasts` uses models that aren't `Data.Bean` if (_.isFunction(this.model.setDefault)) { var defaults = {}; defaults[this.currencyIdFieldName] = currencyFieldValue; defaults[this.baseRateFieldName] = baseRateFieldValue; this.model.setDefault(defaults); } } // track the last currency id to convert the value on change this._lastCurrencyId = this.model.get(this.currencyIdFieldName); } }) }, "quote-footer-input": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.QuoteFooterInputField * @alias SUGAR.App.view.fields.BaseQuotesQuoteFooterInputField * @extends View.Fields.Base.Field */ ({ // Quote-footer-input FieldTemplate (base) /** * The value dollar amount */ value_amount: undefined, /** * The value percent amount */ value_percent: undefined, /** * @inheritdoc */ format: function(value) { if (!value) { this.value_amount = app.currency.formatAmountLocale('0'); this.value_percent = '0%'; } } }) }, "datetimecombo": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.DatetimecomboField * @alias SUGAR.App.view.fields.BaseQuotesDatetimecomboField * @extends View.Fields.Base.DatetimecomboField */ ({ // Datetimecombo FieldTemplate (base) extendsFrom: 'DatetimecomboField', /** * @inheritdoc */ _dispose: function() { // FIXME: this is a bad "fix" added -- when SC-2395 gets done to upgrade bootstrap we need to remove this if (this._hasTimePicker) { this.$(this.secondaryFieldTag).timepicker('remove'); } if (this._hasDatePicker && this.$(this.fieldTag).data('datepicker')) { $(window).off('resize', this.$(this.fieldTag).data('datepicker').place); } this._hasTimePicker = false; this._hasDatePicker = false; this._super('_dispose'); } }) }, "copy": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.CopyField * @alias SUGAR.App.view.fields.BaseQuotesCopyField * @extends View.Fields.Base.CopyField */ ({ // Copy FieldTemplate (base) extendsFrom: 'CopyField', /** * If this field is on a view that is converting from a "Ship To" Subpanel */ isConvertingFromShipping: undefined, /** * If this is a Quote Record Copy */ isCopy: undefined, /** * Is this the first time the Copy field has run */ firstRun: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.firstRun = true; this.isCopy = this.context.get('copy') || false; this.isConvertingFromShipping = this.view.isConvertFromShippingOrBilling === 'shipping'; }, /** * Extending to set Shipping Account Name field editable after copy * * @inheritdoc */ sync: function(enable) { var shippingAcctNameField; var isChecked = this._isChecked(); // do not sync field mappings if this is a quote record copy if (this.isCopy) { enable = false; } this._super('sync', [enable]); // do not sync field mappings if this is a quote record copy if (this.firstRun) { this.firstRun = false; } // if this is coming from a Ship To subpanel and the Copy Billing to Shipping box // is not checked then re-enable the Shipping Account Name field so it can be canceled if (!isChecked) { shippingAcctNameField = this.getField('shipping_account_name'); if (shippingAcctNameField) { shippingAcctNameField.setDisabled(false); } } }, /** * @inheritdoc * Overwriting the copy method so that the billing and shipping details are correct when a quote is created from * Accounts Quotes Bill-to or Ship-to subpanel */ copy: function(from, to) { var _link = this.context.get('fromLink'); var _fromModule = this.context.previous('parentModel') ? this.context.previous('parentModel').get('_module') : ''; // came from Accounts Quote Bill-To if (_link === 'quotes' && this.firstRun === true && _fromModule === 'Accounts') { var billingAccounts = this.model.get('billing_accounts'); if (!this.model.has(from)) { return; } if (_.isUndefined(this._initialValues[to])) { this._initialValues[to] = this.model.get(to); } if (to === 'shipping_account_name') { this.model.set(to, billingAccounts.name); } else if (to === 'shipping_account_id') { this.model.set(to, billingAccounts.id); } else if (app.acl.hasAccessToModel('edit', this.model, to)) { this.model.set(to, billingAccounts[to]); } } else if (_link === 'quotes_shipto' && this.firstRun === true && _fromModule === 'Accounts') { // came from // Accounts Quote Ship-To var shippingAccounts = this.model.get('shipping_accounts'); if (_.isUndefined(this._initialValues[to])) { this._initialValues[to] = this.model.get(to); } if (to === 'shipping_account_name') { this.model.set(from, shippingAccounts.name); } else if (to === 'shipping_account_id') { this.model.set(from, shippingAccounts.id); } else if (app.acl.hasAccessToModel('edit', this.model, from)) { this.model.set(from, shippingAccounts[from]); } } else { this._super('copy', [from, to]); } }, /** * Extending to add the model value condition in pre-rendered versions of the field * * @inheritdoc */ toggle: function() { this.sync(this._isChecked()); }, /** * Pulling this out to a function that can be checked from multiple places if the field * is checked or if the field does not exist yet (pre-render) then use the model value * * @return {boolean} True if the field is checked or false if not * @private */ _isChecked: function() { return this.$fieldTag ? this.$fieldTag.is(':checked') : this.model.get(this.name); }, /** * Extending to check if we need to add sync events or not * * @inheritdoc */ syncCopy: function(enable) { if ((!this.isConvertingFromShipping && !_.isUndefined(this._isChecked())) || (this.isConvertingFromShipping && this._isChecked())) { // if this view is not coming from a Ship To convert subpanel, // or if it IS but the user specifically checked the Copy Billing to Shipping checkbox this._super('syncCopy', [enable]); } else { // set _inSync to be false so that sync() will work properly this._inSync = false; if (!enable) { // remove sync events from the model this.model.off(null, this.copyChanged, this); return; } } } }) }, "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.DateField * @alias SUGAR.App.view.fields.BaseQuotesDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * @inheritdoc */ _dispose: function() { // FIXME: this is a bad "fix" added -- when SC-2395 gets done to upgrade bootstrap we need to remove this if (this._hasDatePicker && this.$(this.fieldTag).data('datepicker')) { $(window).off('resize', this.$(this.fieldTag).data('datepicker').place); } this._hasDatePicker = false; this._super('_dispose'); } }) }, "fieldset": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.FieldsetField * @alias SUGAR.App.view.fields.BaseQuotesFieldsetField * @extends View.Fields.Base.FieldsetField */ ({ // Fieldset FieldTemplate (base) extendsFrom: 'FieldsetField', /** * The currency field name to use on the model */ currencyField: 'currency_id', /** * The base rate field name to use on the model */ baseRateField: 'base_rate', /** * The closed statuses of Quotes stage */ closedStatuses: ['Closed Accepted', 'Closed Dead', 'Closed Lost'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.listenTo(this.model, `change:${this.currencyField}`, this.updateBaseRate); }, /** * Updates the base rate field based on the currency_id field */ updateBaseRate: function() { if (this.name === 'conversion_rate_lock' && !_.includes(this.closedStatuses, this.model.get('quote_stage')) && !this.model.get('lock_conversion_rates') ) { let currencyId = this.model.get(this.currencyField); let lockedRates = this.model.get('locked_currency_rates') || {}; let baseRate = lockedRates[currencyId]; let currencyRate = baseRate || app.metadata.getCurrency(currencyId).conversion_rate; this.model.set(this.baseRateField, currencyRate); this._render(); } } }) }, "taxrate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.TaxrateField * @alias SUGAR.App.view.fields.BaseQuotesTaxrateField * @extends View.Fields.Base.EnumField */ ({ // Taxrate FieldTemplate (base) extendsFrom: 'RelateField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // If the field is on the Quotes create view, try to get a default tax rate if (options.view.name === 'create') { this.getDefaultTaxRate(); } }, /** * Get the lowest order active tax rate the user can access */ getDefaultTaxRate: function() { let taxRateCollection = app.data.createBeanCollection('TaxRates'); taxRateCollection.fetch({ fields: ['id', 'name', 'status', 'list_order', 'value'], filter: [ {'status': {'$in': ['Active']}} ], params: { order_by: 'list_order:asc', }, limit: 1, success: data => { if (data.models.length === 0 || !this.model) { return; } let taxRate = data.models[0]; this.setValue({ id: taxRate.get('id'), name: taxRate.get('name'), value: taxRate.get('value') }); this.view.defaultTaxRateValues = { taxrate_id: taxRate.get('id'), taxrate_name: taxRate.get('name'), taxrate_value: taxRate.get('value') }; } }); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:taxrate_value', this._onTaxRateChange, this); }, /** * Sets a new "tax" value when the taxrate changes * * @param {Data.Bean} model The changed model * @param {string} taxrateValue The new taxrate value "8.25", "!0", etc * @private */ _onTaxRateChange: function(model, taxrateValue) { taxrateValue = taxrateValue || '0'; var taxratePercent = app.math.div(taxrateValue, '100'); var newTax = app.math.mul(this.model.get('taxable_subtotal'), taxratePercent); this.model.set('tax', newTax); }, /** * Extending to add taxrate_value to the id/name values * * @inheritdoc */ _onSelect2Change: function(e) { var plugin = $(e.target).data('select2'); var id = e.val; var value; var collection; var attributes = {}; if (_.isUndefined(id)) { return; } value = (id) ? plugin.selection.find('span').text() : $(this).data('rname'); collection = plugin.context; if (collection && !_.isEmpty(id)) { // if we have search results use that to set new values var model = collection.get(id); attributes.id = model.id; attributes.value = model.get('value'); attributes.name = model.get('name'); _.each(model.attributes, function(value, field) { if (app.acl.hasAccessToModel('view', model, field)) { attributes[field] = attributes[field] || model.get(field); } }); } else if (e.currentTarget.value && value) { // if we have previous values keep them attributes.id = value; attributes.name = e.currentTarget.value; attributes.value = value; } else { // default to empty attributes.id = ''; attributes.name = ''; attributes.value = ''; } this.setValue(attributes); }, /** * Extending to add taxrate_value to the id/name values * * @inheritdoc */ setValue: function(models) { if (!models) { return; } var updateRelatedFields = true; var values = { taxrate_id: models.id, taxrate_name: models.name, taxrate_value: models.value }; if (_.isArray(models)) { // Does not make sense to update related fields if we selected // multiple models updateRelatedFields = false; } this.model.set(values); if (updateRelatedFields) { // TODO: move this to SidecarExpressionContext // check if link field is currently populated if (this.model.get(this.fieldDefs.link)) { // unset values of related bean fields in order to make the model load // the values corresponding to the currently selected bean this.model.unset(this.fieldDefs.link); } else { // unsetting what is not set won't trigger "change" event, // we need to trigger it manually in order to notify subscribers // that another related bean has been chosen. // the actual data will then come asynchronously this.model.trigger('change:' + this.fieldDefs.link); } } } }) }, "quote-data-actionmenu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.QuoteDataActionmenuField * @alias SUGAR.App.view.fields.BaseQuotesQuoteDataActionmenuField * @extends View.Fields.Base.BaseActionmenuField */ ({ // Quote-data-actionmenu FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'BaseActionmenuField', /** * Skipping ActionmenuField's override, just returning this.def.buttons * * @inheritdoc */ _getChildFieldsMeta: function() { return app.utils.deepCopy(this.def.buttons); } }) }, "tristate-checkbox": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.TristateCheckboxField * @alias SUGAR.App.view.fields.BaseQuotesTristateCheckboxField * @extends View.Fields.Base.BaseField */ ({ // Tristate-checkbox FieldTemplate (base) /** * @inheritdoc */ events: { 'click .checkbox': 'onCheckboxClicked' }, /** * The list of possible states the field can be in * @type Object */ statesData: undefined, /** * The previous state's state data * @type Object */ previousState: undefined, /** * The name of the previous state * @type string */ previousStateName: undefined, /** * The current state's state data * @type Object */ currentState: undefined, /** * The name of the current state * @type string */ currentStateName: undefined, /** * If the field is required by other fields * @type boolean */ isRequired: undefined, /** * Text for the field's tooltip * @type string */ tooltipText: undefined, /** * List of any dependent fields * @type Object */ dependentFields: undefined, /** * Stored version of the app lang tooltip label */ tooltipLabel: undefined, /** * Service related fields * @type Array */ serviceRelatedFieldArr: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.dependentFields = _.clone(this.def.dependentFields) || {}; this.statesData = this._getStatesData(); this.isRequired = this.def.required || false; // if current state is not defined the get the initial state this.changeState(options.viewDefs && options.viewDefs.currentState ? options.viewDefs.currentState : this._getInitialState()); this.serviceRelatedFieldArr = [ 'service_start_date', 'service_end_date', 'renewable', 'service_duration', 'service', ]; this.tooltipLabel = app.lang.get('LBL_CONFIG_TOOLTIP_FIELD_REQUIRED_BY', this.module); if (this.name === 'service_duration') { //See if the Service Duration column is added to the worksheet columns var hasServiceDurationCol = _.find(this.context.get('worksheet_columns'), function(col) { return col.name === 'service_duration'; }); //If the service duration column exists in worksheet columns //Mark it as checked in the howto panels if (!_.isUndefined(hasServiceDurationCol)) { this.changeState('checked'); } } }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.on( 'config:' + this.def.eventViewName + ':' + this.name + ':related:toggle', this._onToggleRelatedField, this ); this.context.on('config:fields:' + this.def.eventViewName + ':reset', this._onFieldsReset, this); }, /** * @inheritdoc */ bindDomChange: function() { }, /** * Handles changing from the current state to the next state * * @param {string} nextState The next state to transition to */ changeState: function(nextState) { this.previousState = this.currentState; this.previousStateName = this.currentStateName; this.currentStateName = nextState; this.currentState = this.statesData[this.currentStateName]; this.render(); }, /** * @inheritdoc */ render: function() { this._updateTooltipText(); this._super('render'); if (this.currentState.isIndeterminate) { this.$('.checkbox').prop('indeterminate', true); } }, /** * Returns if this is a required field or not * * @return {Object} If required was sent in from the field def or false * @protected */ _getIsRequired: function() { return this.def.required || false; }, /** * Returns the possible states data for the field * * @return {Object} * @protected */ _getStatesData: function() { return { unchecked: { ariaState: 'false', checked: false, nextState: 'checked', nextStateIfRequired: 'checked', // filled isIndeterminate: false }, checked: { ariaState: 'true', checked: true, nextState: 'unchecked', // filled nextStateIfRequired: 'filled', isIndeterminate: false }, filled: { ariaState: 'mixed', checked: false, nextState: 'unchecked', nextStateIfRequired: 'checked', isIndeterminate: true } }; }, /** * Toggles field's inclusion in dependentFields to be ready for updating tooltip text * * @param {Object|Array} relatedFields Related fields that are dependent upon this field * @param {boolean} toggleFieldOn True if we're toggling fields on * @private */ _onToggleRelatedField: function(relatedFields, toggleFieldOn) { if (!_.isArray(relatedFields)) { // make sure related fields is an array relatedFields = [relatedFields]; } if (toggleFieldOn) { _.each(relatedFields, function(relatedField) { //If service field is toggled on //Add all the other service related fields to the dependent fields as they are all co-dependent if (_.contains(this.serviceRelatedFieldArr, relatedField.name)) { _.each(this.serviceRelatedFieldArr, function(serviceRelatedField) { this.dependentFields[serviceRelatedField] = { module: relatedField.def.labelModule, field: serviceRelatedField, reason: 'related_fields' }; }, this); } else { this.dependentFields[relatedField.name] = { module: relatedField.def.labelModule, field: relatedField.name, reason: 'related_fields' }; } }, this); this.isRequired = true; if (this.currentStateName === 'unchecked') { // if we haven't changed this field from unchecked yet // change to the related state if (_.contains(this.serviceRelatedFieldArr, this.name)) { // if this is a service field, change the state of all its related field to checked this.changeState('checked'); } else { this.changeState('filled'); } } } else { _.each(relatedFields, function(relatedField) { if (_.contains(this.serviceRelatedFieldArr, relatedField.name)) { // if one service field is deleted from the dependentFields list, // remove all the service related fields from it as well _.each(this.serviceRelatedFieldArr, function(serviceRelatedField) { delete this.dependentFields[serviceRelatedField]; }, this); } else { delete this.dependentFields[relatedField.name]; } }, this); if (_.isEmpty(this.dependentFields)) { // Removing related fields that are not required by any displayed fields and is not checked if (this.currentStateName === 'filled') { this.changeState('unchecked'); } if (this.currentStateName === 'checked' && _.contains(this.serviceRelatedFieldArr, this.name)) { // removing all the service related fields if even one is not checked and is not being displayed this.changeState('unchecked'); } this.isRequired = false; } } // bubble up the related fields if (this.def.relatedFields) { if (toggleFieldOn || (!toggleFieldOn && !this.isRequired && this.currentStateName === 'unchecked')) { // only add this field when we're toggling fields on, // or when toggling them off and this field is no longer required // and this field is unchecked relatedFields.push(this); } _.each(this.def.relatedFields, function(fieldName) { // If the toggled field is a service field // Don't trigger the related toggle listener for service related fields // else it results in an infinite loop if (!_.contains(this.serviceRelatedFieldArr, fieldName)) { this.context.trigger( 'config:' + this.def.eventViewName + ':' + fieldName + ':related:toggle', relatedFields, toggleFieldOn ); } }, this); } this.render(); }, /** * Handles when the Restore Defaults link is clicked in config-columns * * @protected */ _onFieldsReset: function(defaultFieldList) { // reset dependent fields back this.dependentFields = _.clone(this.def.dependentFields) || {}; this.isRequired = !_.isEmpty(this.dependentFields); if (!_.contains(defaultFieldList, this.name)) { if (this.def.initialState === 'checked' && !this.isRequired) { this.def.initialState = 'unchecked'; } else if (this.def.initialState === 'unchecked' && this.isRequired) { this.def.initialState = 'checked'; } else if (this.def.initialState === 'checked' && this.isRequired && _.intersection(defaultFieldList, this.dependentFields).length === 0) { this.def.initialState = 'unchecked'; this.def.dependentFields = {}; } } else { // Making sure default fields are checked. this.def.initialState = 'checked'; } this.changeState(this._getInitialState()); }, /** * Handles when a user clicks on the field input * * @param {Event} evt The click event object */ onCheckboxClicked: function(evt) { var nextState = this.isRequired ? this.currentState.nextStateIfRequired : this.currentState.nextState; var summaryColumns = this.view.model.get('summary_columns'); evt.preventDefault(); if (this.def.eventViewName === 'summary_columns' && summaryColumns && summaryColumns.length >= 6 && nextState === 'checked') { app.alert.show('max_summaryColumns_reached', { level: 'warning', messages: app.lang.get('LBL_SUMMARY_WORKSHEET_COLUMNS_MAX_WARNING', this.module), autoclose: true }, this); nextState = 'unchecked'; this._onCheckboxClicked(this.currentStateName, nextState); this.changeState(nextState); } else { this._onCheckboxClicked(this.currentStateName, nextState); // if the nextState for any service related field is 'filled', set the nextState to 'unchecked' // this only happens while unchecking any service field if (_.contains(this.serviceRelatedFieldArr, this.name) && nextState === 'filled') { nextState = 'unchecked'; } this.changeState(nextState); } }, /** * Handle any other events or actions that need to happen * when the checkbox is clicked, but before we change state. * * @param {string} currentState The name of the current state * @param {string} nextState The name of the next state * @protected */ _onCheckboxClicked: function(currentState, nextState) { this.context.trigger( 'config:' + this.def.eventViewName + ':field:change', this, currentState, nextState ); }, /** * @inheritdoc */ _updateTooltipText: function() { var text; var isLTR = app.lang.direction === 'ltr'; this.tooltipText = ''; if (!_.isEmpty(this.dependentFields)) { this.tooltipText = '<div class="tristate-checkbox-config-tooltip">' + this.tooltipLabel + '<ul>'; _.each(this.dependentFields, function(field) { text = isLTR ? field.module + ' - ' + field.field : field.field + ' - ' + field.module; this.tooltipText += '<li>' + text + '</li>'; }, this); this.tooltipText += '</ul></div>'; } }, /** * Returns the initial state for the field * * @return {string} The initial state for the field * @protected */ _getInitialState: function() { return this.def.initialState || 'unchecked'; } }) }, "convert-to-opportunity": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.ConvertToOpportunity * @alias SUGAR.App.view.fields.BaseQuotesConvertToOpportunity * @extends View.Fields.Base.RowactionField */ ({ // Convert-to-opportunity FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc * * @param {Object} options */ initialize: function(options) { this._super('initialize', [options]); this.type = 'rowaction'; this.context.on('button:convert_to_opportunity:click', this._onCreateOppFromQuoteClicked, this); }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('sync', this._toggleDisable, this); this.model.on('change:opportunity_id', this._toggleDisable, this); }, /** * Handler for when "Create Opp from Quote" is clicked * @private */ _onCreateOppFromQuoteClicked: function() { var id = this.model.get('id'); var url = app.api.buildURL('Quotes/' + id + '/opportunity'); app.alert.show('convert_to_opp', { level: 'info', title: app.lang.get('LBL_QUOTE_TO_OPPORTUNITY_STATUS'), messages: [''] }); app.api.call( 'create', url, null, { success: this._onCreateOppFromQuoteCallback, error: this._onCreateOppFromQuoteError }); }, /** * Success callback for Create Opp From Quote * @param data Data from the server * @private */ _onCreateOppFromQuoteCallback: function(data) { var id = data.record.id; var url = 'Opportunities/' + id; app.alert.dismiss('convert_to_opp'); app.router.navigate(url, {trigger: true}); }, /** * Error callback for Create Opp From Quote * @param data * @private */ _onCreateOppFromQuoteError: function(data) { app.alert.dismiss('convert_to_opp'); app.alert.show('error_convert', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: [data.message] }); }, /** * Reusable method for the event actions * * @private */ _toggleDisable: function() { if (this.disposed || _.isUndefined(this.model) || _.isNull(this.model)) { return; } var opportunityId = this.model.get('opportunity_id'); this.setDisabled(!(_.isUndefined(opportunityId) || _.isEmpty(opportunityId))); }, /** * @inheritdoc */ isAllowedDropdownButton: function() { // Filter logic for when it's on a dashlet return this.view.name !== 'dashlet-toolbar'; } }) }, "textarea": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.TextareaField * @alias SUGAR.App.view.fields.BaseQuotesTextareaField * @extends View.Fields.Base.TextareaField */ ({ // Textarea FieldTemplate (base) extendsFrom: 'BaseTextareaField', /** * @inheritdoc * * Format the value to a string. * Return an empty string for undefined, null and object types. * Convert boolean to 1 or 0. * Convert array, int and other types to a string. * * @param {mixed} value to format * @return {string} the formatted value */ format: function(value) { if (_.isString(value)) { if (this.tplName !== 'edit') { let shortComment = value; var max = this.tplName === 'quote-data-grand-totals-header' ? 20 : this._settings.max_display_chars; value = { long: this.getDescription(value, false), defaultValue: value, }; if (value.long && value.long.string.length > max) { value.short = this.getDescription(shortComment, true); } } return value; } if (_.isUndefined(value) || _.isNull(value) || (_.isObject(value) && !_.isArray(value)) ) { return ''; } if (_.isBoolean(value)) { return value === true ? '1' : '0'; } return value.toString(); } }) } }} , "views": { "base": { "quote-data-grand-totals-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.QuoteDataGrandTotalsHeaderView * @alias SUGAR.App.view.views.BaseQuotesQuoteDataGrandTotalsHeaderView * @extends View.Views.Base.View */ ({ // Quote-data-grand-totals-header View (base) /** * @inheritdoc */ events: { 'click [name="create_qli_button"]': '_onCreateQLIBtnClicked', 'click [name="create_comment_button"]': '_onCreateCommentBtnClicked', 'click [name="create_group_button"]': '_onCreateGroupBtnClicked' }, /** * @inheritdoc */ className: 'quote-data-grand-totals-header-wrapper quote-totals-row border-[--border-base] border-y flex h-11', /** * Handles when the create Quoted Line Item button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateQLIBtnClicked: function(evt) { this.context.trigger('quotes:defaultGroup:create', 'qli'); }, /** * Handles when the create Comment button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateCommentBtnClicked: function(evt) { this.context.trigger('quotes:defaultGroup:create', 'note'); }, /** * Handles when the create Group button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateGroupBtnClicked: function(evt) { this.context.trigger('quotes:group:create'); } }) }, "config-totals-footer-rows": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigTotalsFooterRowsView * @alias SUGAR.App.view.views.BaseQuotesConfigTotalsFooterRowsView * @extends View.Views.Base.View */ ({ // Config-totals-footer-rows View (base) /** * CSS Class for Totals fields */ sortableFieldsContainerClass: 'totals-fields', /** * CSS Class for Grand Totals fields */ sortableGrandTotalFieldsContainerClass: 'grand-total-fields', /** * Array to hold the Totals fields objects */ footerFields: undefined, /** * Array to hold the Grand Totals fields objects */ footerGrandTotalFields: undefined, /** * Data attribute key to use for Totals fields */ fieldTotalKey: 'total', /** * Data attribute key to use for Grand Totals fields */ fieldGrandTotalKey: 'grand-total', /** * Array to hold the server synced fields objects */ syncedFields: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.footerFields = []; this.footerGrandTotalFields = []; this.syncedFields = []; }, /** * Sets an array of fields into the footer rows * * @param {Array} footerFields The array of footer fields to set in the footer rows view */ setFooterRowFields: function(footerFields) { this.syncedFields = _.clone(footerFields); this.footerFields = []; this.footerGrandTotalFields = []; this.model.set(this.options.eventViewName, this.syncedFields); _.each(this.syncedFields, function(field) { field.syncedType = field.type; field.type = 'currency'; field.syncedCssClass = field.syncedCssClass || field.css_class || ''; field.css_class = ''; if (field.syncedCssClass.indexOf('grand-total') === -1) { this.footerFields.push(field); } else { this.footerGrandTotalFields.push(field); } }, this); this.render(); }, /** * Adds a field to the list of footer rows * * @param {Object} field The field defs of the field to add */ addFooterRowField: function(field) { var newFieldsArr; // add field to top of footerFields this.footerFields.unshift(field); // rebuild the fields array for the model newFieldsArr = this._parseFieldsForModel(); // save and render the new fields state this.model.set(this.options.eventViewName, newFieldsArr); this.render(); }, /** * Removes a field from the list of footer rows * * @param {Object} field The field defs of the field to remove */ removeFooterRowField: function(field) { var newFieldsArr; // remove field from wherever it exists this.footerFields = _.reject(this.footerFields, function(f) { return f.name === field.name; }); this.footerGrandTotalFields = _.reject(this.footerGrandTotalFields, function(f) { return f.name === field.name; }); // rebuild the fields array for the model newFieldsArr = this._parseFieldsForModel(); // save and render the new fields state this.model.set(this.options.eventViewName, newFieldsArr); this.render(); }, /** * @inheritdoc */ render: function() { this._super('render'); this.$('.connected-containers').sortable({ // the items to make sortable items: '.sortable-item', // adds a slow animation when "dropping" a group, removing this causes the row // to immediately snap into place wherever it's sorted revert: true, // connect all connected-containers with each other connectWith: '.connected-containers', // allow drag to only go in Y axis direction axis: 'y', // the CSS class to apply to the placeholder underneath the helper clone the user is dragging placeholder: 'ui-state-highlight', // the cursor to use when dragging cursor: 'move', // handler for when dragging stops; the "drop" event stop: _.bind(this._onDragStop, this) }).disableSelection(); this.$('.connected-containers').droppable({ accept: '.sortable-item', stop: _.bind(this._onDragStop, this) }); }, /** * Handles when a user drops a dragged item into a sortable/droppable container * * @param {jQuery.Event} evt The jQuery drag stop event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onDragStop: function(evt, ui) { var $el = $(ui.item || ui.draggable); var fieldName = $el.data('fieldName'); var fieldType = $el.data('fieldType'); var groupType = $el.parent().data('groupType'); var sortableTotalItemsCssSelector = '.' + this.sortableFieldsContainerClass + ' .sortable-item'; var sortableGrandTotalItemsCssSelector = '.' + this.sortableGrandTotalFieldsContainerClass + ' .sortable-item'; var newFieldsArr; if (fieldType !== groupType) { // the field has changed groups if (fieldType === this.fieldTotalKey && groupType === this.fieldGrandTotalKey) { // was total, now grand-total this._moveFieldToNewPosition( fieldName, this.footerFields, this.footerGrandTotalFields, sortableGrandTotalItemsCssSelector ); } else if (fieldType === this.fieldGrandTotalKey && groupType === this.fieldTotalKey) { // was grand-total, now total this._moveFieldToNewPosition( fieldName, this.footerGrandTotalFields, this.footerFields, sortableTotalItemsCssSelector ); } // set the new group type onto the field $el.data('fieldType', groupType); } else { // field stayed in same group if (groupType === 'total') { this._moveFieldToNewPosition( fieldName, this.footerFields, this.footerFields, sortableTotalItemsCssSelector ); } else { this._moveFieldToNewPosition( fieldName, this.footerGrandTotalFields, this.footerGrandTotalFields, sortableGrandTotalItemsCssSelector ); } } newFieldsArr = this._parseFieldsForModel(); this.model.set(this.options.eventViewName, newFieldsArr); this.render(); }, /** * Parses footerFields and footerGrandTotalFields cleaning up CSS classes and merging them into one array * * @return {Array} The merged, processed array from footerFields and footerGrandTotalFields * @private */ _parseFieldsForModel: function() { var newFieldsArr = []; var cssArr; var tmpField; _.each(this.footerFields, function(field) { cssArr = []; tmpField = _.clone(field); if (tmpField.syncedCssClass) { cssArr = cssArr.concat(tmpField.syncedCssClass.split(' ')); } if (tmpField.css_class) { cssArr = cssArr.concat(tmpField.css_class.split(' ')); } if (tmpField.syncedType) { tmpField.type = tmpField.syncedType; } if (cssArr.length) { cssArr = _.chain(cssArr) // only unique classes .uniq() // remove any grand-total css class since this is not in the grand total section .without(this.fieldGrandTotalKey) .value(); tmpField.css_class = cssArr.join(' '); } newFieldsArr.push(_.pick(tmpField, 'name', 'type', 'label', 'css_class', 'default')); }, this); _.each(this.footerGrandTotalFields, function(field) { cssArr = []; tmpField = _.clone(field); if (tmpField.syncedCssClass) { cssArr = cssArr.concat(tmpField.syncedCssClass.split(' ')); } if (tmpField.css_class) { cssArr = cssArr.concat(tmpField.css_class.split(' ')); } if (tmpField.syncedType) { tmpField.type = tmpField.syncedType; } // make sure the grand total items have the grand total class cssArr.push(this.fieldGrandTotalKey); if (cssArr.length) { tmpField.css_class = _.uniq(cssArr).join(' '); } newFieldsArr.push(_.pick(tmpField, 'name', 'type', 'label', 'css_class', 'default')); }, this); return newFieldsArr; }, /** * Moves a field to a new group and position if oldGroup and newGroup are different. * If oldGroup and newGroup are the same, it just moves a field to a new position * inside the same group. * * @param {string} fieldName The name of the field being moved * @param {Array} oldGroup The old group's array of fields * @param {Array} newGroup The new group's array of fields * @param {string} newGroupSelector The css selector for the new group * @private */ _moveFieldToNewPosition: function(fieldName, oldGroup, newGroup, newGroupSelector) { var tmpField; var tmpFieldIndex; var $newGroupElements; // find the index of the item in the old group tmpFieldIndex = _.findIndex(oldGroup, function(row) { return row.name === fieldName; }, this); // remove the field from the old group tmpField = oldGroup.splice(tmpFieldIndex, 1)[0]; // get the elements inside the new group $newGroupElements = this.$(newGroupSelector); // get the index of the field in the new group tmpFieldIndex = _.findIndex($newGroupElements, function(el) { return $(el).data('fieldName') === fieldName; }, this); // add the moved field into the new group newGroup.splice(tmpFieldIndex, 0, tmpField); } }) }, "product-catalog-dashlet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ProductCatalogDashletView * @alias SUGAR.App.view.views.QuotesProductCatalogDashletView * @extends View.Views.Base.ProductCatalogDashletView * @deprecated Use {@link View.Views.Base.ProductCatalogDashletView} instead */ ({ // Product-catalog-dashlet View (base) extendsFrom: 'ProductCatalogDashletView', initialize: function(options) { app.logger.warn('View.Views.Base.Quotes.ProductCatalogDashletView is deprecated. Use ' + 'View.Views.Base.ProductCatalogDashletView instead'); this._super('initialize', [options]); } }) }, "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.MassupdateView * @alias SUGAR.App.view.views.BaseQuotesMassupdateView * @extends View.Views.Base.MassupdateView */ ({ // Massupdate View (base) extendsFrom: 'MassupdateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['QuotesConversionRateLocking']); this._super('initialize', [options]); }, /** * @inheritdoc * * Extends parent function to add confirmation regarding conversion rate locking */ handleValidationSuccess: function(massUpdate, validate) { let attributes = validate.attributes || this.getAttributes(); if (_.has(attributes, 'lock_conversion_rates')) { let label = attributes.lock_conversion_rates ? 'LBL_QUOTES_CONVERSION_LOCK_MASSUPDATE_CONFIRM' : 'LBL_QUOTES_CONVERSION_UNLOCK_MASSUPDATE_CONFIRM'; let message = app.lang.get(label, 'Quotes', { lockConversionRatesName: app.lang.get(this._getLockFieldLabel(), 'Quotes') }); app.alert.show('quote_confirm_lock_change', { level: 'confirmation', messages: message, onConfirm: () => { this._super('handleValidationSuccess', [massUpdate, validate]); } }); } else { this._super('handleValidationSuccess', [massUpdate, validate]); } } }) }, "config-footer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigFooterView * @alias SUGAR.App.view.views.BaseQuotesConfigFooterView * @extends View.Views.Base.Quotes.ConfigFooterView */ ({ // Config-footer View (base) /** * @inheritdoc */ extendsFrom: 'QuotesConfigPanelView', /** * @inheritdoc */ events: { 'click .restore-defaults-btn': 'onClickRestoreDefaultsBtn' }, /** * The default list of field names for the Quotes worksheet columns */ listDefaultFieldNames: [ 'new_sub', 'tax', 'shipping', 'total' ], /** * The Label names from each of the default fields */ listDefaultFieldNameLabels: undefined, /** * The list header view * @type {View.Views.Base.Quotes.ConfigTotalsFooterRowsView} */ footerRowsView: undefined, /** * Contains an array of all the default fields to reset the list header */ defaultFields: [{ name: 'new_sub', type: 'currency' }, { name: 'tax', type: 'currency', related_fields: ['taxrate_value'] }, { name: 'shipping', type: 'quote-footer-currency', css_class: 'quote-footer-currency', default: '0.00' }, { name: 'total', label: 'LBL_LIST_GRAND_TOTAL', type: 'currency', css_class: 'grand-total', convertToBase: false }], /** * Contains an array of all the current fields in the list header */ footerRowFields: undefined, /** * @inheritdoc */ initialize: function(options) { var namesLen = this.listDefaultFieldNames.length; var quoteGrandTotalFooterListMeta = app.metadata.getView('Quotes', 'quote-data-grand-totals-footer'); var field; var fieldLabels = []; var fieldLabel; var fieldLabelModule; this._super('initialize', [options]); this.quotesFieldMeta = app.metadata.getModule('Quotes', 'fields'); // pluck all the fields arrays from panels and flatten into one array this.footerRowFields = _.flatten(_.pluck(quoteGrandTotalFooterListMeta.panels, 'fields')); _.each(this.footerRowFields, function(field) { field.labelModule = this._getFieldLabelModule(field); }, this); // build the list header labels and defaultFields this.listDefaultFieldNameLabels = []; for (var i = 0; i < namesLen; i++) { // try to get view defs from the quote-data-group-list meta field = _.find(this.footerRowFields, function(headerField) { return this.listDefaultFieldNames[i] === headerField.name; }, this); if (!field) { // if the field didn't exist in the group list meta, use the field vardef field = this.quotesFieldMeta[this.listDefaultFieldNames[i]]; } // use either label (viewdefs) or vname (vardefs) if (field && (field.label || field.vname)) { fieldLabel = field.label || field.vname; fieldLabelModule = 'Quotes'; fieldLabels.push(app.lang.get(fieldLabel, fieldLabelModule)); } } this.listDefaultFieldNameLabels = fieldLabels.join(', '); }, /** * @inheritdoc */ _getEventViewName: function() { return 'footer_rows'; }, /** * Returns the module to use for the label if no label module is given * * @param {Object} field * @return {string} * @private */ _getFieldLabelModule: function(field) { return field.labelModule || 'Quotes'; }, /** * @inheritdoc * * Only return currency type fields from the Quotes module for the Footer view */ _getPanelFields: function() { var fields = []; _.each(this.context.get('quotesFields'), function(f, key) { if (f.type === 'currency') { fields.push(_.extend({ name: key }, f)); } }, this); return fields; }, /** * @inheritdoc */ _getPanelFieldsModule: function() { return 'Quotes'; }, /** * @inheritdoc */ onConfigPanelShow: function() { if (this.dependentFields) { this.context.trigger('config:fields:change', this.eventViewName, this.panelFields); } }, /** * @inheritdoc */ _onDependentFieldsChange: function(context, fieldDeps) { var pFieldDeps; var pRelatedFields; var pRelatedField; var pDependentField; var tmpRelatedFields; var relatedFieldsList = []; var tmpField; this._super('_onDependentFieldsChange', [context, fieldDeps]); pFieldDeps = this.dependentFields.Quotes; pRelatedFields = this.relatedFields.Quotes; _.each(this.panelFields, function(field) { pDependentField = pFieldDeps[field.name]; pRelatedField = pRelatedFields[field.name]; if (pDependentField) { tmpRelatedFields = _.extend({}, pDependentField.locked, pDependentField.related); if (!_.isEmpty(tmpRelatedFields)) { field.dependentFields = tmpRelatedFields; field.required = true; } if (field.required && !field.initialState) { field.initialState = 'filled'; relatedFieldsList.push(field.name); } } if (pRelatedField) { tmpRelatedFields = _.extend({}, pRelatedField.locked, pRelatedField.related); if (!_.isEmpty(tmpRelatedFields)) { field.relatedFields = field.relatedFields || []; _.each(tmpRelatedFields, function(relField, relFieldName) { field.relatedFields.push(relFieldName); }, this); } } tmpField = _.find(this.footerRowFields, function(headerField) { return headerField.name === field.name; }); if (tmpField) { // if this panelField exists in footerRowFields, set to visible field.initialState = 'checked'; } }, this); this.model.set(this.eventViewName + '_related_fields', relatedFieldsList); // Signal to the layout that the fields for this panel are loaded this.layout.trigger('config:panel:fields:loaded', this); }, /** * @inheritdoc */ _onConfigFieldChange: function(field, oldState, newState) { var fieldVarDef = this.quotesFieldMeta[field.name]; var fieldViewDef; var wasVisible = oldState === 'checked'; var isNowVisible = newState === 'checked'; var isUnchecked = newState === 'unchecked'; var columnChanged = false; var toggleRelatedFields; if (!wasVisible && isNowVisible) { // field was not visible, but now is visible fieldViewDef = { name: fieldVarDef.name, type: fieldVarDef.type, label: fieldVarDef.vname || fieldVarDef.label }; fieldViewDef.labelModule = this._getFieldLabelModule(field); // add the column to header fields this.footerRowsView.addFooterRowField(fieldViewDef); toggleRelatedFields = true; columnChanged = true; } else if (wasVisible && !isNowVisible) { // field was visible, but now is not visible, so remove from columns // remove the column from header fields this.footerRowsView.removeFooterRowField(fieldVarDef); toggleRelatedFields = false; columnChanged = true; } else if (!wasVisible && !isNowVisible && isUnchecked) { columnChanged = true; toggleRelatedFields = false; } if (columnChanged) { if (!_.isUndefined(toggleRelatedFields) && field.def.relatedFields) { _.each(field.def.relatedFields, function(fieldName) { this.context.trigger( 'config:' + this.eventViewName + ':' + fieldName + ':related:toggle', field, toggleRelatedFields ); }, this); } } }, /** * @inheritdoc */ render: function() { this._super('render'); this.footerRowsView = app.view.createView({ context: this.context, eventViewName: this.eventViewName, type: 'config-totals-footer-rows', layout: this, model: this.model }); this.$('.quote-footer-rows').append(this.footerRowsView.el); // set the column header fields and render this.footerRowsView.setFooterRowFields(this.footerRowFields); }, /** * @inheritdoc */ _customFieldDef: function(def) { def.eventViewName = this.eventViewName; return def; }, /** * Handles the click event when user clicks to Restore Default fields * * @param {jQuery.Event} evt The jQuery click event */ onClickRestoreDefaultsBtn: function(evt) { var fieldList = _.pluck(this.defaultFields, 'name'); this.footerRowsView.setFooterRowFields(this.defaultFields); this.context.trigger('config:fields:' + this.eventViewName + ':reset', fieldList); } }) }, "config-list-header-columns": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigListHeaderColumnsView * @alias SUGAR.App.view.views.BaseQuotesConfigListHeaderColumnsView * @extends View.Views.Base.FlexListView */ ({ // Config-list-header-columns View (base) /** * @inheritdoc */ extendsFrom: 'FlexListView', /** * @inheritdoc */ plugins: [ 'MassCollection', 'ReorderableColumns' ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.massCollection = this.collection; this.leftColumns = []; this.addMultiSelectionAction(); this.template = app.template.getView('config-list-header-columns', 'Quotes'); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.on('list:reorder:columns', this.onSheetColumnsOrderChanged, this); }, /** * Handles when there's a change in the order of list header columns * * @param {Object} fields The fields object sent from ReorderableColumns plugin * @param {Array} newFieldNameOrder The new order of field names */ onSheetColumnsOrderChanged: function(fields, newFieldNameOrder) { var newFieldOrder = []; var headerFields = this.model.get(this.options.eventViewName); _.each(newFieldNameOrder, function(fieldName) { newFieldOrder.push(_.find(headerFields, function(field) { return field.name === fieldName; })); }, this); this.model.set(this.options.eventViewName, newFieldOrder); }, /** * Add multi selection field to left column using Quote data fields * * @override */ addMultiSelectionAction: function() { var buttons = []; var disableSelectAllAlert = !!this.meta.selection.disable_select_all_alert; if (this.layout && this.layout.name === 'config-summary') { var _generateMeta = function(buttons, disableSelectAllAlert) { return { name: '', type: 'button', icon: 'sicon-plus', value: false, sortable: false }; }; this.leftColumns.push(_generateMeta(buttons, disableSelectAllAlert)); } else { var _generateMeta = function(buttons, disableSelectAllAlert) { return { name: 'quote-data-mass-actions', type: 'fieldset', fields: [ { type: 'quote-data-actionmenu', buttons: buttons || [], disable_select_all_alert: !!disableSelectAllAlert } ], value: false, sortable: false }; }; buttons = this.meta.selection.actions; this.leftColumns.push(_generateMeta(buttons, disableSelectAllAlert)); } }, /** * @inheritdoc */ render: function() { var groupBtn; var massDeleteBtn; this._super('render'); groupBtn = _.find(this.nestedFields, function(field) { return field.name === 'group_button'; }); massDeleteBtn = _.find(this.nestedFields, function(field) { return field.name === 'massdelete_button'; }); if (groupBtn) { groupBtn.setDisabled(true); } if (massDeleteBtn) { massDeleteBtn.setDisabled(true); } }, /** * Sets the List Header column field names and re-renders * * @param {Array} headerFieldList The list of field */ setColumnHeaderFields: function(headerFieldList) { headerFieldList = _.clone(headerFieldList); this.meta.panels = [ { fields: headerFieldList }]; this.model.set(this.options.eventViewName, headerFieldList); this._fields = this.parseFields(); this.render(); }, /** * Adds a column header to the list columns * * @param {Object} field The field defs of the field to add */ addColumnHeaderField: function(field) { var columns = this.model.get(this.options.eventViewName); columns.unshift(field); this.meta.panels[0].fields = columns; this.model.set(this.options.eventViewName, columns); this._fields = this.parseFields(); this.render(); }, /** * Removes a column header from the list columns * * @param {Object} field The field defs of the field to remove */ removeColumnHeaderField: function(field) { var fields = this.meta.panels[0].fields; fields = _.reject(fields, function(headerField) { return headerField.name === field.name; }); this.meta.panels[0].fields = fields; this.model.set(this.options.eventViewName, fields); this._fields = this.parseFields(); this.render(); } }) }, "config-panel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigPanelView * @alias SUGAR.App.view.views.BaseQuotesConfigPanelView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-panel View (base) /** * @inheritdoc */ extendsFrom: 'BaseConfigPanelView', /** * Holds an array of field names for the panel */ panelFieldNameList: undefined, /** * Holds an array of field viewdefs for the panel */ panelFields: undefined, /** * Contains the map of all related field dependencies */ dependentFields: undefined, /** * Contains the map of all dependencies for each field */ relatedFields: undefined, /** * The view name ID to use in events */ eventViewName: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.eventViewName = this._getEventViewName(); this.getPanelFieldNamesList(); var helpUrl = { more_info_url: '<a href="' + app.help.getMoreInfoHelpURL('config', 'QuotesConfig') + '" target="_blank">', more_info_url_close: '</a>', }; var viewQuotesObj = app.help.get('Quotes', 'config_opps', helpUrl); this.quotesDocumentation = app.template.getView('config-panel.help', this.module)(viewQuotesObj); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.once('change:dependentFields', this._onDependentFieldsChange, this); this.context.on('config:' + this.eventViewName + ':field:change', this._onConfigFieldChange, this); }, /** * Returns the event and view name for this config panel. * Should be overridden by child views. * * @return {string} * @private */ _getEventViewName: function() { return 'config_panel'; }, /** * Handles when the field dependencies list comes back from the config endpoint. * Should be extended in child classes to include anything specific views need to do * with the field dependencies list. * * @param {Core.Context} context * @param {Object} fieldDeps Dependent Fields * @protected */ _onDependentFieldsChange: function(context, fieldDeps) { this.dependentFields = _.clone(fieldDeps); this.relatedFields = _.clone(this.context.get('relatedFields')); this.panelFields = this._buildPanelFieldsList(); }, /** * Handles when a checkbox on the RHS gets toggled * * @param {View.Fields.Base.TristateCheckboxField} field The field that was toggled * @param {string} oldState The old state for the field * @param {string} newState The new state for the field * @protected */ _onConfigFieldChange: function(field, oldState, newState) { }, /** * Returns an Array of field names to be used by the panel fields * * @return {Array} */ getPanelFieldNamesList: function() { this.panelFieldNameList = []; }, /** * Returns an Array of field names to be used by the panel fields * * @param {Array} fields The array of fields to use for panelFields * @return {Array} * @protected */ _buildPanelFieldsList: function() { var fields = this._getPanelFields(); var moduleName = this._getPanelFieldsModule(); // convert fieldsObj to an array then sort the array by name if (!_.isArray(fields)) { var tmpArray = []; _.each(fields, function(value, key) { tmpArray.push(_.extend(value, { name: key })); }, this); fields = tmpArray; } // apply any additional sorting to the fields fields = this._customFieldsSorting(fields); // return an array of the objects that pass the criteria fields = this._customFieldsProcessing(fields); fields = _.map(fields, function(field) { var def = { name: field.name, label: app.lang.get(field.label, moduleName), type: 'tristate-checkbox', labelModule: moduleName, locked: field.locked, related: field.related }; return this._customFieldDef(def); }, this); return fields; }, /** * Extensible function to get the fields array to be used in buildPanelFieldsList * * @private */ _getPanelFields: function() { return []; }, /** * Extensible function to get the module name for the buildPanelFieldsList * * @private */ _getPanelFieldsModule: function() { return this.module; }, /** * Handles any custom changes to the field defs a child view might need to make * * @param {Object} def The field def * @return {Object} * @protected */ _customFieldDef: function(def) { return def; }, /** * Handles any custom field sorting that child classes might need to do. * By default, sort by the name field * * @param {Array} arr The fields array * @return {Array} * @protected */ _customFieldsSorting: function(arr) { return _.sortBy(arr, 'name'); }, /** * Handles any custom field processing, array manipulation, or changes * that child classes might need to do * * @param {Array} arr The fields array * @return {Array} * @protected */ _customFieldsProcessing: function(arr) { return arr; } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.RecordView * @alias SUGAR.App.view.views.BaseQuotesRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * Track the calculated fields from the model to be used when checking for unsaved changes * * @type {Array} */ calculatedFields: [], /** * registers additional editable fields from supporting quotes views */ additionalEditableFields: [], /** * Track the number of items in edit mode. * @type {number} */ editCount: 0, /** * Hashtable to keep track of id's in edit mode * @type {Object} */ editIds: {}, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary', 'QuotesViewSaveHelper', 'QuotesConversionRateLocking']); this._super('initialize', [options]); // get all the calculated fields from the model this.calculatedFields = _.chain(this.model.fields) .where({calculated: true}) .pluck('name') .value(); this.additionalEditableFields = []; }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.on('editable:handleEdit', this._handleEditShippingField, this); this.context.on('quotes:editableFields:add', function(field) { this.additionalEditableFields.push(field); this.editableFields.push(field); }, this); this.context.on('quotes:item:toggle', this._handleItemToggled, this); }, /** * @inheritdoc */ setEditableFields: function() { this._super('setEditableFields'); if (this.editableFields) { _.each(this.additionalEditableFields, function(field) { this.editableFields.push(field); }, this); } }, /** * @inheritdoc * * Overrides the existing record duplicateClicked to handle the unique * Quotes->ProductBundles->Products|ProductBundleNotes data structure */ duplicateClicked: function() { var bundles; var loadViewObj; var bundleModels = []; // create an empty Quote Bean var quoteModelCopy; var quoteContextCollection; var mainDropdownBtn; var copyItemCount = 0; if (this.editCount) { app.alert.show('quotes_qli_editmode', { level: 'error', title: '', messages: [app.lang.get('LBL_COPY_LINE_ITEMS', 'Quotes')] }); return; } // get the Edit dropdown button mainDropdownBtn = this.getField('main_dropdown'); // close the dropdown menu mainDropdownBtn.$el.removeClass('open'); bundles = this.model.get('bundles'); quoteModelCopy = app.data.createBean(this.model.module); quoteContextCollection = this.context.get('collection'); quoteModelCopy.copy(this.model); _.each(bundles.models, function(bundle) { var items = []; var bundleData = bundle.toJSON(); var pbItems = bundle.get('product_bundle_items'); // re-set pbItems (if it exists and if pbItems.models exists) to be pbItems.models pbItems = pbItems && pbItems.models; // loop over the product bundle items _.each(pbItems, function(pbItem) { var tmpItem = pbItem.toJSON(); var newBean; // get rid of an item's id and quote_id delete tmpItem.id; delete tmpItem.quote_id; if (_.isEmpty(tmpItem.product_template_name)) { // if product_template_name is empty, use the QLI's name tmpItem.product_template_name = tmpItem.name; } else { // if product_template_name is not empty, set that to the QLI's name tmpItem.name = tmpItem.product_template_name; } newBean = app.data.createBean(tmpItem._module, tmpItem); // set isCopied on the bean for currency fields to be set properly newBean.isCopied = true; copyItemCount++; // creates a Bean and pushes the individual Products|ProductBundleNotes to the array items.push(newBean); }, this); // remove any id or sugarlogic entries from the bundle data delete bundleData.id; delete bundleData['_products-rel_exp_values']; // remove any leftover create/delete arrays delete bundleData.products; // set items array onto the bundleData bundleData.product_bundle_items = items; bundleModels.push(bundleData); }, this); // get rid of the existing bundles data on the model quoteModelCopy.unset('bundles'); // set the model onto the context->collection quoteContextCollection.reset(quoteModelCopy); loadViewObj = { action: 'edit', collection: quoteContextCollection, copy: true, create: true, layout: 'create', model: quoteModelCopy, module: 'Quotes', relatedRecords: bundleModels, copyItemCount: copyItemCount }; // lead the Quotes create layout app.controller.loadView(loadViewObj); // update the browser URL with the proper app.router.navigate('#Quotes/create', {trigger: false}); }, /** * handles keeping track how many items are in edit mode. * @param {boolean} isEdit * @param {number} id id of the row being toggled * @private */ _handleItemToggled: function(isEdit, id) { if (isEdit) { if (_.isUndefined(this.editIds[id])) { this.editIds[id] = true; this.editCount++; } } else if (!isEdit && this.editCount > 0) { delete this.editIds[id]; this.editCount--; } }, /** * Override the save clicked function to check if things are in edit mode before saving. * * @inheritdoc */ saveClicked: function() { //if we don't have any qlis in edit mode, save. If we do, show a warning. if (this.editCount == 0) { this._super('saveClicked'); } else { app.alert.show('quotes_qli_editmode', { level: 'error', title: '', messages: [app.lang.get('LBL_SAVE_LINE_ITEMS', 'Quotes')] }); } }, /** * Override the cancel clicked function to retrigger sugarlogic. * * @inheritdoc */ cancelClicked: function() { this._super('cancelClicked'); this.context.trigger('list:editrow:fire'); }, /** * This is only when the Shipping field is clicked to handle toggling * it to Edit mode since it's outside of this view's element. This is * exactly the same as record.handleEdit except it grabs the jQuery * event target from the full page instead of this.el and also uses the * `this.editableFields` instead of this.getField to find the shipping field. * * @param {jQuery.Event} e The jQuery Click Event * @private */ _handleEditShippingField: function(e) { var $target; var cellData; var field; var cell; if (e) { // having to open this to full page $ instead of this.$ $target = $(e.target); cell = $target.parents('.record-cell'); } cellData = cell.data(); field = _.find(this.editableFields, function(field) { return field.name === cellData.name; }); // Set Editing mode to on. this.inlineEditMode = true; this.setButtonStates(this.STATE.EDIT); this.toggleField(field); if (cell.closest('.headerpane').length > 0) { this.toggleViewButtons(true); this.adjustHeaderpaneFields(); } }, /** * Extracts the field names from the metadata for directly related views/panels. * @param {string} [module] Module name. */ _getDataFields: function() { return _.union(this._super('_getDataFields'), ['locked_currency_rates']); }, /** * @inheritdoc */ getCustomSaveOptions: function(options) { options = options || {}; var returnObject = {}; // get the value that the server sent back var syncedValue = this.model.getSynced('currency_id'); // has the currency_id changed? if (this.model.get('currency_id') !== syncedValue) { // make copy of original function we are extending var origSuccess = options.success; // only do this if the currency_id field actually changes returnObject = { success: _.bind(function() { if (_.isFunction(origSuccess)) { origSuccess.apply(this, arguments); } // create the payload var bulkSaveRequests = this._createBulkBundlesPayload(); // send the payload this._sendBulkBundlesUpdate(bulkSaveRequests); }, this) }; } return returnObject; }, /** * Utility method to create the payload that will be send to the server via the bulk api call * to update all the product bundles currencies * @private */ _createBulkBundlesPayload: function() { // loop over all the bundles and create the requests const quoteModel = this.model; var bundles = this.model.get('bundles'); var bulkSaveRequests = []; var url; bundles.each(function(bundle) { // if the bundle is new, don't try and save it if (!bundle.isNew()) { // create the update url url = app.api.buildURL(bundle.module, 'update', { id: bundle.get('id') }); // save the request with the two fields that need to be updated // on the product bundle bulkSaveRequests.unshift({ url: url.substr(4), method: 'PUT', data: { currency_id: quoteModel.get('currency_id'), base_rate: quoteModel.get('base_rate') } }); } }); return bulkSaveRequests; }, /** * Send the payload via the bulk api * @param {Array} bulkSaveRequests * @private */ _sendBulkBundlesUpdate: function(bulkSaveRequests) { if (!_.isEmpty(bulkSaveRequests)) { app.api.call( 'create', app.api.buildURL(null, 'bulk'), { requests: bulkSaveRequests }, { success: _.bind(this._onBulkBundlesUpdateSuccess, this) } ); } }, /** * Update the bundles when the results from the bulk api call * @param {Array} bulkResponses * @private */ _onBulkBundlesUpdateSuccess: function(bulkResponses) { var bundles = this.model.get('bundles'); var bundle; _.each(bulkResponses, function(record) { bundle = bundles.get(record.contents.id); if (bundle) { bundle.setSyncedAttributes(record.contents); bundle.set(record.contents); } }, this); }, /** * @inheritdoc */ hasUnsavedChanges: function() { return this.hasUnsavedQuoteChanges(); }, /** * @inheritdoc */ handleSave: function() { this.checkConversionRateLock(() => { this._super('handleSave'); }); } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseQuotesConfigHeaderButtonsView * @extends View.View.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) /** * @inheritdoc */ extendsFrom: 'BaseConfigHeaderButtonsView', /** * @inheritdoc */ _getSaveConfigAttributes: function() { _.each(this.model.get('worksheet_columns'), function(column) { if (column.name === 'service_duration') { column.fields = column.fields || [ { 'name': 'service_duration_value', 'label': 'LBL_SERVICE_DURATION_VALUE' }, { 'name': 'service_duration_unit', 'label': 'LBL_SERVICE_DURATION_UNIT' }, ]; column.css_class = 'service-duration-field'; column.inline = true; } if (column.name === 'discount_field') { column.css_class += ' discount-field quote-discount-percent'; column.fields = [{ name: 'discount_amount', label: 'LBL_DISCOUNT_AMOUNT', type: 'discount-amount', discountFieldName: 'discount_select', related_fields: ['currency_id'], convertToBase: true, base_rate_field: 'base_rate', showTransactionalAmount: true }, { name: 'discount_select', type: 'discount-select', options: [], }]; } }, this); var saveObj = this.model.toJSON(); var lineNum; var footerRows = []; var quotesMeta = app.metadata.getModule('Quotes', 'fields'); // make sure related_fields contains description, currency_id, base_rate, quote_id, name, and // product_template_name & _id fields var requiredRelatedFields = [ 'service_duration_value', 'service_duration_unit', 'catalog_service_duration_value', 'catalog_service_duration_unit', 'subtotal', 'description', 'currency_id', 'base_rate', 'account_id', 'quote_id', 'name', 'position', 'product_template_id', 'product_template_name' ]; // make sure line_num field exists in worksheet_columns lineNum = _.find(saveObj.worksheet_columns, function(col) { return col.name === 'line_num'; }, this); if (!lineNum) { saveObj.worksheet_columns.unshift({ name: 'line_num', label: null, widthClass: 'cell-xsmall', css_class: 'line_num text-center', type: 'line-num', readonly: true }); } // tweak any worksheet columns fields _.each(saveObj.worksheet_columns, function(col) { if (col.name === 'product_template_name') { // force product_template_name to be required if it exists col.required = true; } if (col.type === 'image') { col.readonly = true; } if (col.label === 'LBL_DISCOUNT_AMOUNT' && col.name === 'discount_amount') { col.label = 'LBL_DISCOUNT_AMOUNT_VALUE'; } if (col.type === 'relate') { requiredRelatedFields.push(col.id_name); } if (col.type === 'parent') { requiredRelatedFields.push(col.id_name); requiredRelatedFields.push(col.type_name); } if (col.name === 'service_duration') { _.each(col.fields, function(field) { requiredRelatedFields.push(field.name); }, this); } }, this); _.each(requiredRelatedFields, function(field) { if (!_.contains(saveObj.worksheet_columns_related_fields, field)) { saveObj.worksheet_columns_related_fields.push(field); } }); _.each(saveObj.footer_rows, function(row) { var obj = { name: row.name, type: row.syncedType || row.type }; if (row.syncedCssClass || row.css_class) { obj.css_class = row.syncedCssClass || row.css_class; } if (row.hasOwnProperty('default')) { obj.default = row.default; } if (quotesMeta[row.name] && !quotesMeta[row.name].formula) { obj.type = 'quote-footer-currency'; obj.default = '0.00'; if (!obj.css_class || (row.css_class && row.css_class.indexOf('quote-footer-currency') === -1)) { obj.css_class = 'quote-footer-currency'; } } footerRows.push(obj); }, this); saveObj.footer_rows = footerRows; return saveObj; } }) }, "config-columns": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigColumnsView * @alias SUGAR.App.view.views.BaseQuotesConfigColumnsView * @extends View.Views.Base.Quotes.ConfigPanelView */ ({ // Config-columns View (base) /** * @inheritdoc */ extendsFrom: 'QuotesConfigPanelView', /** * @inheritdoc */ events: { 'click .restore-defaults-btn': 'onClickRestoreDefaultsBtn' }, /** * The default list of field names for the Quotes worksheet columns */ listDefaultFieldNames: [ 'quantity', 'product_template_name', 'mft_part_num', 'discount_price', 'discount_field', 'total_amount' ], /** * The Label names from each of the default fields */ listDefaultFieldNameLabels: undefined, /** * The list header view * @type {View.Views.Base.Quotes.ConfigListHeaderColumnsView} */ listHeaderView: undefined, /** * Contains an array of all the default fields to reset the list header */ defaultFields: undefined, /** * Contains an array of all the current fields in the list header */ listHeaderFields: undefined, /** * Products Module vardefs */ productsFieldMeta: undefined, /** * @inheritdoc */ initialize: function(options) { var productListMeta = app.metadata.getView('Products', 'quote-data-group-list'); this._super('initialize', [options]); this.productsFieldMeta = app.metadata.getModule('Products', 'fields'); //If service duration value and unit exists //Add a custom service_duration field in the productsFieldMeta if (!_.isUndefined(this.productsFieldMeta.service_duration_value) && !_.isUndefined(this.productsFieldMeta.service_duration_unit)) { var durationField = { 'name': 'service_duration', 'type': 'fieldset', 'css_class': 'service-duration-field', 'label': 'LBL_SERVICE_DURATION', 'inline': true, 'show_child_labels': false, 'fields': [ this.productsFieldMeta.service_duration_value, this.productsFieldMeta.service_duration_unit, ], 'related_fields': [ 'service_start_date', 'service_end_date', 'renewable', 'service', ], }; this.productsFieldMeta.service_duration = durationField; } this.defaultFields = []; // pluck all the fields arrays from panels and flatten into one array this.listHeaderFields = _.flatten(_.pluck(productListMeta.panels, 'fields')); // exclude the line_num field this.listHeaderFields = _.reject(this.listHeaderFields, function(field) { return field.name === 'line_num'; }); _.each(this.listHeaderFields, function(field) { field.labelModule = this._getFieldLabelModule(field); }, this); this.model.set(this.eventViewName, this.listHeaderFields); }, /** * @inheritdoc */ _getEventViewName: function() { return 'worksheet_columns'; }, /** * Returns the module to use for the label if no label module is given * * @param {Object} field * @return {string} * @private */ _getFieldLabelModule: function(field) { var label = field.label || field.vname; var labelModule = field.labelModule || 'Products'; var tmpLabel = app.lang.get(label, labelModule); if (tmpLabel.indexOf('LBL_') !== -1) { labelModule = 'Quotes'; } return labelModule; }, /** * @inheritdoc */ _onDependentFieldsChange: function(context, fieldDeps) { var pFieldDeps; var pRelatedFields; var pRelatedField; var pDependentField; var tmpRelatedFields; var relatedFieldsList = []; var tmpField; this._super('_onDependentFieldsChange', [context, fieldDeps]); pFieldDeps = this.dependentFields.Products; pRelatedFields = this.relatedFields.Products; // build default fields var defaultWorksheetColumns = this.context.get('defaultWorksheetColumns'); // pluck all the fields arrays from panels and flatten into one array this.defaultFields = _.flatten(_.pluck(defaultWorksheetColumns.panels, 'fields')); // exclude the line_num field this.defaultFields = _.reject(this.defaultFields, function(field) { return field.name === 'line_num'; }); // building Default Fields this.buildDefaultFields(); _.each(this.panelFields, function(field) { pDependentField = pFieldDeps[field.name]; pRelatedField = pRelatedFields[field.name]; if (pDependentField) { tmpRelatedFields = _.extend({}, pDependentField.locked, pDependentField.related); if (!_.isEmpty(tmpRelatedFields)) { field.dependentFields = tmpRelatedFields; field.required = true; } if (field.required && !field.initialState) { field.initialState = 'filled'; relatedFieldsList.push(field.name); } } if (pRelatedField) { tmpRelatedFields = _.extend({}, pRelatedField.locked, pRelatedField.related); if (!_.isEmpty(tmpRelatedFields)) { field.relatedFields = field.relatedFields || []; _.each(tmpRelatedFields, function(relField, relFieldName) { field.relatedFields.push(relFieldName); }, this); } } tmpField = _.find(this.listHeaderFields, function(headerField) { return headerField.name === field.name; }); if (tmpField) { // if this panelField exists in listHeaderFields, set to visible field.initialState = 'checked'; } }, this); this.model.set(this.eventViewName + '_related_fields', relatedFieldsList); // Signal to the layout that the fields for this panel are loaded this.layout.trigger('config:panel:fields:loaded', this); }, /** * */ buildDefaultFields: function() { var field; var fieldLabel; var fieldLabels = []; var fieldLabelModule; var tmpField; var _defaultFields = this.defaultFields; this.listDefaultFieldNameLabels = _.pluck(_defaultFields, 'name'); var namesLen = this.listDefaultFieldNameLabels.length; // build the list header labels and defaultFields this.listDefaultFieldNameLabels = []; this.defaultFields = []; for (var i = 0; i < namesLen; i++) { // try to get view defs from the quote-data-group-list meta field = _.find(this.listHeaderFields, function(headerField) { return this.listDefaultFieldNames[i] === headerField.name; }, this); if (!field) { // if the field didn't exist in the group list meta, use the field vardef field = _.find(_defaultFields, {name: this.listDefaultFieldNames[i]}) || this.productsFieldMeta[this.listDefaultFieldNames[i]]; } // use either label (viewdefs) or vname (vardefs) if (field && (field.label || field.vname)) { fieldLabel = field.label || field.vname; // check Products strings first fieldLabel = app.lang.get(fieldLabel, 'Products'); fieldLabelModule = 'Products'; if (fieldLabel.indexOf('LBL_') !== -1) { // if Products label just returned LBL_ string, check Quotes fieldLabel = app.lang.get(fieldLabel, 'Quotes'); fieldLabelModule = 'Quotes'; } fieldLabels.push(fieldLabel); tmpField = { name: field.name, label: fieldLabel, labelModule: fieldLabelModule, widthClass: field.widthClass, css_class: field.css_class || field.cssClass || '' }; if (field.name === 'product_template_name') { tmpField.type = 'quote-data-relate'; tmpField.required = true; } if (field.type === 'currency') { tmpField.convertToBase = true; tmpField.showTransactionalAmount = true; tmpField.related_fields = ['currency_id', 'base_rate']; } if (field.name === 'discount_field') { tmpField.type = 'fieldset'; tmpField.css_class += ' discount-field quote-discount-percent'; tmpField.fields = [{ name: 'discount_amount', label: 'LBL_DISCOUNT_AMOUNT', type: 'discount-amount', discountFieldName: 'discount_select', related_fields: ['currency_id'], convertToBase: true, base_rate_field: 'base_rate', showTransactionalAmount: true }, { name: 'discount_select', type: 'discount-select', options: [], }]; } // push the fieldDefs to default fields this.defaultFields.push(tmpField); } } this.listDefaultFieldNameLabels = fieldLabels.join(', '); }, /** * @inheritdoc */ _onConfigFieldChange: function(field, oldState, newState) { var fieldVarDef = _.find(this.defaultFields, {name: field.name}) ? _.find(this.defaultFields, {name: field.name}) : this.productsFieldMeta[field.name]; var fieldViewDef; var wasVisible = oldState === 'checked'; var isNowVisible = newState === 'checked'; var isUnchecked = newState === 'unchecked'; var columnChanged = false; var toggleRelatedFields; var serviceRelatedFieldsArr = [ 'service_duration', 'service_start_date', 'service_end_date', 'renewable', 'service' ]; if (!wasVisible && isNowVisible) { // field was not visible, but now is visible fieldViewDef = { name: fieldVarDef.name, type: fieldVarDef.type, label: fieldVarDef.vname || fieldVarDef.label }; if (fieldVarDef.type === 'relate') { fieldViewDef.id_name = fieldVarDef.id_name; } if (fieldVarDef.type === 'parent') { fieldViewDef.id_name = fieldVarDef.id_name; fieldViewDef.type_name = fieldVarDef.type_name; } fieldViewDef.name === 'discount_amount' ? (fieldViewDef.label = app.lang.get('LBL_DISCOUNT_AMOUNT_VALUE', 'Products')) : fieldViewDef.label; if (fieldViewDef.name === 'discount') { fieldViewDef = fieldVarDef; } fieldViewDef.labelModule = this._getFieldLabelModule(field); // add the column to header fields this.listHeaderView.addColumnHeaderField(fieldViewDef); // if a service field is added, then add all its related fields to the worksheet column as well if (_.intersection(fieldVarDef.related_fields, serviceRelatedFieldsArr).length > 0) { _.each(fieldVarDef.related_fields, function(relField) { var relatedFieldVarDef = this.productsFieldMeta[relField]; fieldViewDef = {}; fieldViewDef = { name: relatedFieldVarDef.name, type: relatedFieldVarDef.type, label: relatedFieldVarDef.vname || relatedFieldVarDef.label }; fieldViewDef.labelModule = this._getFieldLabelModule(relatedFieldVarDef); this.listHeaderView.addColumnHeaderField(fieldViewDef); }, this); } toggleRelatedFields = true; columnChanged = true; } else if (wasVisible && !isNowVisible) { // field was visible, but now is not visible, so remove from columns // remove the column from header fields this.listHeaderView.removeColumnHeaderField(fieldVarDef); // if a service field is removed, then remove all its related fields to the worksheet column as well if (_.intersection(fieldVarDef.related_fields, serviceRelatedFieldsArr).length > 0) { _.each(fieldVarDef.related_fields, function(relField) { var relatedFieldVarDef = this.productsFieldMeta[relField]; this.listHeaderView.removeColumnHeaderField(relatedFieldVarDef); }, this); } toggleRelatedFields = false; columnChanged = true; } else if (!wasVisible && !isNowVisible && isUnchecked) { columnChanged = true; toggleRelatedFields = false; } if (columnChanged) { if (!_.isUndefined(toggleRelatedFields) && field.def.relatedFields) { _.each(field.def.relatedFields, function(fieldName) { this.context.trigger( 'config:' + this.eventViewName + ':' + fieldName + ':related:toggle', field, toggleRelatedFields ); }, this); } } }, /** * @inheritdoc */ _getPanelFields: function() { return this.context.get('productsFields'); }, /** * @inheritdoc */ _getPanelFieldsModule: function() { return 'Products'; }, /** * @inheritdoc */ render: function() { this._super('render'); this.listHeaderView = app.view.createView({ context: this.context, eventViewName: this.eventViewName, type: 'config-list-header-columns', layout: this, model: this.model }); this.$('.quote-data-list-table').append(this.listHeaderView.el); // set the column header fields and render this.listHeaderView.setColumnHeaderFields(this.listHeaderFields); }, /** * Handles the click event when user clicks to Restore Default fields * @param evt */ onClickRestoreDefaultsBtn: function(evt) { var fieldList = _.pluck(this.defaultFields, 'name'); this.listHeaderView.setColumnHeaderFields(this.defaultFields); this.context.trigger('config:fields:' + this.eventViewName + ':reset', fieldList); }, /** * @inheritdoc */ onConfigPanelShow: function() { if (this.dependentFields) { //picking the service duration value and unit //these will be reinserted in the panelFields as a single fieldset var durationValueField = _.find(this.panelFields, function(field) { return field.name === 'service_duration_value'; }); var durationUnitField = _.find(this.panelFields, function(field) { return field.name === 'service_duration_unit'; }); //If service duration value and unit exists, don't add these to the howto panel columns //instead, add a custom service_duration field that ecapsulates both if (!_.isUndefined(durationValueField) && !_.isUndefined(durationUnitField)) { //removing the service duration unit and value fields from the howto panel this.panelFields = _.without(this.panelFields, durationUnitField, durationValueField); var durationField = { 'name': 'service_duration', 'type': 'tristate-checkbox', 'css_class': 'service-duration-field', 'label': 'LBL_SERVICE_DURATION', 'labelModule': 'Products', 'fields': [ { 'name': 'service_duration_value', 'label': 'LBL_SERVICE_DURATION_VALUE', }, { 'name': 'service_duration_unit', 'label': 'LBL_SERVICE_DURATION_UNIT', } ], 'relatedFields': [ 'service', 'service_start_date', 'service_end_date', 'renewable', ], 'eventViewName': this.eventViewName, }; this.panelFields = _.union(this.panelFields, [durationField]); } this.context.trigger('config:fields:change', this.eventViewName, this.panelFields); } }, /** * @inheritdoc */ _customFieldDef: function(def) { def.name === 'discount_amount' ? (def.label = app.lang.get('LBL_DISCOUNT_AMOUNT_VALUE', 'Products')) : def.label; def.eventViewName = this.eventViewName; return def; }, /** * @inheritdoc */ _dispose: function() { if (this.listHeaderView) { this.listHeaderView.dispose(); this.listHeaderView = null; } this._super('_dispose'); } }) }, "config-summary": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigSummaryView * @alias SUGAR.App.view.views.BaseQuotesConfigSummaryView * @extends View.Views.Base.Quotes.ConfigPanelView */ ({ // Config-summary View (base) /** * @inheritdoc */ extendsFrom: 'QuotesConfigPanelView', /** * @inheritdoc */ events: { 'click .restore-defaults-btn': 'onClickRestoreDefaultsBtn' }, /** * The default list of field names for the Quotes summary columns */ listDefaultFieldNames: [ 'deal_tot', 'new_sub', 'tax', 'shipping', 'total' ], /** * The Label names from each of the default fields */ listDefaultFieldNameLabels: undefined, /** * The list header view * @type {View.Views.Base.Quotes.ConfigListHeaderColumnsView} */ listHeaderView: undefined, /** * Contains an array of all the default fields to reset the list header */ defaultFields: undefined, /** * Contains an array of all the current fields in the list header */ listHeaderFields: undefined, /** * @inheritdoc */ initialize: function(options) { var namesLen = this.listDefaultFieldNames.length; var quoteGrandTotalHeaderListMeta = app.metadata.getView('Quotes', 'quote-data-grand-totals-header'); var field; var fieldLabels = []; var fieldLabel; var fieldLabelModule; var tmpField; this._super('initialize', [options]); this.quotesFieldMeta = app.metadata.getModule('Quotes', 'fields'); this.defaultFields = []; // pluck all the fields arrays from panels and flatten into one array this.listHeaderFields = _.flatten(_.pluck(quoteGrandTotalHeaderListMeta.panels, 'fields')); _.each(this.listHeaderFields, function(field) { field.labelModule = this._getFieldLabelModule(field); }, this); // build the list header labels and defaultFields this.listDefaultFieldNameLabels = []; for (var i = 0; i < namesLen; i++) { // try to get view defs from the quote-data-group-list meta field = _.find(this.listHeaderFields, function(headerField) { return this.listDefaultFieldNames[i] === headerField.name; }, this); if (!field) { // if the field didn't exist in the group list meta, use the field vardef field = this.quotesFieldMeta[this.listDefaultFieldNames[i]]; } // use either label (viewdefs) or vname (vardefs) if (field && (field.label || field.vname)) { fieldLabel = field.label || field.vname; // check Products strings first fieldLabel = app.lang.get(fieldLabel, 'Quotes'); fieldLabelModule = 'Quotes'; fieldLabels.push(fieldLabel); tmpField = { name: field.name, label: fieldLabel, labelModule: fieldLabelModule, widthClass: field.widthClass, css_class: field.css_class || field.cssClass || '' }; // push the fieldDefs to default fields this.defaultFields.push(tmpField); } } this.listDefaultFieldNameLabels = fieldLabels.join(', '); this.model.set(this.eventViewName, this.listHeaderFields); }, /** * @inheritdoc */ _getEventViewName: function() { return 'summary_columns'; }, /** * Returns the module to use for the label if no label module is given * * @param {Object} field * @return {string} * @private */ _getFieldLabelModule: function(field) { return field.labelModule || 'Quotes'; }, /** * @inheritdoc */ _getPanelFields: function() { var fields = []; _.each(this.context.get('quotesFields'), function(f, key) { if (f.type !== 'collection' && key.indexOf('_id') === -1) { fields.push(_.extend({ name: key }, f)); } }, this); return fields; }, /** * @inheritdoc */ _getPanelFieldsModule: function() { return 'Quotes'; }, /** * @inheritdoc */ onConfigPanelShow: function() { if (this.dependentFields) { this.context.trigger('config:fields:change', this.eventViewName, this.panelFields); } }, /** * @inheritdoc */ _onDependentFieldsChange: function(context, fieldDeps) { var pFieldDeps; var pRelatedFields; var pRelatedField; var pDependentField; var tmpRelatedFields; var relatedFieldsList = []; var tmpField; this._super('_onDependentFieldsChange', [context, fieldDeps]); pFieldDeps = this.dependentFields.Quotes; pRelatedFields = this.relatedFields.Quotes; _.each(this.panelFields, function(field) { pDependentField = pFieldDeps[field.name]; pRelatedField = pRelatedFields[field.name]; if (pDependentField) { tmpRelatedFields = _.extend({}, pDependentField.locked, pDependentField.related); if (!_.isEmpty(tmpRelatedFields)) { field.dependentFields = tmpRelatedFields; field.required = true; } if (field.required && !field.initialState) { field.initialState = 'filled'; relatedFieldsList.push(field.name); } } if (pRelatedField) { tmpRelatedFields = _.extend({}, pRelatedField.locked, pRelatedField.related); if (!_.isEmpty(tmpRelatedFields)) { field.relatedFields = field.relatedFields || []; _.each(tmpRelatedFields, function(relField, relFieldName) { field.relatedFields.push(relFieldName); }, this); } } tmpField = _.find(this.listHeaderFields, function(headerField) { return headerField.name === field.name; }); if (tmpField) { // if this panelField exists in listHeaderFields, set to visible field.initialState = 'checked'; } }, this); this.model.set(this.eventViewName + '_related_fields', relatedFieldsList); // Signal to the layout that the fields for this panel are loaded this.layout.trigger('config:panel:fields:loaded', this); }, /** * @inheritdoc */ _onConfigFieldChange: function(field, oldState, newState) { var fieldVarDef = this.quotesFieldMeta[field.name]; var fieldViewDef; var wasVisible = oldState === 'checked'; var isNowVisible = newState === 'checked'; var isUnchecked = newState === 'unchecked'; var columnChanged = false; var toggleRelatedFields; if (!wasVisible && isNowVisible) { // field was not visible, but now is visible fieldViewDef = { name: fieldVarDef.name, type: fieldVarDef.type, label: fieldVarDef.vname || fieldVarDef.label }; fieldViewDef.labelModule = this._getFieldLabelModule(field); // add the column to header fields this.listHeaderView.addColumnHeaderField(fieldViewDef); toggleRelatedFields = true; columnChanged = true; } else if (wasVisible && !isNowVisible) { // field was visible, but now is not visible, so remove from columns // remove the column from header fields this.listHeaderView.removeColumnHeaderField(fieldVarDef); toggleRelatedFields = false; columnChanged = true; } else if (!wasVisible && !isNowVisible && isUnchecked) { columnChanged = true; toggleRelatedFields = false; } if (columnChanged) { if (!_.isUndefined(toggleRelatedFields) && field.def.relatedFields) { _.each(field.def.relatedFields, function(fieldName) { this.context.trigger( 'config:' + this.eventViewName + ':' + fieldName + ':related:toggle', field, toggleRelatedFields ); }, this); } } }, /** * @inheritdoc */ render: function() { this._super('render'); this.listHeaderView = app.view.createView({ context: this.context, eventViewName: this.eventViewName, type: 'config-list-header-columns', layout: this, model: this.model }); this.$('.quote-summary-data-list-table').append(this.listHeaderView.el); // set the column header fields and render this.listHeaderView.setColumnHeaderFields(this.listHeaderFields); }, /** * @inheritdoc */ _customFieldDef: function(def) { def.eventViewName = this.eventViewName; return def; }, /** * Handles the click event when user clicks to Restore Default fields * @param evt */ onClickRestoreDefaultsBtn: function(evt) { var fieldList = _.pluck(this.defaultFields, 'name'); this.listHeaderView.setColumnHeaderFields(this.defaultFields); this.context.trigger('config:fields:' + this.eventViewName + ':reset', fieldList); } }) }, "quote-data-list-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.QuoteDataListHeaderView * @alias SUGAR.App.view.views.BaseQuotesQuoteDataListHeaderView * @extends View.Views.Base.View */ ({ // Quote-data-list-header View (base) /** * @inheritdoc */ events: { 'click [name="group_button"]': '_onCreateGroupBtnClicked', 'click [name="massdelete_button"]': '_onDeleteBtnClicked', 'click [data-check=all]': 'checkAll' }, /** * @inheritdoc */ plugins: [ 'MassCollection', 'QuotesLineNumHelper' ], /** * @inheritdoc */ tagName: 'thead', /** * @inheritdoc */ className: 'quote-data-list-header', /** * Array of left column fields */ leftColumns: undefined, /** * Array of fields to use in the template */ _fields: undefined, /** * If this view is currently in the /create view or not */ isCreateView: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.leftColumns = []; var qliListMetadata = app.metadata.getView('Products', 'quote-data-group-list'); if (qliListMetadata && qliListMetadata.panels) { this.meta.panels = qliListMetadata.panels; } _.each(this.meta.panels, function(panel) { _.each(panel.fields, function(field) { if (!field.labelModule) { field.labelModule = 'Quotes'; } }, this); }, this); this.isCreateView = this.context.get('create') || false; if (this.layout.isCreateView) { this.leftColumns.push({ 'type': 'fieldset', 'fields': [], 'value': false, 'sortable': false }); } else { this.addMultiSelectionAction(); } this._fields = _.flatten(_.pluck(this.meta.panels, 'fields')); }, /** * @inheritdoc */ bindDataChange: function() { var bundles; this._super('bindDataChange'); if (!this.isCreateView) { bundles = this.model.get('bundles'); if (bundles) { bundles.on('change', this._checkMassActions, this); } } // massCollection has the Quote record as its only model, // reset this during initialization so it's empty if (this.massCollection) { this.massCollection.on('add remove reset', this._massCollectionChange, this); } }, /** * Called when items are added or removed from the massCollection. Handles checking or * unchecking the CheckAll checkbox as well as calls _checkMassActions to set button states * * @param {Data.Bean} model The model that was added or removed * @param {Data.MixedBeanCollection} massCollection The mass collection on the context * @private */ _massCollectionChange: function(model, massCollection) { var $checkAllField = this.$('[data-check=all]'); if (massCollection.length === 0 && $checkAllField.length) { // uncheck the check-all box if there are no more items $checkAllField.prop('checked', false); } // check to see if we need mass actions available as well _.delay(_.bind(this._checkMassActions, this), 25); }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.massCollection) { // remove any Quotes models from the massCollectio this.massCollection.models = _.filter(this.massCollection.models, function(model) { return model.module !== 'Quotes'; }); } this._checkMassActions(); }, /** * Handles checking and unchecking all items in the quote data list * * @param {jQuery.Event} event The click event from the input checkbox */ checkAll: function(event) { var $checkbox = $(event.currentTarget); if ($(event.target).hasClass('checkall') || event.type === 'keydown') { $checkbox.prop('checked', !$checkbox.is(':checked')); } if ($checkbox.is(':checked')) { this.context.trigger('quotes:collections:all:checked'); } else { this.context.trigger('quotes:collections:not:all:checked'); } }, /** * Checks if bundles are empty and sets mass actions disabled if empty * * @private */ _checkMassActions: function() { var massActionsField; var groupBtn; var massDeleteBtn; var disableMassActions; var quoteModel; if (this.disposed) { return; } massActionsField = this.getField('quote-data-mass-actions'); groupBtn = this.getField('group_button'); massDeleteBtn = this.getField('massdelete_button'); disableMassActions = false; quoteModel = _.find(this.massCollection.models, function(model) { return model.get('_module') === 'Quotes'; }); if (quoteModel) { // get rid of any Quotes models from the mass collection this.massCollection.remove(quoteModel, {silent: true}); } if (this._bundlesAreEmpty()) { if (massActionsField) { massActionsField.setDisabled(true); } } else { // qlis exist if (massActionsField) { massActionsField.setDisabled(false); } disableMassActions = this.massCollection.models.length === 0; if (groupBtn) { groupBtn.setDisabled(disableMassActions); } if (massDeleteBtn) { massDeleteBtn.setDisabled(disableMassActions); } } }, /** * Returns if the bundles are empty or not * * @return {boolean} True if bundles are empty, false if any bundle contains an item * @private */ _bundlesAreEmpty: function() { var bundlesHaveItems = false; var bundles = this.model.get('bundles'); if (bundles) { bundlesHaveItems = bundles.every(function(bundle) { return bundle.get('product_bundle_items').length === 0; }); } return bundlesHaveItems; }, /** * Adds the left column fields */ addMultiSelectionAction: function() { var _generateMeta = function(buttons, disableSelectAllAlert) { return { name: 'quote-data-mass-actions', type: 'fieldset', fields: [ { type: 'quote-data-actionmenu', buttons: buttons || [], disable_select_all_alert: !!disableSelectAllAlert } ], value: false, sortable: false }; }; var buttons = this.meta.selection.actions; var disableSelectAllAlert = !!this.meta.selection.disable_select_all_alert; this.leftColumns.push(_generateMeta(buttons, disableSelectAllAlert)); }, /** * Handles when the create Group button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateGroupBtnClicked: function(evt) { if (this.massCollection.length) { this.context.on('quotes:group:create:success', this._onNewGroupedItemsCreateSuccess, this); this.context.trigger('quotes:group:create'); this.context.set('_justGroupedItems', true); } else { app.alert.show('quote_grouping_message', { level: 'error', title: '', messages: [ app.lang.get('LBL_GROUP_NOTHING_SELECTED', this.module) ] }); } }, /** * Called when the group in which any selected items are to be grouped has * successfully been saved. Clears app alerts and removes the context listener * for the create success event * * @param {Object} newGroupData The new ProductBundle to add selected items into * @private */ _onNewGroupedItemsCreateSuccess: function(newGroupData) { this.context.off('quotes:group:create:success', this._onNewGroupedItemsCreateSuccess); this.layout.moveMassCollectionItemsToNewGroup(newGroupData); }, /** * Handles when the Delete button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onDeleteBtnClicked: function(evt) { var deleteConfirmMsg = 'LBL_ALERT_CONFIRM_DELETE'; if (this.massCollection.length) { if (this.massCollection.length > 1) { deleteConfirmMsg += '_PLURAL'; } app.alert.show('confirm_delete', { level: 'confirmation', title: app.lang.get('LBL_ALERT_TITLE_WARNING') + ':', messages: [app.lang.get(deleteConfirmMsg, '')], onConfirm: _.bind(function() { app.alert.show('deleting_line_item', { level: 'info', messages: [app.lang.get('LBL_ALERT_DELETING_ITEM', 'ProductBundles')] }); this.context.trigger('quotes:selected:delete', this.massCollection); }, this) }); } else { app.alert.show('quote_grouping_message', { level: 'error', title: '', messages: [ app.lang.get('LBL_DELETE_NOTHING_SELECTED', this.module) ] }); } }, /** * @inheritdoc */ _dispose: function() { var bundles; if (!this.isCreateView) { bundles = this.model.get('bundles'); bundles.off('change', null, this); } // in case something weird happens where this view gets // disposed between adding the listener and removing, // go ahead and remove it on dispose if it exists this.context.off('quotes:group:create:success', null, this); if (this.massCollection) { this.massCollection.off('add remove reset', null, this); } this._super('_dispose'); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.CreateView * @alias SUGAR.App.view.views.BaseQuotesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Holds the ProductBundles/Products/ProductBundleNotes fields meta for different views */ moduleFieldsMeta: undefined, /** * Field map for where Opp/RLI fields (values) should map to Quote fields (keys) */ convertToQuoteFieldMap: { Opportunities: { opportunity_id: 'id', opportunity_name: 'name', renewal: 'renewal' }, RevenueLineItems: { name: 'name', opportunity_id: 'opportunity_id', opportunity_name: 'opportunity_name' }, defaultBilling: { billing_account_id: 'account_id', billing_account_name: 'account_name' }, defaultShipping: { shipping_account_id: 'account_id', shipping_account_name: 'account_name' } }, /** * A list of billing field names to pull from the Account model to the Quote model */ acctBillingToQuoteConvertFields: [ 'billing_address_city', 'billing_address_country', 'billing_address_postalcode', 'billing_address_state', 'billing_address_street' ], /** * A list of shiping field names to pull from the Account model to the Quote model */ acctShippingToQuoteConvertFields: [ 'shipping_address_city', 'shipping_address_country', 'shipping_address_postalcode', 'shipping_address_state', 'shipping_address_street' ], /** * If this Create view is from converting items from other modules to Quotes, is this * converting from a 'shipping' or 'billing' subpanel, or undefined if neither. */ isConvertFromShippingOrBilling: undefined, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['QuotesViewSaveHelper', 'LinkedModel']); var fromSubpanel = options.context.get('fromSubpanel'); this._super('initialize', [options]); if (options.context.get('convert') && !fromSubpanel) { this._prepopulateQuote(options); } else if (fromSubpanel) { options.context.get('model').link = options.context.get('subpanelLink'); } this.moduleFieldsMeta = {}; this._buildMeta('ProductBundleNotes', 'quote-data-group-list'); this._buildMeta('ProductBundles', 'quote-data-group-header'); this._buildMeta('Products', 'quote-data-group-list'); // gets the name of any field where calculated is true this.calculatedFields = _.chain(this.model.fields) .where({calculated: true}) .pluck('name') .value(); // Set the bundles as a separate model validation task so the Quote Record can validate by itself // then it calls the bundles validation this.model.addValidationTask('quote_bundles_' + this.cid, _.bind(this.validateBundleModels, this)); }, /** * Prepopulates the Quote context model with related module fields * * @param {Object} options The initialize options Object * @protected */ _prepopulateQuote: function(options) { var parentModel = options.context.get('parentModel'); var ctxModel = options.context.get('model'); var parentModule = parentModel.module; var parentModelAcctIdFieldName = parentModule === 'Accounts' ? 'id' : 'account_id'; var linkModel; var quoteData = {}; var fieldMap; this.isConvertFromShippingOrBilling = undefined; if (ctxModel && parentModel) { linkModel = this.createLinkModel(parentModel, options.context.get('fromLink')); // get the JSON attributes of the linked model quoteData = linkModel.toJSON(); // create a field map from the default fields and module-specific fields fieldMap = _.extend({}, this.convertToQuoteFieldMap[parentModule]); if (quoteData.shipping_account_id || quoteData.shipping_contact_id) { // if the linked model had any shipping_ fields, set it to 'shipping' this.isConvertFromShippingOrBilling = 'shipping'; quoteData.copy = false; } else if (quoteData.billing_account_id || quoteData.billing_contact_id) { // if the linked model had any billing_ fields, set it to 'billing' this.isConvertFromShippingOrBilling = 'billing'; } if (parentModule !== 'Accounts') { // since its not from an Acct shipping/billing link, add in the default Acct field mappings if (this.isConvertFromShippingOrBilling === 'shipping') { fieldMap = _.extend(fieldMap, this.convertToQuoteFieldMap.defaultShipping); } else if (this.isConvertFromShippingOrBilling === 'billing') { fieldMap = _.extend(fieldMap, this.convertToQuoteFieldMap.defaultBilling); } else { fieldMap = _.extend( fieldMap, this.convertToQuoteFieldMap.defaultShipping, this.convertToQuoteFieldMap.defaultBilling ); } } // copy field data from the parentModel to the quoteData object _.each(fieldMap, function(otherModuleField, quoteField) { quoteData[quoteField] = parentModel.get(otherModuleField); }, this); // make an api call to get related Account data app.api.call('read', app.api.buildURL('Accounts/' + parentModel.get(parentModelAcctIdFieldName)), null, { success: _.bind(this._setAccountInfo, this) }); // make an api call to get related Opportunity data if (parentModule === 'RevenueLineItems') { var oppId = parentModel.get(fieldMap.opportunity_id); app.api.call('read', app.api.buildURL('Opportunities/' + oppId), null, { success: _.bind(function(oppData) { this.model.set('renewal', oppData.renewal); }, this), error: function(data) { app.error.handleHttpError(data, parentModel); } }); } // set new quoteData attributes onto the create model ctxModel.set(quoteData); } }, /** * Sets the related Account info on the Quote bean * * @param {Object} accountInfoData The Account info returned from the Accounts/:id endpoint * @protected */ _setAccountInfo: function(accountInfoData) { var acctData = {}; var fields = []; if (this.isConvertFromShippingOrBilling === 'shipping') { // if this is a shipping conversion, set the Account shipping fields fields = this.acctShippingToQuoteConvertFields; } else if (this.isConvertFromShippingOrBilling === 'billing') { // if this is a billing conversion, set the Account billing fields fields = this.acctBillingToQuoteConvertFields; } else { // if this is neither a shipping nor billing conversion, // set both Account shipping & billing fields fields = fields.concat( this.acctBillingToQuoteConvertFields, this.acctShippingToQuoteConvertFields ); } _.each(fields, function(fieldName) { acctData[fieldName] = accountInfoData[fieldName]; }, this); this.model.set(acctData); }, /** * Builds the `this.moduleFieldsMeta` object * * @param {string} moduleName The module name to get meta for * @param {string} viewName The view name from the module to get view defs for * @private */ _buildMeta: function(moduleName, viewName) { var viewMeta; var modMeta; var metaFields = {}; var modMetaField; modMeta = app.metadata.getModule(moduleName); viewMeta = app.metadata.getView(moduleName, viewName); if (modMeta && viewMeta) { _.each(viewMeta.panels, function(panel) { _.each(panel.fields, function(field) { modMetaField = modMeta.fields[field.name]; metaFields[field.name] = _.extend({}, modMetaField, field); }, this); }, this); this.moduleFieldsMeta[moduleName] = metaFields; } }, /** * Validates the models in the Quote's ProductBundles * * @param {Object} fields The list of fields to validate. * @param {Object} recordErrors The errors object during this validation task. * @param {Function} callback The callback function to continue validation. */ validateBundleModels: function(fields, recordErrors, callback) { var returnCt = 0; var totalItemsToValidate = 0; var bundles = this.model.get('bundles'); var productBundleItems; var pbModelsAsyncCt = 0; recordErrors = recordErrors || {}; if (bundles && bundles.length) { //Check to see if we have only the default group if (bundles.length === 1) { productBundleItems = bundles.models[0].get('product_bundle_items'); //check to see if that group is empty, if so, return the valid status of the parent. if (productBundleItems.length === 0) { callback(null, fields, recordErrors); return; } } totalItemsToValidate += bundles.length; // get the count of items totalItemsToValidate = _.reduce(bundles.models, function(memo, bundle) { return memo + bundle.get('product_bundle_items').length; }, totalItemsToValidate); // loop through each ProductBundles bean _.each(bundles.models, function(bundleModel) { // call validate on the ProductBundle model (if group name were required or some other field) bundleModel.isValidAsync(this.moduleFieldsMeta[bundleModel.module], _.bind(function(isValid, errors) { // increment the validate count returnCt++; // get the bundle items for this bundle to validate later productBundleItems = bundleModel.get('product_bundle_items'); // add any errors returned to the main record errors recordErrors = _.extend(recordErrors, errors); if (!isValid) { // if the bundleModel has bad fields, // trigger the error on the bundle model bundleModel.trigger('error:validation'); } // add any product bundle items to the async count pbModelsAsyncCt += productBundleItems.length; if (productBundleItems.length === 0) { // only try to use the callback here if this bundle is empty and // there are no other bundle items async waiting to validate if (pbModelsAsyncCt === 0 && returnCt === totalItemsToValidate) { // if we've validated the correct number of models, call the callback fn callback(null, fields, recordErrors); } } // loop through each product_bundle_items Products/ProductBundleNotes bean _.each(productBundleItems.models, function(pbModel) { // call validate on the Product/ProductBundleNote model pbModel.isValidAsync(this.moduleFieldsMeta[pbModel.module], _.bind(function(isValid, errors) { // increment the validate count returnCt++; pbModelsAsyncCt--; // add any errors returned to the main record errors recordErrors = _.extend(recordErrors, errors); if (!isValid) { // if the qli/pbn has bad fields, // trigger the error on the bundle model pbModel.trigger('error:validation'); } // trigger validation complete and process the errors for this model pbModel.trigger('validation:complete', pbModel._processValidationErrors(errors)); if (errors.description) { // if this is a ProductBundleNotes model where "description" field is required // we have already triggered to process validation errors on the PBN model to show // description is required, now we need to delete it off the error object // so that the Quote record "description" field doesn't show as required since // they have the same field name. So if errors.description (specifically checking // if this model validation threw the error) then remove it off the recordErrors // object that we're passing back delete recordErrors.description; } if (returnCt === totalItemsToValidate) { // if we've validated the correct number of models, call the callback fn callback(null, fields, recordErrors); } }, this)); }, this); bundleModel.trigger('validation:complete', bundleModel._processValidationErrors(errors)); }, this)); }, this); } else { // if there are no bundles to validate then just return callback(null, fields, recordErrors); } }, /** * @inheritdoc */ hasUnsavedChanges: function() { return this.hasUnsavedQuoteChanges(); }, /** * @inheritdoc */ getCustomSaveOptions: function(options) { var parentSuccessCallback; var config = app.metadata.getModule('Opportunities', 'config'); var bundles = this.model.get('bundles'); var isConvert = this.context.get('convert'); var hasItems = 0; var userId = this.model.get('assigned_user_id'); var accountId = this.model.get('billing_account_id') || null; _.each(bundles.models, function(bundle) { var pbItems = bundle.get('product_bundle_items'); _.each(pbItems.models, function(itemModel) { itemModel.set({ account_id: accountId, assigned_user_id: userId, }); if (isConvert && itemModel.module === 'Products' && itemModel.get('revenuelineitem_id')) { hasItems++; } }, this); bundle.set({ product_bundle_items: pbItems, assigned_user_id: userId }); }, this); this.model.set('assigned_user_id', userId); if (config && config.opps_view_by === 'RevenueLineItems' && isConvert && hasItems) { parentSuccessCallback = options.success; options.success = _.bind(this._customQuotesCreateSave, this, parentSuccessCallback); } return options; }, /** * Checks all Products in bundles to make sure each Product has quote_id set * then calls the main success function that was passed in from base Create view * * @private */ _customQuotesCreateSave: function(parentSuccessCallback, model) { var quoteId = model.get('id'); var bundles = model.get('bundles'); var rliId; var pbItems; var bulkRequest; var bulkUrl; var bulkCalls = []; _.each(bundles.models, function(pbModel) { pbItems = pbModel.get('product_bundle_items'); _.each(pbItems.models, function(itemModel) { if (itemModel.module === 'Products') { rliId = itemModel.get('revenuelineitem_id'); if (rliId) { bulkUrl = app.api.buildURL('RevenueLineItems/' + rliId + '/link/quotes/' + quoteId); bulkRequest = { url: bulkUrl.substr(4), method: 'POST', data: { id: rliId, link: 'quotes', relatedId: quoteId, related: { quote_id: quoteId } } }; bulkCalls.push(bulkRequest); } } }, this); }, this); if (bulkCalls.length) { app.api.call('create', app.api.buildURL(null, 'bulk'), { requests: bulkCalls }, { success: parentSuccessCallback }); } } }) }, "config-drawer-howto": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ConfigDrawerHowtoView * @alias SUGAR.App.view.views.BaseQuotesConfigDrawerHowtoView * @extends View.Views.Base.BaseConfigDrawerHowtoView */ ({ // Config-drawer-howto View (base) extendsFrom: 'BaseConfigDrawerHowtoView', /** * @inheritdoc */ events: { 'keyup .searchbox': 'onSearchFilterChanged' }, /** * List of field defs for the left column of the howto area */ fieldsListLeft: undefined, /** * List of field defs for the right column of the howto area */ fieldsListRight: undefined, /** * Contains all fields hidden by search */ hiddenFields: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.fieldsListLeft = []; this.fieldsListRight = []; this.hiddenFields = []; }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.on('config:fields:change', this.onFieldsChange, this); }, /** * Handles when the list of fields changes for the howto panel * * @param {string} eventName The name of the event * @param {Array} fieldsList The list of fields to add to the view */ onFieldsChange: function(eventName, fieldsList) { var len = fieldsList.length; var listRightIndex = len >> 1; var listLeftIndex = len - listRightIndex; this.hiddenFields = []; this.fieldsListLeft = _.initial(fieldsList, listLeftIndex); this.fieldsListRight = _.rest(fieldsList, listRightIndex); this.render(); }, /** * Handles when search term is changed, hides and shows fields */ onSearchFilterChanged: _.debounce(function(evt) { var searchTerm = $(evt.currentTarget).val(); var lowerName; var lowerLabel; if (searchTerm) { searchTerm = searchTerm.toLowerCase(); } // re-show all fields _.each(this.hiddenFields, function(field) { field.show(); }, this); // reset hidden fields this.hiddenFields = []; _.each(this.fields, function(field) { if (field.name) { lowerName = field.name.toLowerCase(); } if (field.label) { lowerLabel = field.label.toLowerCase(); } if ((lowerName && lowerName.indexOf(searchTerm) === -1) && (lowerLabel && lowerLabel.indexOf(searchTerm) === -1)) { // the field name AND label DO NOT CONTAIN the search term, // hide the field field.hide(); this.hiddenFields.push(field); } }, this); }, 400), /** * @inheritdoc */ render: function() { this._super('render'); // set the indeterminate checkbox input this.$('.indeterminate').prop('indeterminate', true); }, /** * @inheritdoc */ _dispose: function() { // get rid of any field references this.fieldsListLeft = []; this.fieldsListRight = []; this.hiddenFields = []; this._super('_dispose'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "panel-top": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Panel-top View (base) extendsFrom: 'PanelTopView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['MassQuote']); this._super('initialize', [options]); }, /** * Overriding to create a Quote from a Subpanel using the Quotes create view not a drawer * * @inheritdoc */ createRelatedClicked: function(event) { var massCollection = this.context.get('mass_collection'); var module = this.context.parent.get('module'); if (!massCollection) { massCollection = this.context.get('collection').clone(); if (!_.contains(['Accounts', 'Opportunities', 'Contacts'], module)) { massCollection.fromSubpanel = true; } this.context.set('mass_collection', massCollection); } this.layout.trigger('list:massquote:fire'); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.PreviewView * @alias SUGAR.App.view.views.BaseQuotesPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['QuotesConversionRateLocking']); this._super('initialize', [options]); }, /** * @inheritdoc */ handleSave: function() { this.checkConversionRateLock(() => { this._super('handleSave'); }); } }) }, "product-catalog": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.ProductCatalogView * @alias SUGAR.App.view.views.QuotesProductCatalogView * @extends View.Views.Base.ProductCatalogView * @deprecated Use {@link View.Views.Base.ProductCatalogView} instead */ ({ // Product-catalog View (base) extendsFrom: 'ProductCatalogView', initialize: function(options) { app.logger.warn('View.Views.Base.Quotes.ProductCatalogView is deprecated. Use ' + 'View.Views.Base.ProductCatalogView instead'); this._super('initialize', [options]); } }) }, "quote-data-grand-totals-footer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Quotes.QuoteDataGrandTotalsFooterView * @alias SUGAR.App.view.views.BaseQuotesQuoteDataGrandTotalsFooterView * @extends View.Views.Base.View */ ({ // Quote-data-grand-totals-footer View (base) /** * @inheritdoc */ className: 'quote-data-grand-totals-footer flex justify-end px-3 mb-3' }) } }} , "layouts": { "base": { "extra-info": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Quotes.ExtraInfoLayout * @alias SUGAR.App.view.layouts.BaseQuotesExtraInfoLayout * @extends View.Views.Base.Layout */ ({ // Extra-info Layout (base) /** * @inheritdoc */ className: 'quote-data-container' }) }, "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Quotes.ConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseQuotesConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) /** * @inheritdoc */ extendsFrom: 'BaseConfigDrawerLayout', /** * Checks Quotes ACLs to see if the User is a system admin, admin, * or if the user has a developer role for the Quotes module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().Quotes; var isSysAdmin = (app.user.get('type') === 'admin'); var isAdmin = !_.has(acls, 'admin'); var isDev = !_.has(acls, 'developer'); return (isSysAdmin || isAdmin || isDev); }, /** * Checks if there's actually config in the metadata for the current module * todo: remove this function once config data is actually in the application. * * @return {boolean} * @private */ _checkConfigMetadata: function() { //todo: remove this function once config data is actually in the application. return true; }, /** * @inheritdoc */ loadData: function() { if (this._checkModuleAccess()) { app.api.call( 'read', app.api.buildURL('Quotes', 'config'), null, { success: _.bind(this.onConfigSuccess, this) } ); } }, /** * Success handler for when loadData returns * * @param {Object} data The server response */ onConfigSuccess: function(data) { this.context.set(data); } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Quotes.ConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseQuotesConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) /** * @inheritdoc */ extendsFrom: 'BaseConfigDrawerContentLayout', /** * @inheritdoc */ bindDataChange: function() { this.on('config:panel:fields:loaded', this.onConfigPanelFieldsLoad, this); }, /** * Handles when the fields for a config panel are loaded. If the panel is * the current/active one, set its fields as the current config fields * @param configPanel the panel view containing the loaded fields */ onConfigPanelFieldsLoad: function(configPanel) { if (configPanel.name === this.selectedPanel) { this.context.trigger('config:fields:change', configPanel.eventViewName, configPanel.panelFields); } }, /** * @inheritdoc */ _switchHowToData: function(helpId) { switch (helpId) { case 'config-columns': case 'config-summary': case 'config-footer': this.currentHowToData.title = app.lang.get('LBL_CONFIG_FIELD_SELECTOR', this.module, { moduleName: app.lang.get('LBL_MODULE_NAME', this.module) }); this.currentHowToData.text = ''; break; } } }) }, "quote-data-list-groups": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Quotes.QuoteDataListGroupsLayout * @alias SUGAR.App.view.layouts.BaseQuotesQuoteDataListGroupsLayout * @extends View.Views.Base.Layout */ ({ // Quote-data-list-groups Layout (base) /** * @inheritdoc */ tagName: 'table', /** * @inheritdoc */ className: 'table dataTable quote-data-list-table', /** * Array of records from the Quote data */ records: undefined, /** * An Array of ProductBundle IDs currently in the Quote */ groupIds: undefined, /** * Holds the layout metadata for ProductBundlesQuoteDataGroupLayout */ quoteDataGroupMeta: undefined, /** * The Element tag to apply jQuery.Sortable on */ sortableTag: 'tbody', /** * The ID of the default group */ defaultGroupId: undefined, /** * If this layout is currently in the /create view or not */ isCreateView: undefined, /** * Contains any current bulk save requests being processed */ currentBulkSaveRequests: undefined, /** * Counter for how many bundles are being saved */ bundlesBeingSavedCt: undefined, /** * Array that holds any current api requests */ saveQueue: undefined, /** * If this is initializing from a Quote's "Copy" functionality */ isCopy: undefined, /** * Keeps track of the number of items to be copied during a Quote's "Copy" functionality */ copyItemCount: undefined, /** * Keeps track of the number of bundles to be copied during a Quote's "Copy" functionality */ copyBundleCount: undefined, /** * Keeps track of the copyBundle functionality */ copyBundleCompleted: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.saveQueue = []; this.groupIds = []; this.currentBulkSaveRequests = []; this.quoteDataGroupMeta = app.metadata.getLayout('ProductBundles', 'quote-data-group'); this.bundlesBeingSavedCt = 0; this.isCreateView = this.context.get('create') || false; this.isCopy = this.context.get('copy') || false; this.copyItemCount = 0; this.copyBundleCount = 0; //Setup the neccesary child context before data is populated so that child views/layouts are correctly linked var pbContext = this.context.getChildContext({link: 'product_bundles'}); pbContext.set('create', this.isCreateView); pbContext.prepare(false, true); this.before('render', this.beforeRender, this); this.on('list:scrollLock', this._scrollLock, this); }, /** * @inheritdoc */ bindDataChange: function() { var userACLs = app.user.getAcls(); this.model.on('change:show_line_nums', this._onShowLineNumsChanged, this); this.model.on('change:bundles', this._onProductBundleChange, this); this.context.on('quotes:group:create', this._onCreateQuoteGroup, this); this.context.on('quotes:group:delete', this._onDeleteQuoteGroup, this); this.context.on('quotes:selected:delete', this._onDeleteSelectedItems, this); this.context.on('quotes:defaultGroup:create', this._onCreateDefaultQuoteGroup, this); this.context.on('quotes:defaultGroup:save', this._onSaveDefaultQuoteGroup, this); if (!(_.has(userACLs.Quotes, 'edit') || _.has(userACLs.Products, 'access') || _.has(userACLs.Products, 'edit'))) { // only listen for PCDashlet if this is Quotes and user has access // to both Quotes and Products // need to trigger on app.controller.context because of contexts changing between // the PCDashlet, and Opps create being in a Drawer, or as its own standalone page // app.controller.context is the only consistent context to use var viewDetails = this.closestComponent('record') ? this.closestComponent('record') : this.closestComponent('create'); if (!_.isUndefined(viewDetails)) { app.controller.context.on(viewDetails.cid + ':productCatalogDashlet:add', this._onProductCatalogDashletAddItem, this); } } // check if this is create mode, in which case add an empty array to bundles if (this.isCreateView) { this._onProductBundleChange(this.model.get('bundles')); if (this.isCopy) { this.copyItemCount = this.context.get('copyItemCount'); if (this.copyItemCount) { this.toggleCopyAlert(true); } // set this function to happen async after the alert has been displayed _.delay(_.bind(function() { this._setCopyQuoteData(); }, this), 250); } } else { this.model.once('sync', function(model) { var bundles = this.model.get('bundles'); this._checkProductsQuoteLink(); if (bundles.length === 0) { this._onProductBundleChange(bundles); } }, this); } }, /** * Toggles showing and hiding the "Copying QLI" alert when using the Copy functionality * * @param {boolean} showAlert True if we need to show alert, false if we need to dismiss it */ toggleCopyAlert: function(showAlert) { var alertId = 'quotes_copy_alert'; var titleLabel; if (showAlert) { titleLabel = this.copyItemCount > 8 ? 'LBL_QUOTE_COPY_ALERT_MESSAGE_LONG_TIME' : 'LBL_QUOTE_COPY_ALERT_MESSAGE'; app.alert.show(alertId, { level: 'process', closeable: false, autoClose: false, title: app.lang.get(titleLabel, 'Quotes') }); } else { app.alert.dismiss(alertId); } }, /** * Handles decrementing the total copy item count and * checks if we need to dismiss the copy alert, or * decrements the copy bundle count and checks if we need to render * * @param {boolean} bundleComplete True if we're completing a bundle */ completedCopyItem: function(bundleComplete) { this.copyItemCount--; if (this.copyItemCount === 0) { this.toggleCopyAlert(false); } if (bundleComplete) { this.copyBundleCount--; if (this.copyBundleCount === 0) { this.copyBundleCompleted = true; this.render(); } } }, /** * Handles grabbing the relatedRecords passed in from the context, creating the ProductBundle groups, * and adding items into those groups * * @private */ _setCopyQuoteData: function() { var relatedRecords = this.context.get('relatedRecords'); var defaultGroup = this._getComponentByGroupId(this.defaultGroupId); this.copyBundleCount = relatedRecords.length; // loop over the bundles _.each(relatedRecords, function(record) { // check if this record is the "default group" if (record.default_group) { _.each(record.product_bundle_items, function(pbItem) { // set the item to use the edit template for quote-data-editablelistbutton pbItem.modelView = 'edit'; // add this model to the toggledModels for edit view defaultGroup.quoteDataGroupList.toggledModels[pbItem.cid] = pbItem; // update the copy item number this.completedCopyItem(); }, this); // add the whole collection of PBItems to the list collection at once defaultGroup.quoteDataGroupList.collection.add(record.product_bundle_items); // update the existing default group this._updateDefaultGroupWithNewData(defaultGroup, record); // update the copy bundle number this.completedCopyItem(true); } else { // listen for a new group being created during the _onCreateQuoteGroup function this.context.once( 'quotes:group:create:success', _.bind(this._onCopyQuoteDataNewGroupedCreateSuccess, this, record), this ); // create a new quote group this._onCreateQuoteGroup(); } }, this); }, /** * Called during a Quote record "Copy" to set a group's record data on the model * and adds any items to the group's collection * * @param {Object} record The ProductBundle JSON data to set on the model * @param {Data.Bean} pbModel The ProductBundle Model * @private */ _onCopyQuoteDataNewGroupedCreateSuccess: function(record, pbModel) { var group = this._getComponentByGroupId(pbModel.cid); // set the group's name on the model group.model.set({ name: record.name }); // loop over each product bundle item and add it to the group rows _.each(record.product_bundle_items, function(pbItem) { // set the item to use the edit template for quote-data-editablelistbutton pbItem.modelView = 'edit'; // add this model to the toggledModels for edit view group.quoteDataGroupList.toggledModels[pbItem.cid] = pbItem; // update the copy item number this.completedCopyItem(); }, this); // add the whole collection of PBItems to the list collection at once group.quoteDataGroupList.collection.add(record.product_bundle_items); // update the copy bundle number this.completedCopyItem(true); // update the group line number counts group.trigger('quotes:line_nums:reset'); }, /** * Listens for the Product Catalog Dashlet to sent ProductTemplate data * * @param {Object} productData The ProductTemplate data to convert to a QLI * @private */ _onProductCatalogDashletAddItem: function(productData) { // Set the assigned user as the currently logged in user productData.assigned_user_id = app.user.get('id'); productData.assigned_user_name = app.user.get('full_name'); // Update price on Flexible Duration Service productData.catalog_service_duration_value = productData.service_duration_value; productData.catalog_service_duration_unit = productData.service_duration_unit; var defaultGroup = this._getComponentByGroupId(this.defaultGroupId); if (defaultGroup) { // trigger event on default group to add the product data defaultGroup.trigger('quotes:group:create:qli', 'products', productData); } // trigger event on the context to let dashlet know this is done adding the product var viewDetails = this.closestComponent('record') ? this.closestComponent('record') : this.closestComponent('create'); if (!_.isUndefined(viewDetails)) { app.controller.context.trigger(viewDetails.cid + ':productCatalogDashlet:add:complete'); } }, /** * Checks all Products in bundles to make sure each Product has quote_id and account_id set * * @private */ _checkProductsQuoteLink: function() { var quoteId = this.model.get('id'); var accountId = this.model.get('billing_account_id'); var bundles = this.model.get('bundles'); var prodId; var pbItems; var bulkRequest; var bulkUrl; var bulkCalls = []; _.each(bundles.models, function(pbModel) { pbItems = pbModel.get('product_bundle_items'); _.each(pbItems.models, function(itemModel) { prodId = itemModel.get('id'); if (itemModel.module === 'Products' && prodId) { // if the product exists but doesn't have a quote ID saved, save it if (_.isEmpty(itemModel.get('quote_id'))) { bulkUrl = app.api.buildURL('Products/' + prodId + '/link/quotes/' + quoteId); bulkRequest = { url: bulkUrl.substr(4), method: 'POST', data: { id: prodId, link: 'quotes', relatedId: quoteId, related: { quote_id: quoteId } } }; bulkCalls.push(bulkRequest); } } }, this); }, this); if (bulkCalls.length) { app.api.call('create', app.api.buildURL(null, 'bulk'), { requests: bulkCalls }, null, { success: _.bind(function(bulkResponses) { _.each(bulkResponses, function(response) { var record = response.contents.record; var relatedRecord = response.contents.related_record; if (!_.isUndefined(record) && this.model) { var bundles = this.model.get('bundles'); _.each(bundles.models, function(pbModel) { var pbItems = pbModel.get('product_bundle_items'); _.each(pbItems.models, function(itemModel) { if (itemModel.get('id') === record.id) { // update the product model this._updateModelWithRecord(itemModel, record); } }, this); }, this); } // update the quote model if (relatedRecord._module === 'Quotes') { this._updateModelWithRecord(this.model, relatedRecord); } }, this); }, this) }); } }, /** * Handles when the show_line_nums attrib changes on the Quotes model, triggers if * line numbers should be shown or not * * @param {Data.Bean} model The Quotes Bean the change happened on * @param {boolean} showLineNums If the line nums should be shown or not * @private */ _onShowLineNumsChanged: function(model, showLineNums) { this.context.trigger('quotes:show_line_nums:changed', showLineNums); }, /** * Handles the quotes:defaultGroup:create event from a separate layout context * and triggers the correct create event on the default group to add a new item * * @param {string} itemType The type of item to create: 'qli' or 'note' * @private */ _onCreateDefaultQuoteGroup: function(itemType) { //Ensure the default group exists if (!this.defaultGroupId) { this.model.get('bundles').add(this._getDefaultGroupModel()); } var linkName = itemType == 'qli' ? 'products' : 'product_bundle_notes'; var group = this._getComponentByGroupId(this.defaultGroupId); group.trigger('quotes:group:create:' + itemType, linkName); }, /** * @inheritdoc */ _render: function() { var sortableItems; var cssClasses; this._super('_render'); sortableItems = this.$(this.sortableTag); if (sortableItems.length) { _.each(sortableItems, function(sortableItem) { $(sortableItem).sortable({ // allow draggable items to be connected with other tbody elements connectWith: 'tbody', // allow drag to only go in Y axis direction axis: 'y', // the items to make sortable items: 'tr.sortable', // adds a slow animation when "dropping" a group, removing this causes the row // to immediately snap into place wherever it's sorted revert: true, // the CSS class to apply to the placeholder underneath the helper clone the user is dragging placeholder: 'ui-state-highlight', // handler for when dragging starts start: _.bind(this._onDragStart, this), // handler for when dragging stops; the "drop" event stop: _.bind(this._onDragStop, this), // handler for when dragging an item into a group over: _.bind(this._onGroupDragTriggerOver, this), // handler for when dragging an item out of a group out: _.bind(this._onGroupDragTriggerOut, this), // the cursor to use when dragging cursor: 'move', // Don't allow dragging to start from clicking in the actions menu cancel: '.not-sortable, .dropdown-toggle, .dropdown-menu' }); }, this); } //wrap in container div for scrolling if (!this.$el.parent().hasClass('flex-list-view-content')) { cssClasses = 'flex-list-view-content'; if (this.isCreateView) { cssClasses += ' create-view'; } this.$el.wrap( '<div class="' + cssClasses + '"></div>' ); this.$el.parent().wrap( '<div class="flex-list-view left-actions quote-data-table-scrollable"></div>' ); } }, /** * Event handler for the sortstart "drag" event * * @param {jQuery.Event} evt The jQuery sortstart event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onDragStart: function(evt, ui) { // clear the current displayed tooltip app.tooltip.clear(); // disable any future tooltips from appearing until drag stop has occurred app.tooltip._disable(); }, /** * Event handler for the sortstop "drop" event * * @param {jQuery.Event} evt The jQuery sortstop event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onDragStop: function(evt, ui) { var $item = $(ui.item.get(0)); var oldGroupId = $item.data('group-id'); var newGroupId = $($item.parent()).data('group-id'); // check if the row is in edit mode var isRowInEdit = $item.hasClass('tr-inline-edit'); var triggerOldGroup = false; var oldGroup; var newGroup; var rowId; var saveDefaultGroup; var existingRows; var newPosition; // get the new group (may be the same group) newGroup = this._getComponentByGroupId(newGroupId); // make sure item was dropped in a different group than it started in if (oldGroupId !== newGroupId) { // since the groups are different, also trigger events for the old group triggerOldGroup = true; // get the row id from the name="Products_modelID" attrib rowId = $item.attr('name').split('_')[1]; // get if we need to save the new default group list or not saveDefaultGroup = newGroup.model.isNew() || false; // get the old and new quote-data-group components oldGroup = this._getComponentByGroupId(oldGroupId); existingRows = newGroup.$('tr.quote-data-group-list:not(:hidden):not(.empty-row)'); newPosition = _.findIndex(existingRows, function(item) { return ($(item).attr('name') == $item.attr('name')); }); this._moveItemToNewGroup(oldGroupId, newGroupId, rowId, isRowInEdit, newPosition, true, true); } else { // get the requests from updated rows this.currentBulkSaveRequests = this.currentBulkSaveRequests.concat(this._updateRowPositions(newGroup)); } // only make the bulk call if there are actual requests, if user drags row // but puts it in same place there should be no updates if (!this.isCreateView && !_.isEmpty(this.currentBulkSaveRequests)) { if (triggerOldGroup) { // trigger group changed for old group to check themselves oldGroup.trigger('quotes:group:changed'); // trigger save start for the old group oldGroup.trigger('quotes:group:save:start'); // trigger the group to reset it's line numbers oldGroup.trigger('quotes:line_nums:reset', oldGroup.groupId, oldGroup.collection); } // trigger group changed for new group to check themselves newGroup.trigger('quotes:group:changed'); // trigger save start for the new group newGroup.trigger('quotes:group:save:start'); // trigger the group to reset it's line numbers newGroup.trigger('quotes:line_nums:reset', newGroup.groupId, newGroup.collection); if (saveDefaultGroup) { this._saveDefaultGroupThenCallBulk(oldGroup, newGroup, this.currentBulkSaveRequests); } else { this._callBulkRequests(_.bind(this._onSaveUpdatedGroupSuccess, this, oldGroup, newGroup)); } } // re-enable tooltips in the app app.tooltip._enable(); // Fix for cancel/save buttons disappearing from jQuery animations $item.find('.action-button-wrapper').removeClass('open'); }, /** * Temporarily overwrites the css from the .scroll-width class so that * row field dropdown menues aren't clipped by overflow-x property. */ _scrollLock: function(lock) { var $content = this.$el.parent('.flex-list-view-content'); if (lock) { $content.css({'overflow-y': 'visible', 'overflow-x': 'hidden'}); } else { $content.removeAttr('style'); } $content.toggleClass('scroll-locked', lock); }, /** * Moves all items from mass_collection into a new group * based on the `newGroupData` info * * @param {Object} newGroupData The new ProductBundle to * be used to move the mass_collection items into */ moveMassCollectionItemsToNewGroup: function(newGroupData) { var newGroupId = newGroupData.related_record.id; var massCollection = this.context.get('mass_collection'); var oldGroupId; var isRowInEdit; var modelCt = {}; var updateLinkBean; var positionCt = 0; // since model.link.bean is the same exact reference to a group's model across all models // in a group, if multiple items in the same group are moved, we have to only update the // model to the new model.link.bean when it's the last model in the group being moved. If we // update a model.link.bean, it will change all other model.link.bean references in that group, // so we have to count all the models in a group, and only update the model.link.bean when it's // the last model we're updating for that group _.each(massCollection.models, function(model) { oldGroupId = model.link.bean.id; if (modelCt[oldGroupId]) { modelCt[oldGroupId]++; } else { modelCt[oldGroupId] = 1; } }, this); _.each(massCollection.models, function(model) { // get the old Group ID from the model link oldGroupId = model.link.bean.id; // get if the row was in Edit mode if modelView exists and is set to 'edit' isRowInEdit = model.modelView && model.modelView === 'edit' || false; // set selected to false since this model will no longer be in the mass collection model.selected = false; // decrement the model count for this group modelCt[oldGroupId]--; // updateLinkBean should only be true when this is the last model in the group (modelCt === 0) updateLinkBean = modelCt[oldGroupId] === 0; model.set('position', positionCt++); this._moveItemToNewGroup(oldGroupId, newGroupId, model.cid, isRowInEdit, undefined, updateLinkBean, false); }, this); // the items have all been moved on the frontend now call the BulkAPI // to flush out this.currentBulkSaveRequests to update the server this._callBulkRequests(_.bind(this._onSaveUpdatedMassCollectionItemsSuccess, this)); if (massCollection) { massCollection.reset(); } }, /** * Handles the success call from moving MassCollection items to a new group * * @param {Object} bulkResponses Response data from the bulk requests * @private */ _onSaveUpdatedMassCollectionItemsSuccess: function(bulkResponses) { _.each(bulkResponses, function(data) { var record = data.contents.record; var relatedRecord = data.contents.related_record; var newGroup; var model; // if data.contents.record was empty but contents has an id (old group GET request) if (_.isUndefined(record) && (data.contents.id && data.contents.hasOwnProperty('date_modified'))) { // this is a GET request record = data.contents; } newGroup = this._getComponentByGroupId(record.id); if (newGroup) { // check if record is the one on this collection if (newGroup.model && record && newGroup.model.get('id') === record.id) { this._updateModelWithRecord(newGroup.model, record); } if (relatedRecord) { // check if the related_record is in the newGroup model = newGroup.collection.get(relatedRecord.id); if (model) { this._updateModelWithRecord(model, relatedRecord); } } } }, this); _.each(this._components, function(comp) { if (comp.type === 'quote-data-group') { _.each(comp._components, function(subComp) { if (subComp.type === 'quote-data-group-list') { // re-initialize the SugarLogic Context on the QuoteDataGroupList subComp.slContext.initialize( subComp._getSugarLogicDependenciesForModel(subComp.model) ); } }, this); } }, this); }, /** * Updates the syncedAttributes and attributes of a `model` with the `record` data * * @param {Data.Bean} model The model to be updated * @param {Object} record The data to set on the model * @private */ _updateModelWithRecord: function(model, record) { if (model) { // remove any empty product_Bundle_items data if (record.hasOwnProperty('product_bundle_items') && _.isEmpty(record.product_bundle_items)) { delete record.product_bundle_items; } model.set(record); model.setSyncedAttributes(model.attributes); } }, /** * Moves an item with `itemId` from `oldGroupId` to the `newGroupId` ProductBundle * * @param {string} oldGroupId The ID of the old ProductBundle to move the item from * @param {string} newGroupId The ID of the new ProductBundle to move the item to * @param {string} itemId The ID of the item to move * @param {boolean} isRowInEdit If the row to move is in edit mode or not * @param {number|undefined} [newPos] The new position to place the item in * @param {boolean} updateLinkBean If we should update the model's link bean or not * @param {boolean} updatePos If we should update the model's position or not * @private */ _moveItemToNewGroup: function(oldGroupId, newGroupId, itemId, isRowInEdit, newPos, updateLinkBean, updatePos) { var oldGroup = this._getComponentByGroupId(oldGroupId); var newGroup = this._getComponentByGroupId(newGroupId); var rowModel = oldGroup.collection.get(itemId); var url; var linkName; var bulkMoveRequest; var oldGroupModelId = oldGroup.model.id; var newGroupModelId = newGroup.model.id; var itemModelId = rowModel.id; // if newPos is not passed in, make it the newGroup collection length newPos = _.isUndefined(newPos) ? newGroup.collection.length : newPos; // set the new position, so it's only set when the item is saved via the relationship change // and not again for the position update rowModel.set('position', newPos); // remove the rowModel from the old group oldGroup.removeRowModel(rowModel, isRowInEdit); // add rowModel to the new group newGroup.addRowModel(rowModel, isRowInEdit); if (updateLinkBean) { // update the link on all the models in the new group collection to be the newGroup's model _.each(newGroup.collection.models, function(newGroupCollectionModel) { newGroupCollectionModel.link = { bean: newGroup.model, isNew: newGroupCollectionModel.link.isNew, name: newGroupCollectionModel.link.name }; }, this); } if (updatePos) { // get the requests from updated rows for old and new group this.currentBulkSaveRequests = this.currentBulkSaveRequests.concat(this._updateRowPositions(oldGroup)); this.currentBulkSaveRequests = this.currentBulkSaveRequests.concat(this._updateRowPositions(newGroup)); } // move the item to the new group linkName = rowModel.module === 'Products' ? 'products' : 'product_bundle_notes'; url = app.api.buildURL('ProductBundles/' + newGroupModelId + '/link/' + linkName + '/' + itemModelId); bulkMoveRequest = { url: url.substr(4), method: 'POST', data: { id: newGroupModelId, link: linkName, relatedId: itemModelId, position: newPos } }; // add the group switching call to the newPos element of the bulk requests // so position "0" will be the 0th element in currentBulkSaveRequests this.currentBulkSaveRequests.splice(newPos, 0, bulkMoveRequest); // get the new totals after everything has happened for the old group url = app.api.buildURL('ProductBundles/' + oldGroupModelId); bulkMoveRequest = { url: url.substr(4), method: 'GET' }; this.currentBulkSaveRequests.push(bulkMoveRequest); // update the line numbers in the groups oldGroup.trigger('quotes:line_nums:reset', oldGroup.groupId, oldGroup.collection); newGroup.trigger('quotes:line_nums:reset', newGroup.groupId, newGroup.collection); }, /** * Handles saving the default quote group when a user adds a new QLI/Note to an unsaved default group * and clicks the save button from the new QLI/Note row * * @param {Function} successCallback Callback function sent from the QuoteDataEditablelistField so the field * knows when the group save is successful and the field can continue saving the new row model * @private */ _onSaveDefaultQuoteGroup: function(successCallback) { var group = this._getComponentByGroupId(this.defaultGroupId); app.alert.show('saving_default_group_alert', { level: 'success', autoClose: false, messages: app.lang.get('LBL_SAVING_DEFAULT_GROUP_ALERT_MSG', 'Quotes') }); app.api.relationships('create', 'Quotes', { 'id': this.model.get('id'), 'link': 'product_bundles', 'related': { position: 0, default_group: true } }, null, { success: _.bind(function(group, successCallback, serverData) { app.alert.dismiss('saving_default_group_alert'); this._updateDefaultGroupWithNewData(group, serverData.related_record); // call the callback to continue the save stuff successCallback(); }, this, group, successCallback) }); }, /** * Updates a group with the latest server data, updates the model, groupId, and DOM elements * * @param {View.QuoteDataGroupLayout} group The QuoteDataGroupLayout to update * @param {Object} recordData The new record data from the server * @private */ _updateDefaultGroupWithNewData: function(group, recordData) { if (this.defaultGroupId !== group.model.cid) { // remove the old default group ID from groupIds this.groupIds = _.without(this.groupIds, this.defaultGroupId); // add the new group ID so we dont add the default group twice this.groupIds.push(group.model.cid); } // update defaultGroupId with new id this.defaultGroupId = group.model.cid; // set the new data on the group model group.model.set(recordData); // update groupId with new id group.groupId = this.defaultGroupId; // update the group's dom tbody el with the correct group id group.$el.attr('data-group-id', this.defaultGroupId); group.$el.data('group-id', this.defaultGroupId); // update the tr's inside the group's dom tbody el with the correct group id group.$('tr').attr('data-group-id', this.defaultGroupId); group.$('tr').data('group-id', this.defaultGroupId); }, /** * Handles saving the default quote data group if it has not been saved yet, * then when that save success returns, it calls save on all the bulk requests * with the new proper group ID * * @param {View.QuoteDataGroupLayout} oldGroup The old QuoteDataGroupLayout * @param {View.QuoteDataGroupLayout} newGroup The new QuoteDataGroupLayout - default group that needs saving * @param {Array} bulkSaveRequests The array of bulk save requests * @private */ _saveDefaultGroupThenCallBulk: function(oldGroup, newGroup, bulkSaveRequests) { var newGroupOldId = newGroup.model.get('id'); app.alert.show('saving_default_group_alert', { level: 'success', autoClose: false, messages: app.lang.get('LBL_SAVING_DEFAULT_GROUP_ALERT_MSG', 'Quotes') }); app.api.relationships('create', 'Quotes', { 'id': this.model.get('id'), 'link': 'product_bundles', 'related': _.extend({ position: 0 }, newGroup.model.toJSON()) }, null, { success: _.bind(this._onDefaultGroupSaveSuccess, this, oldGroup, newGroup, bulkSaveRequests, newGroupOldId) }); }, /** * Called when the default group has been saved successfully and we have the new proper group id. It * updates all the bulk requests replacing the old "fake" group ID with the new proper DB-saved group ID, * updates newGroup with the new data and group ID and calls the save on the remaining bulk requests * * @param {View.QuoteDataGroupLayout} oldGroup The old QuoteDataGroupLayout * @param {View.QuoteDataGroupLayout} newGroup The new QuoteDataGroupLayout * @param {Array} bulkSaveRequests The array of bulk save requests * @param {string} newGroupOldId The previous "fake" group ID for newGroup * @param {Object} serverData The server response from saving the newGroup * @private */ _onDefaultGroupSaveSuccess: function(oldGroup, newGroup, bulkSaveRequests, newGroupOldId, serverData) { var newId = serverData.related_record.id; app.alert.dismiss('saving_default_group_alert'); // update all the bulk save requests that have the old newGroup ID with the newly saved group ID _.each(bulkSaveRequests, function(req) { req.url = req.url.replace(newGroupOldId, newId); }, this); this._updateDefaultGroupWithNewData(newGroup, serverData.related_record); // call the remaining bulk requests this._callBulkRequests(_.bind(this._onSaveUpdatedGroupSuccess, this, oldGroup, newGroup)); }, /** * Calls the bulk request endpoint with an array of requests * * @param {Function} [successCallback] The success callback function * @private */ _callBulkRequests: function(successCallback) { var successWrapper = { success: _.bind(this.handleSaveQueueSuccess, this, successCallback) }; var apiCall = app.api.call('create', app.api.buildURL(null, 'bulk'), { requests: this.currentBulkSaveRequests }, successWrapper); var saveQueueObj = { callReturned: false, customSuccess: {}, request: apiCall, responseData: {} }; this.saveQueue.push(saveQueueObj); // reset currentBulkSaveRequests this.currentBulkSaveRequests = []; }, /** * Handles all responses that are returned by the save queue * * @param {Function|undefined} customSuccess The custom success handler function that should be called next * @param {Object} responseData The response returned by the server for a call * @param {HttpRequest} httpRequest The HTTP Request that is returning from the api call */ handleSaveQueueSuccess: function(customSuccess, responseData, httpRequest) { if (this.saveQueue.length && this.saveQueue[0].request === httpRequest) { // there are items in the save queue and the httpRequest // that was returned exactly matches the next item in the saveQueue // removes this.saveQueue[0] from the array, since the request being // processed is the current top of the saveQueue, we don't need to do // anything with it just shift it off the array this.saveQueue.shift(); if (_.isFunction(customSuccess)) { // if this has been returned in the proper order customSuccess(responseData); } // now that the latest request has been processed, check if other // items in the saveQueue need to be processed or not this._processSaveQueue(); } else { // the httpRequest being returned does not match the next request that // should be processed, so save it for later _.some(this.saveQueue, function(queueObj) { if (queueObj.request === httpRequest) { queueObj.callReturned = true; queueObj.customSuccess = customSuccess; queueObj.responseData = responseData; return true; } return false; }, this); } }, /** * Handles checking if more items in `this.saveQueue` need to be processed and then processes them * calling itself again to make sure any remaining items get checked and processed. * * @private */ _processSaveQueue: function() { var saveQueueObj; // check if the next first request in the saveQueue has returned // and needs to be processed or not if (this.saveQueue.length && this.saveQueue[0].callReturned) { // there are api calls still in the saveQueue and now // the first one already has response data that needs to be handled // removes this.saveQueue[0] from the array and places it into saveQueueObj saveQueueObj = this.saveQueue.shift(); if (_.isFunction(saveQueueObj.customSuccess)) { // check if this had previously been returned out of order and // is now the first item in saveQueue it will have customSuccess saved saveQueueObj.customSuccess(saveQueueObj.responseData); } this._processSaveQueue(); } }, /** * The success event handler for when a user reorders or moves an item to a different group * * @param {View.QuoteDataGroupLayout} oldGroup The old QuoteDataGroupLayout * @param {View.QuoteDataGroupLayout} newGroup The new QuoteDataGroupLayout * @param {Array} bulkResponses The responses from each of the bulk requests * @protected */ _onSaveUpdatedGroupSuccess: function(oldGroup, newGroup, bulkResponses) { var deleteResponse = _.find(bulkResponses, function(resp) { return resp.contents.id && !resp.contents.hasOwnProperty('date_modified'); }); var deletedGroupId = deleteResponse && deleteResponse.contents.id; var deletedGroup; var newGroupBundle; var deletedGroupBundle; var bundles; if (oldGroup) { oldGroup.trigger('quotes:group:save:stop'); } newGroup.trigger('quotes:group:save:stop'); // remove the deleted group if it exists if (deletedGroupId) { app.alert.dismiss('deleting_bundle_alert'); app.alert.show('deleted_bundle_alert', { level: 'success', autoClose: true, messages: app.lang.get('LBL_DELETED_BUNDLE_SUCCESS_MSG', 'Quotes') }); // get the deleted group deletedGroup = this._getComponentByGroupId(deletedGroupId); // get the bundle for the deleted group deletedGroupBundle = deletedGroup.model.get('product_bundle_items'); // get the bundle for the new group newGroupBundle = newGroup.model.get('product_bundle_items'); // add the deleted group's models to the new group _.each(deletedGroupBundle.models, function(model) { newGroupBundle.add(model); model.link = { bean: newGroup.model, isNew: model.link.isNew, name: model.link.name }; }, this); } _.each(bulkResponses, _.bind(function(oldGroup, newGroup, data) { var record = data.contents.record; var relatedRecord = data.contents.related_record; var model; var isGetRequest = false; // remove position and line_num fields if they exist relatedRecord = _.omit(relatedRecord, 'position', 'line_num'); // if data.contents.record was empty but contents has an id (DELETE and GET) and date_modified (only GET) if (_.isUndefined(record) && (data.contents.id && data.contents.hasOwnProperty('date_modified'))) { // this is a GET request isGetRequest = true; record = data.contents; } // on DELETE record and relatedRecord will both be missing // on GET ProductBundles relatedRecord will not exist but isGetRequest should be set above // on any other request, relatedRecord will be set if (record && (relatedRecord || isGetRequest)) { // only update if there are new records to update with if (oldGroup && !oldGroup.disposed) { // check if record is the one on this collection if (oldGroup.model && record && oldGroup.model.get('id') === record.id) { this._updateModelWithRecord(oldGroup.model, record); } // if oldGroup exists, check if the related_record is in the oldGroup model = oldGroup.collection.get(relatedRecord.id); if (model) { this._updateModelWithRecord(model, relatedRecord); } } if (newGroup) { // check if record is the one on this collection if (newGroup.model && record && newGroup.model.get('id') === record.id) { this._updateModelWithRecord(newGroup.model, record); } // check if the related_record is in the newGroup model = newGroup.collection.get(relatedRecord.id); if (model) { this._updateModelWithRecord(model, relatedRecord); } } } }, this, oldGroup, newGroup), this); if (deletedGroupId) { // remove the deleted group ID from the main groupIds this.groupIds = _.without(this.groupIds, deletedGroupId); // get the main bundles collection bundles = this.model.get('bundles'); // remove the deleted group's model from the main bundles bundles.remove(deletedGroup.model); if (bundles._linkedCollections && bundles._linkedCollections.product_bundles && bundles._linkedCollections.product_bundles._delete && bundles._linkedCollections.product_bundles._delete.length) { // clear out the bundles linkedCollections delete var del = bundles._linkedCollections.product_bundles._delete; bundles._linkedCollections.product_bundles._delete = _.reject(del, function(model) { return model.cid === deletedGroup.model.cid; }); } // dispose the group deletedGroup.dispose(); // remove the component from the layout this.removeComponent(deletedGroup); // once new items are added to the default group, update the group's line numbers newGroup.trigger('quotes:line_nums:reset', newGroup.groupId, newGroup.collection); } }, /** * Iterates through all rows in a group and updates the positions for the rows if necessary * * @param {View.QuoteDataGroupLayout} dataGroup The group component * @return {Array} * @protected */ _updateRowPositions: function(dataGroup) { var retCalls = []; var rows = dataGroup.$('tr.quote-data-group-list:not(:hidden):not(.empty-row)'); var $row; var rowNameSplit; var rowId; var rowModule; var rowModel; var url; var linkName; var dataGroupModelId; var itemModelId; _.each(rows, _.bind(function(dataGroup, retObj, row, index) { $row = $(row); rowNameSplit = $row.attr('name').split('_'); rowModule = rowNameSplit[0]; rowId = rowNameSplit[1]; rowModel = dataGroup.collection.get(rowId); if (rowModel.get('position') != index && !rowModel.isNew()) { dataGroupModelId = dataGroup.model.id; itemModelId = rowModel.id; linkName = rowModule === 'Products' ? 'products' : 'product_bundle_notes'; url = app.api.buildURL('ProductBundles/' + dataGroupModelId + '/link/' + linkName + '/' + itemModelId); retCalls.push({ url: url.substr(4), method: 'PUT', data: { position: index } }); rowModel.set('position', index); } }, this, dataGroup, retCalls), this); if (retCalls.length) { // if items have changed positions, sort the collection // using the collection.comparator compare function dataGroup.collection.sort(); } return retCalls; }, /** * Gets a quote-data-group component by the group ID * * @param {string} groupId The group's id * @protected */ _getComponentByGroupId: function(groupId) { // since groupId could be the cid or the model.id we should check both places return _.find(this._components, function(group) { return group.name === 'quote-data-group' && (group.groupId === groupId || (group.model && group.model.id === groupId)); }); }, /** * Handles when user drags an item into/over a group * * @param {jQuery.Event} evt The jQuery sortover event * @param {Object} ui The jQuery Sortable UI Object * @protected */ _onGroupDragTriggerOver: function(evt, ui) { var groupId = $(evt.target).data('group-id'); var group = this._getComponentByGroupId(groupId); if (group) { group.trigger('quotes:sortable:over', evt, ui); } }, /** * Handles when user drags an item out of a group * * @param {jQuery.Event} evt The jQuery sortout event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onGroupDragTriggerOut: function(evt, ui) { var groupId = $(evt.target).data('group-id'); var group = this._getComponentByGroupId(groupId); if (group) { group.trigger('quotes:sortable:out', evt, ui); } }, /** * Removes the sortable plugin from any rows that have the plugin added * so we don't add plugin multiple times and for dispose cleanup */ beforeRender: function() { var groups = this.$(this.sortableTag); if (groups.length) { _.each(groups, function(group) { if ($(group).hasClass('ui-sortable')) { $(group).sortable('destroy'); } }, this); } }, /** * Creates the default ProductBundles Bean with default group ID * * @return {Data.Bean} * @protected */ _getDefaultGroupModel: function() { var defaultGroup = this._createNewProductBundleBean(null, 0, true); // if there is not a default group yet, add one this.defaultGroupId = defaultGroup.cid; return defaultGroup; }, /** * Creates a new ProductBundle Bean * * @param {String) groupId The groupId to use, if not passed in, will generate a new UUID * @param {number) newPosition The position to use for the group * @param {boolean) isDefaultGroup If this group is the default group or not * @return {Data.Bean} * @protected */ _createNewProductBundleBean: function(groupId, newPosition, isDefaultGroup) { newPosition = newPosition || 0; isDefaultGroup = isDefaultGroup || false; return app.data.createBean('ProductBundles', { _module: 'ProductBundles', _action: 'create', _link: 'product_bundles', default_group: isDefaultGroup, currency_id: this.model.get('currency_id'), base_rate: this.model.get('base_rate'), product_bundle_items: [], product_bundle_notes: [], position: newPosition }); }, /** * Handler for when quote_data changes on the model * * @param {Backbone.Model|Data.MixedBeanCollection} productBundles The quote_data object that changed * @protected */ _onProductBundleChange: function(productBundles) { var hasDefaultGroup = false; var defaultGroupModel; // after adding and deleting models, the change event is like its change for the model, where the // model is the first param and not the actual value it's self. if (productBundles instanceof Backbone.Model) { productBundles = productBundles.get('bundles'); } // check to see if there's a default group in the bundle if (productBundles && productBundles.length > 0) { hasDefaultGroup = _.some(productBundles.models, bundle => bundle.get('default_group')); } if (!hasDefaultGroup) { defaultGroupModel = this._getDefaultGroupModel(); // calling unshift on the collection with silent so it doesn't // cause this function to be triggered again halfway thru productBundles.unshift(defaultGroupModel); } else { // default group exists, get the ID defaultGroupModel = _.find(productBundles.models, bundle => bundle.get('default_group')); this.defaultGroupId = defaultGroupModel.cid; } let editGroupRecId = this.model.get('editGroupRecId'); productBundles.each(function(bundle) { // The block prevents numerous unnecessary API requests to the server ("/ExpressionEngine/.../related") // that "freeze" the UI, especially when there are many groups of QLIs in the Quote if (editGroupRecId && bundle.id !== editGroupRecId && !bundle.get('default_group')) { return; } // Check to see if the group already exists, but the model has been // replaced, as is the case when saving at the Quote record level, // when the server returns new models for the collection fields. In // that case, replace the model of the group with the new one let group = this._getComponentByGroupId(bundle.id); if (!this.isCreateView && !_.isEmpty(group) && group.groupId !== bundle.cid) { this._switchGroupModel(group, bundle); } else if (!_.contains(this.groupIds, bundle.cid)) { this.groupIds.push(bundle.cid); this._addQuoteGroupToLayout(bundle); } }, this); if (!this.isCopy) { this.render(); } else if (this.copyBundleCount === 0 && this.copyBundleCompleted) { // Rendering when user tries to create a group in Quote Copy Mode. this.render(); } this.model.unset('editGroupRecId'); }, /** * Switches the model of an existing group and updates the groupId list * * @param {Layout} group the quote-data-group layout that represents a group * of line items (Product Bundle) * @param {Bean} model the model of the Product Bundle to switch the group to * @private */ _switchGroupModel: function(group, model) { this.groupIds = _.without(this.groupIds, group.groupId); group.switchModel(model); this.groupIds.push(group.groupId); }, /** * Adds the actual quote-data-group layout component to this layout * * @param {Object} [bundle] The ProductBundle data object * @private */ _addQuoteGroupToLayout: function(bundle) { var pbContext = this.context.getChildContext({link: 'product_bundles'}); var groupLayout = app.view.createLayout({ context: pbContext, meta: this.quoteDataGroupMeta, type: 'quote-data-group', layout: this, module: 'ProductBundles', model: bundle }); groupLayout.initComponents(undefined, pbContext, 'ProductBundles'); this.addComponent(groupLayout); }, /** * Handles the quotes:group:create event * Creates a new empty quote data group and renders the groups * * @private */ _onCreateQuoteGroup: function() { var bundles = this.model.get('bundles'); var nextPosition = 0; var highestPositionBundle = bundles.max(function(bundle) { return bundle.get('position'); }); var newBundle; this.bundlesBeingSavedCt++; // handle on the off chance that no bundles exist on the quote. if (!_.isEmpty(highestPositionBundle)) { nextPosition = parseInt(highestPositionBundle.get('position')) + this.bundlesBeingSavedCt; } if (this.isCreateView) { // do not perform saves on create view newBundle = this._createNewProductBundleBean(undefined, nextPosition, false); // set the _justSaved flag so the new bundle header starts in edit mode newBundle.set('_justSaved', true); // ignore preferred currency so that we keep the selected currency. newBundle.ignoreUserPrefCurrency = true; // add the new bundle which will add it to the layout and groupIds bundles.add(newBundle); // trigger that the group create was successful and pass the new group data this.context.trigger('quotes:group:create:success', newBundle); } else { app.alert.show('adding_bundle_alert', { level: 'info', autoClose: false, messages: app.lang.get('LBL_ADDING_BUNDLE_ALERT_MSG', 'Quotes') }); app.api.relationships('create', 'Quotes', { 'id': this.model.get('id'), 'link': 'product_bundles', 'related': { currency_id: this.model.get('currency_id'), base_rate: this.model.get('base_rate'), position: nextPosition } }, null, { success: _.bind(this._onCreateQuoteGroupSuccess, this) }); } }, /** * Success callback handler for when a quote group is created * * @param {Object} newBundleData The new Quote group data * @private */ _onCreateQuoteGroupSuccess: function(newBundleData) { this.bundlesBeingSavedCt--; app.alert.dismiss('adding_bundle_alert'); app.alert.show('added_bundle_alert', { level: 'success', autoClose: true, messages: app.lang.get('LBL_ADDED_BUNDLE_SUCCESS_MSG', 'Quotes') }); var bundles = this.model.get('bundles'); // make sure that the product_bundle_items array is there if (_.isUndefined(newBundleData.related_record.product_bundle_items)) { newBundleData.related_record.product_bundle_items = []; } newBundleData.related_record._justSaved = true; // now add the new record to the bundles collection bundles.add(newBundleData.related_record); if (this.model.get('show_line_nums')) { // if show_line_nums is true, trigger the event so the new group will add the line_num field this.context.trigger('quotes:show_line_nums:changed', true); } // trigger that the group create was successful and pass the new group data this.context.trigger('quotes:group:create:success', newBundleData); }, /** * Called when line items have been selected and user has clicked Delete Selected. * It prepares the group lists and models to be deleted and adds GET requests * for each group after the deletes * * @param {Data.MixedBeanCollection} massCollection The mass_collection from the quote data list * @private */ _onDeleteSelectedItems: function(massCollection) { var bulkRequests = []; var groupsToUpdate = []; var rowId; var groupId; var groupLayout; var url; _.each(massCollection.models, function(model) { if (model.link) { groupId = model.link.bean.id; rowId = model.get('id'); // add the group ID to update the group later groupsToUpdate.push(groupId); // get the QuoteDataGroupLayout component groupLayout = this._getComponentByGroupId(groupId); // remove this row from the list's toggledModels if it exists delete groupLayout.quoteDataGroupList.toggledModels[rowId]; url = app.api.buildURL(model.module + '/' + rowId); bulkRequests.push({ url: url.substr(4), method: 'DELETE' }); } }, this); // make sure the groups are only in here once groupsToUpdate = _.uniq(groupsToUpdate); _.each(groupsToUpdate, function(groupIdToUpdate) { url = app.api.buildURL('ProductBundles' + '/' + groupIdToUpdate); bulkRequests.push({ url: url.substr(4), method: 'GET' }); }, this); if (bulkRequests.length) { this.currentBulkSaveRequests = bulkRequests; this._callBulkRequests(_.bind(this._onDeleteSelectedItemsSuccess, this, massCollection)); } }, /** * Called on success after _onDeleteSelectedItems sets up models to be deleted. This function * removes deleted models from the MassCollection and the group's layout, and updates group * models with updated data. * * @param {Data.MixedBeanCollection} massCollection The mass_collection from the quote data list * @param {Array} bulkRequests The results from the BulkAPI calls * @private */ _onDeleteSelectedItemsSuccess: function(massCollection, bulkRequests) { var model; var groupId; var groupLayout; var $checkAllCheckbox = this.$('.checkall input').first(); if ($checkAllCheckbox.length) { // uncheck the CheckAll box after items are deleted $checkAllCheckbox.attr('checked', false); } app.alert.dismiss('deleting_line_item'); app.alert.show('deleted_line_item', { level: 'success', autoClose: true, messages: [ app.lang.get('LBL_DELETED_ITEMS_SUCCESS_MSG', this.module) ] }); _.each(bulkRequests, function(request) { model = massCollection.get(request.contents.id); if (model) { // the request was for a model in the massCollection groupId = model.link.bean.id; // get the QuoteDataGroupLayout component groupLayout = this._getComponentByGroupId(groupId); // remove the model from the group layout groupLayout.collection.remove(model); // remove the model from the massCollection massCollection.remove(model); } else { // the request was to update a Bundle group groupId = request.contents.id; // get the QuoteDataGroupLayout component groupLayout = this._getComponentByGroupId(groupId); // update the group's model with the latest contents data this._updateModelWithRecord(groupLayout.model, request.contents); // trigger the line nums to be recalculated groupLayout.trigger('quotes:line_nums:reset', groupLayout.groupId, groupLayout.collection); } }, this); }, /** * Deletes the passed in ProductBundle * * @param {ProductBundlesQuoteDataGroupLayout} groupToDelete The group layout to delete * @private */ _onDeleteQuoteGroup: function(groupToDelete) { var groupId = groupToDelete.model.id; var groupName = groupToDelete.model.get('name') || ''; app.alert.show('confirm_delete_bundle', { level: 'confirmation', autoClose: false, messages: app.lang.get('LBL_DELETING_BUNDLE_CONFIRM_MSG', 'Quotes', { groupName: groupName }), onConfirm: _.bind(this._onDeleteQuoteGroupConfirm, this, groupId, groupName, groupToDelete) }); }, /** * Handler for when the delete quote group confirm box is confirmed * * @param {string} groupId The model ID of the deleted group * @param {string} groupName The model name of the deleted group * @param {View.Layout} groupToDelete The Layout for the deleted group * @private */ _onDeleteQuoteGroupConfirm: function(groupId, groupName, groupToDelete) { var defaultGroup = this._getComponentByGroupId(this.defaultGroupId); var bulkRequests = []; var bundleItems; var positionStart; var linkName; var url; app.alert.show('deleting_bundle_alert', { level: 'info', autoClose: false, messages: app.lang.get('LBL_DELETING_BUNDLE_ALERT_MSG', 'Quotes', { groupName: groupName }) }); if (this.isCreateView) { this._removeGroupFromLayout(groupId, groupToDelete); } else { if (groupToDelete.model && groupToDelete.model.has('product_bundle_items')) { bundleItems = groupToDelete.model.get('product_bundle_items'); } // remove any unsaved models _.each(bundleItems.models, _.bind(function(bundleItems, groupToDelete, model, key, list) { // in _.each, if list is an object, model becomes undefined and list becomes // an array with the last model model = model || list[0]; if (model.isNew()) { delete groupToDelete.quoteDataGroupList.toggledModels[model.cid]; bundleItems.remove(model); } }, this, bundleItems, groupToDelete), this); if (defaultGroup.model && defaultGroup.model.has('product_bundle_items')) { positionStart = defaultGroup.model.get('product_bundle_items').length; } if (bundleItems && bundleItems.length > 0) { _.each(bundleItems.models, _.bind(function(groupId, bulkRequests, posStart, model, index, list) { linkName = (model.module === 'Products' ? 'products' : 'product_bundle_notes'); url = app.api.buildURL('ProductBundles/' + groupId + '/link/' + linkName + '/' + model.id); posStart += index; model.set('position', posStart); bulkRequests.push({ url: url.substr(4), method: 'POST', data: { id: groupId, link: linkName, relatedId: model.id, position: posStart } }); }, this, defaultGroup.model.id, bulkRequests, positionStart)); } url = app.api.buildURL('ProductBundles/' + groupId); bulkRequests.push({ url: url.substr(4), method: 'DELETE' }); this.currentBulkSaveRequests = bulkRequests; if (defaultGroup.model.isNew()) { this._saveDefaultGroupThenCallBulk(groupToDelete, defaultGroup, bulkRequests); } else { this._callBulkRequests(_.bind(this._onSaveUpdatedGroupSuccess, this, groupToDelete, defaultGroup)); } } }, /** * Removes a group from the layout * * @param {string} groupId The model ID of the deleted group * @param {View.Layout} groupToDelete The Layout for the deleted group * @private */ _removeGroupFromLayout: function(groupId, groupToDelete) { app.alert.dismiss('deleting_bundle_alert'); var bundles = this.model.get('bundles'); bundles.remove(groupToDelete.model); this.groupIds = _.without(this.groupIds, groupId); // dispose the group groupToDelete.dispose(); }, /** * @inheritdoc */ _dispose: function() { this.beforeRender(); if (app.controller && app.controller.context) { var viewDetails = this.closestComponent('record') ? this.closestComponent('record') : this.closestComponent('create'); if (!_.isUndefined(viewDetails)) { app.controller.context.off(viewDetails.cid + ':productCatalogDashlet:add', null, this); } } this._super('_dispose'); } }) } }} , "datas": {} }, "Products":{"fieldTemplates": { "base": { "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.CurrencyField * @alias SUGAR.App.view.fields.BaseProductsCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) extendsFrom: 'BaseCurrencyField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // Enabling currency dropdown on Qli ist views this.hideCurrencyDropdown = false; }, /** * @inheritdoc */ format: function(value) { // Skipping the core currencyField call // app.view.Field.prototype.format.call(this, value); this._super('format', [value]); //Check if in 'Edit' mode if (this.tplName === 'edit') { //Display just currency value without currency symbol when entering edit mode for the first time //We want the correct value in input field corresponding to the currency in the dropdown //Example: Dropdown has Euro then display '100.00' instead of '$111.11' return app.utils.formatNumberLocale(value); } var transactionalCurrencyId = this.model.get(this.def.currency_field || 'currency_id'); var convertedCurrencyId = transactionalCurrencyId; var origTransactionValue = value; // If necessary, do a conversion to the preferred currency. Otherwise, // just display the currency as-is. var preferredCurrencyId = this.getPreferredCurrencyId(); if (preferredCurrencyId && preferredCurrencyId !== transactionalCurrencyId) { convertedCurrencyId = preferredCurrencyId; this.transactionValue = app.currency.formatAmountLocale( this.model.get(this.name) || 0, transactionalCurrencyId ); let quoteRate = this._getQuoteRate(); let toRate = quoteRate[preferredCurrencyId] || app.metadata.getCurrency(preferredCurrencyId).conversion_rate; value = app.currency.convertWithRate( value, this.model.get('base_rate'), toRate ); } else { // user preferred same as transactional, no conversion required this.transactionValue = ''; convertedCurrencyId = transactionalCurrencyId; value = origTransactionValue; } return app.currency.formatAmountLocale(value, convertedCurrencyId); }, /** * @inheritdoc */ _render: function() { if ( this.view.name === 'quote-data-group-list' && !app.acl.hasAccessToModel('edit', this.model, this.name) && this.options.viewName === 'edit' ) { this.options.viewName = this.action = 'list'; } this._super('_render'); }, /** * Determines the correct preferred currency ID to convert to depending on * the context this currency field is being displayed in * @return {string|undefined} the ID of the preferred currency if it exists */ getPreferredCurrencyId: function() { // If this is a QLI subpanel, and the user has opted to show in their // preferred currency, use that currency. Otherwise, use the system currency. if (this.context.get('isSubpanel')) { if (app.user.getPreference('currency_show_preferred')) { return app.user.getPreference('currency_id'); } return app.currency.getBaseCurrencyId(); } // Get the preferred currency of the parent context or this context. For // Quotes record view, this will get the Quote's preferred currency var context = this.context.parent || this.context; return context.get('model').get('currency_id'); }, /** * Get the quote rate from the parent context * @return {Object} */ _getQuoteRate: function() { let lockedCurrencyRates = this.model.get('quote_locked_currency_rates'); if (lockedCurrencyRates) { return JSON.parse(lockedCurrencyRates); } else if (this.context.parent && this.context.parent.get('model')) { return this.context.parent.get('model').get('locked_currency_rates') || {}; } return {}; }, /** * @inheritdoc */ updateModelWithValue: function(model, currencyId, val) { // Convert the discount amount value only if it is not in % // Other values will be converted as usual if (val && !(this.name === 'discount_amount' && this.model.get('discount_select'))) { let quoteRate = this._getQuoteRate(); if (quoteRate[currencyId] && quoteRate[this._lastCurrencyId]) { let previousCurrency = quoteRate[this._lastCurrencyId]; let currentCurrency = quoteRate[currencyId]; this.model.set('base_rate', currentCurrency); this.model.set( this.name, app.currency.convertWithRate( val, previousCurrency, currentCurrency ) ); // now defer changes to the end of the thread to avoid conflicts // with other events (from SugarLogic, etc.) this._deferModelChange(); } else { this._super('updateModelWithValue', [model, currencyId, val]); } } } }) }, "quote-data-editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseProductsEditablelistbuttonField * @extends View.Fields.Base.BaseEditablelistbuttonField */ ({ // Quote-data-editablelistbutton FieldTemplate (base) extendsFrom: 'BaseEditablelistbuttonField', /** * Overriding EditablelistbuttonField's Events with mousedown instead of click */ events: { 'mousedown [name=inline-save]': 'saveClicked', 'mousedown [name=inline-cancel]': 'cancelClicked' }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (_.isUndefined(this.changed) && this.model.isNew()) { // when adding additional items to the list, causing additional renders, // this.changed gets set undefined on re-initialize, so we need to make sure // if this is an unsaved model and this.changed is undefined, that we set changed true this.changed = true; } if (this.tplName === 'edit') { this.$el.closest('.left-column-save-cancel').addClass('higher'); } else { this.$el.closest('.left-column-save-cancel').removeClass('higher'); } }, /** * Overriding and not calling parent _loadTemplate as those are based off view/actions and we * specifically need it based off the modelView set by the parent layout for this row model * * @inheritdoc */ _loadTemplate: function() { this.tplName = this.model.modelView || 'list'; if (this.view.action === 'list' && _.indexOf(['edit', 'disabled'], this.action) < 0) { this.template = app.template.empty; } else { this.template = app.template.getField(this.type, this.tplName, this.module); } }, /** * @inheritdoc */ cancelEdit: function() { if (this.isDisabled()) { this.setDisabled(false); } this.changed = false; this.model.revertAttributes(); this.view.clearValidationErrors(); // this is the only line I had to change this.view.toggleRow(this.model.module, this.model.cid, false); // trigger a cancel event across the view layout so listening components // know the changes made in this row are being reverted if (this.view.layout) { this.view.layout.trigger('editablelist:' + this.view.name + ':cancel', this.model); } }, /** * @inheritdoc */ saveClicked: function(evt) { // If name exists but product_template_name is empty, // copy name to product_template_name so the field validates if (!_.isEmpty(this.model.get('name')) && _.isEmpty(this.model.get('product_template_name'))) { this.model.set('product_template_name', this.model.get('name'), {silent: true}); } let parentModel = this.options.context.get('parentModel'); if (parentModel) { parentModel.set('editGroupRecId', $(evt.delegateTarget).closest('tbody').data('record-id')); } this._super('saveClicked', [evt]); }, /** * Called after the save button is clicked and all the fields have been validated, * triggers an event for * * @inheritdoc */ _save: function() { this.view.layout.trigger('editablelist:' + this.view.name + ':saving', true, this.model.cid); if (this.view.model.isNew()) { this.view.context.parent.trigger('quotes:defaultGroup:save', _.bind(this._saveRowModel, this)); } else { this._saveRowModel(); } }, /** * Saves the row's model * * @private */ _saveRowModel: function() { var self = this; var oldModelId = this.model.id || this.model.cid; var successCallback = function(model) { self.changed = false; self.model.modelView = 'list'; if (self.view.layout) { self.view.layout.trigger('editablelist:' + self.view.name + ':save', self.model, oldModelId); // trigger event for QuotesLineNumHelper plugin to re-number the lines self.view.layout.trigger('quotes:line_nums:reset'); self._fetchParentModel(); } if (model.collection._resavePositions) { delete model.collection._resavePositions; var bulkSaveRequests = []; var bulkUrl; var bulkRequest; var linkName; var itemModelId; var collectionId = model.link.bean.id; _.each(model.collection.models, function(mdl) { itemModelId = mdl.id; linkName = mdl.module === 'Products' ? 'products' : 'product_bundle_notes'; bulkUrl = app.api.buildURL('ProductBundles/' + collectionId + '/link/' + linkName + '/' + itemModelId); bulkRequest = { url: bulkUrl.substr(4), method: 'PUT', data: { position: mdl.get('position') } }; bulkSaveRequests.push(bulkRequest); }, this); app.api.call('create', app.api.buildURL(null, 'bulk'), { requests: bulkSaveRequests }); } }; var options = { success: successCallback, error: function(model, error) { if (error.status === 409) { app.utils.resolve409Conflict(error, self.model, function(model, isDatabaseData) { if (model) { if (isDatabaseData) { successCallback(model); } else { self._save(); } } }); } }, complete: function() { // remove this model from the list if it has been unlinked if (self.model.get('_unlinked')) { self.collection.remove(self.model, {silent: true}); self.collection.trigger('reset'); self.view.render(); } else { self.setDisabled(false); } }, lastModified: self.model.get('date_modified'), //Show alerts for this request showAlerts: { 'process': true, 'success': { messages: app.lang.get('LBL_RECORD_SAVED', self.module) } }, relate: this.model.link ? true : false }; options = _.extend({}, options, this.getCustomSaveOptions(options)); this.model.save({}, options); }, /** * @inheritdoc */ _validationComplete: function(isValid) { if (!isValid) { this.setDisabled(false); return; } // also need to make sure the model.changed is empty as well if (!this.changed && !this.model.changed) { this.cancelEdit(); return; } this._save(); } }) }, "discount": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.DiscountField * @alias SUGAR.App.view.fields.BaseProductsDiscountField * @extends View.Fields.Base.Products.CurrencyField */ ({ // Discount FieldTemplate (base) extendsFrom: 'ProductsCurrencyField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); var validationTaskName = 'isNumeric_validator_' + this.cid; // removing the validation task if it exists already for this field this.model.removeValidationTask(validationTaskName); this.model.addValidationTask(validationTaskName, _.bind(this._validateAsNumber, this)); }, /** * Overriding to add the custom validation handler to the dom change event * * @inheritdoc */ bindDomChange: function() { if (!(this.model instanceof Backbone.Model)) { return; } var $el = this.$(this.fieldTag); if ($el.length) { $el.on('change', _.bind(function(evt) { var val = evt.currentTarget.value; this.clearErrorDecoration(); this.model.set(this.name, this.unformat(val)); this.model.doValidate(this.name, _.bind(this._validationComplete, this)); }, this)); } }, /** * Callback for after validation runs. * @param {bool} isValid flag determining if the validation is correct * @private */ _validationComplete: function(isValid) { if (isValid) { app.alert.dismiss('invalid-data'); } }, /** * @inheritdoc * * Listen for the discount_select field to change, when it does, re-render the field */ bindDataChange: function() { this._super('bindDataChange'); // if discount select changes, we need to re-render this field this.model.on('change:discount_select', this.render, this); }, /** * @inheritdoc * * Special handling of the templates, if we are displaying it as a percent, then use the _super call, * otherwise get the templates from the currency field. */ _loadTemplate: function() { if (this.model.get('discount_select') == true) { this._super('_loadTemplate'); } else { this.template = app.template.getField('currency', this.action || this.view.action, this.module) || app.template.empty; this.tplName = this.action || this.view.action; } }, /** * @inheritdoc * * Special handling for the format, if we are in a percent, use the decimal field to handle the percent, otherwise * use the format according to the currency field */ format: function(value) { if (this.model.get('discount_select') == true) { return app.utils.formatNumberLocale(value); } else { //In edit mode hide the currency dropdown for the discount field this.hideCurrencyDropdown = this.tplName === 'edit' ? true : false; return this._super('format', [value]); } }, /** * @inheritdoc * * Special handling for the unformat, if we are in a percent, use the decimal field to handle the percent, * otherwise use the format according to the currency field */ unformat: function(value) { if (this.model.get('discount_select') == true) { var unformattedValue = app.utils.unformatNumberStringLocale(value, true); // if unformat failed, return original value return _.isFinite(unformattedValue) ? unformattedValue : value; } else { return this._super('unformat', [value]); } }, /** * Validate the discount field as a number - do not allow letters * * @param {Object} fields The list of field names and their definitions. * @param {Object} errors The list of field names and their errors. * @param {Function} callback Async.js waterfall callback. * @private */ _validateAsNumber: function(fields, errors, callback) { var value = this.model.get(this.name); if (!_.isFinite(value)) { errors[this.name] = {'number': value}; } callback(null, fields, errors); }, /** * Extending to remove the custom validation task for this field * * @inheritdoc * @private */ _dispose: function() { var validationTaskName = 'isNumeric_validator_' + this.cid; this.model.removeValidationTask(validationTaskName); this._super('_dispose'); } }) }, "discount-amount": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.DiscountField * @alias SUGAR.App.view.fields.BaseProductsDiscountField * @extends View.Fields.Base.CurrencyField */ ({ // Discount-amount FieldTemplate (base) extendsFrom: 'DiscountField', /** * @inheritdoc * * Adds special code for QLI discount rendering on subpanels and Quotes record view */ format: function(value) { if (app.utils.isTruthy(this.model.get(this.discountFieldName))) { return app.utils.formatNumberLocale(value); } else { //In edit mode hide the currency dropdown for the discount field this.hideCurrencyDropdown = this.tplName === 'edit' ? true : false; return this.formatForCurrency(value); } }, /** * Formats the field to show correctly on Quotes record view and QLI subpanels * * @param value * @return {string|*} */ formatForCurrency: function(value) { // Skipping the core currencyField call // app.view.Field.prototype.format.call(this, value); this._super('format', [value]); //Check if in 'Edit' mode if (this.tplName === 'edit') { //Display just currency value without currency symbol when entering edit mode for the first time //We want the correct value in input field corresponding to the currency in the dropdown //Example: Dropdown has Euro then display '100.00' instead of '$111.11' return app.utils.formatNumberLocale(value); } var transactionalCurrencyId = this.model.get(this.def.currency_field || 'currency_id'); var convertedCurrencyId = transactionalCurrencyId; var origTransactionValue = value; // If necessary, do a conversion to the preferred currency. Otherwise, // just display the currency as-is. var preferredCurrencyId = this.getPreferredCurrencyId(); if (preferredCurrencyId && preferredCurrencyId !== transactionalCurrencyId) { convertedCurrencyId = preferredCurrencyId; this.transactionValue = app.currency.formatAmountLocale( this.model.get(this.name) || 0, transactionalCurrencyId ); value = app.currency.convertWithRate( value, this.model.get('base_rate'), app.metadata.getCurrency(preferredCurrencyId).conversion_rate ); } else { // user preferred same as transactional, no conversion required this.transactionValue = ''; convertedCurrencyId = transactionalCurrencyId; value = origTransactionValue; } return app.currency.formatAmountLocale(value, convertedCurrencyId); }, /** * Determines the correct preferred currency ID to convert to depending on * the context this currency field is being displayed in * @return {string|undefined} the ID of the preferred currency if it exists */ getPreferredCurrencyId: function() { // If this is a QLI subpanel, and the user has opted to show in their // preferred currency, use that currency. Otherwise, use the system currency. if (this.context.get('isSubpanel')) { if (app.user.getPreference('currency_show_preferred')) { return app.user.getPreference('currency_id'); } return app.currency.getBaseCurrencyId(); } // Get the preferred currency of the parent context or this context. For // Quotes record view, this will get the Quote's preferred currency var context = this.context.parent || this.context; return context.get('model').get('currency_id'); } }) }, "relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.RelateField * @alias SUGAR.App.view.fields.BaseProductsRelateField * @extends View.Fields.Base.RelateField */ ({ // Relate FieldTemplate (base) extendsFrom: 'BaseRelateField', /** * Formats the filter options for add_on_to_name field. * * @param {boolean} force `true` to force retrieving the filter options whether or not it is available in memory. * @return {Object} The filter options. */ getFilterOptions: function(force) { if (this.name && this.name === 'add_on_to_name' && this.model && !_.isEmpty(this.model.get('account_id'))) { return new app.utils.FilterOptions() .config({ 'initial_filter': 'add_on_plis', 'initial_filter_label': 'LBL_PLI_ADDONS', 'filter_populate': { 'account_id': [this.model.get('account_id')] }, }) .format(); } else { return this._super('getFilterOptions', [force]); } }, }) }, "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Quotes.DateField * @alias SUGAR.App.view.fields.BaseQuotesDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.model && this.name && this.name === 'service_start_date') { this.model.on('addon:pli:changed', this.handleRecalculateServiceDuration, this); this.model.on('change:' + this.name, this.handleRecalculateServiceDuration, this); } }, /** * If this is a coterm QLI, recalculate the service duration when the start date * changes so that the end date remains constant. */ handleRecalculateServiceDuration: function() { if (!_.isEmpty(this.model.get('add_on_to_id')) && app.utils.isTruthy(this.model.get('service'))) { var startDate = app.date(this.model.get('service_start_date')); var endDate = app.date(this.model.get('service_end_date')); if (startDate.isSameOrBefore(endDate)) { // we want to be inclusive of the end date endDate.add(1, 'days'); } // calculates the whole years, months, or days var wholeDurationUnit = this.getWholeDurationUnit( startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD') ); if (!_.isEmpty(wholeDurationUnit)) { this.model.set('service_duration_unit', wholeDurationUnit); this.model.set('service_duration_value', endDate.diff(startDate, wholeDurationUnit + 's')); } else { this.model.set('service_duration_unit', 'day'); this.model.set('service_duration_value', endDate.diff(startDate, 'days')); } } }, /** * Gets the whole years, months, or days between two dates * * @param {string} startDate the start date * @param {string} endDate the end date * @return {string} whole year, month or day unit */ getWholeDurationUnit: function(startDate, endDate) { var start = app.date(startDate); var end = app.date(endDate); var years = end.diff(start, 'years'); start.add(years, 'years'); var months = end.diff(start, 'months'); start.add(months, 'months'); var days = end.diff(start, 'days'); return days > 0 ? 'day' : (months > 0 ? 'month' : (years > 0 ? 'year' : '')); }, /** * @inheritdoc */ _dispose: function() { // FIXME: this is a bad "fix" added -- when SC-2395 gets done to upgrade bootstrap we need to remove this if (this._hasDatePicker && this.$(this.fieldTag).data('datepicker')) { $(window).off('resize', this.$(this.fieldTag).data('datepicker').place); } this._hasDatePicker = false; this._super('_dispose'); } }) }, "image": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Image FieldTemplate (base) extendsFrom: 'BaseImageField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // if this image exists in the Quotes QLI quote data section, force it // to use a detail template and don't allow the image field to be editable if (this.view.module === 'ProductBundles') { this.action = 'detail'; this.options.viewName = 'detail'; this.def.width = 16; this.def.height = 16; } } }) }, "line-num": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.LineNumField * @alias SUGAR.App.view.fields.BaseProductsLineNumField * @extends View.Fields.Base.IntField */ ({ // Line-num FieldTemplate (base) extendsFrom: 'IntField' }) }, "quote-data-actionmenu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.QuoteDataActionmenuField * @alias SUGAR.App.view.fields.BaseProductsQuoteDataActionmenuField * @extends View.Fields.Base.ActionmenuField */ ({ // Quote-data-actionmenu FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'BaseActionmenuField', /** * Skipping ActionmenuField's override, just returning this.def.buttons * * @inheritdoc */ _getChildFieldsMeta: function() { return app.utils.deepCopy(this.def.buttons); }, /** * Triggers massCollection events to the context.parent * * @inheritdoc */ toggleSelect: function(checked) { var event = !!checked ? 'mass_collection:add' : 'mass_collection:remove'; this.model.selected = !!checked; this.context.parent.trigger(event, this.model); } }) }, "quote-data-relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.QuoteDataRelateField * @alias SUGAR.App.view.fields.BaseProductsQuoteDataRelateField * @extends View.Fields.Base.BaseRelateField */ ({ // Quote-data-relate FieldTemplate (base) extendsFrom: 'BaseRelateField', /** * The temporary "(New QLI}" string to add if users type in their own product name * @type {string} */ createNewLabel: undefined, /** * The temporary ID to user for newly created QLI names * @type {string} */ newQLIId: undefined, /** * Disable the focus drawer record switching for this field */ disableFocusDrawerRecordSwitching: true, /** * @inheritdoc */ initialize: function(options) { this.createNewLabel = app.lang.get('LBL_CREATE_NEW_QLI_IN_DROPDOWN', 'Products'); this.newQLIId = 'newQLIId'; this._super('initialize', [options]); }, /** * Overriding because getSearchModule needs to return Products for this metadata * * @inheritdoc */ _getPopulateMetadata: function() { return app.metadata.getModule('Products'); }, /** * Overridden select2 change handler for the custom case of being able to add new unlinked Products * @param evt * @private */ _onSelect2Change: function(evt) { var $select2 = $(evt.target).data('select2'); var id = evt.val; var value = id ? $select2.selection.find('span').text() : $(evt.target).data('rname'); var collection = $select2.context; var model; var attributes = { id: '', value: '' }; if (value && value.indexOf(this.createNewLabel)) { // if value had new QLI label, remove it value = value.replace(this.createNewLabel, ''); } value = value ? value.trim() : value; // default to given id/value or empty strings, cleans up logic significantly attributes.id = id || ''; attributes.value = value || ''; if (collection && id) { // if we have search results use that to set new values model = collection.get(id); if (model) { attributes.id = model.id; attributes.value = model.get('name'); _.each(model.attributes, function(value, field) { if (app.acl.hasAccessToModel('view', model, field)) { attributes[field] = attributes[field] || model.get(field); } }); } } else if (evt.currentTarget.value && value) { // if we have previous values keep them attributes.id = value; attributes.value = evt.currentTarget.value; } // set the attribute values this.setValue(attributes); if (id === this.newQLIId) { // if this is a new QLI this.model.set({ product_template_id: '', product_template_name: value, name: value }); // update the select2 label this.$(this.fieldTag).select2('val', value); } return; }, /** * Extending to add the custom createSearchChoice option * * @inheritdoc */ _getSelect2Options: function() { return _.extend(this._super('_getSelect2Options'), { createSearchChoice: _.bind(this._createSearchChoice, this) }); }, /** * Extending to also check models' product_template_name/name and product_template_id/id * * @inheritdoc */ format: function(value) { var idList; value = value || this.model.get(this.name) || this.model.get('name'); this._super('format', [value]); // If value is not set (new row item) then the select2 will show the ID and we dont want that if (value) { idList = this.model.get(this.def.id_name) || this.model.get('id'); if (_.isArray(value)) { this.formattedIds = idList.join(this._separator); } else { this.formattedIds = idList; } if (_.isEmpty(this.formattedIds)) { this.formattedIds = value; } } return value; }, /** * Use the Products module and record ID to build route. * * @inheritdoc */ _buildRoute: function() { if (this.model.get('id') && app.acl.hasAccess('view', this.model.get('_module'))) { this.href = '#' + app.router.buildRoute(this.model.get('_module'), this.model.get('id')); } else { // if no access to module, remove the href this.href = undefined; } }, /** * Overriding as should default to the model's ID then if empty go to the link id * * @inheritdoc */ _getRelateId: function() { return this.model.get(this.def.id_name) || this.model.get('id') ; }, /** * Add a new search choice for the user's text * * @param {string} term The text the user is searching for * @return {{id: (*|string), text: *}} * @private */ _createSearchChoice: function(term) { return { id: this.newQLIId, text: term + this.createNewLabel }; }, /** * @inheritdoc */ getFocusContextModelId: function() { return this.model.get('id'); }, /** * @inheritdoc */ getFocusContextModule: function() { return 'Products'; } }) }, "textarea": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Products.TextareaField * @alias SUGAR.App.view.fields.BaseProductsTextareaField * @extends View.Fields.Base.BaseTextareaField */ ({ // Textarea FieldTemplate (base) extendsFrom: 'BaseTextareaField', /** * Making the textarea editable for the Quotes Line items * @inheritdoc */ setMode: function(name) { if (this.view.name === 'quote-data-group-list' && this.tplName === 'list') { app.view.Field.prototype.setMode.call(this, name); } else { this._super('setMode', [name]); } } }) } }} , "views": { "base": { "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Products.MassupdateView * @alias SUGAR.App.view.views.BaseProductsMassupdateView * @extends View.Views.Base.MassupdateView */ ({ // Massupdate View (base) extendsFrom: 'MassupdateView', /** * @inheritdoc */ save: function(forCalcFields) { if (!this.isEndDateEditableByStartDate()) { this.handleUnEditableEndDateErrorMessage(); return; } this._super('save', [forCalcFields]); }, }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Products.RecordlistView * @alias SUGAR.App.view.views.BaseProductsRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc * * Tracks the last row where the view was changed to non-edit */ toggleRow: function(modelId, isEdit) { this._super('toggleRow', [modelId, isEdit]); if (!isEdit) { this.lastToggledModel = this.collection.get(modelId); } }, /** * @inheritdoc */ _addAdditionalFields: function() { this._super('_addAdditionalFields'); this.context.addFields(['quote_locked_currency_rates']); }, /** * Adds a secondary reverting of model attributes when cancelling an edit * view of a row. This fixes issues with service fields not properly * clearing when cancelling the edit */ cancelClicked: function() { if (this.lastToggledModel) { this.lastToggledModel.revertAttributes(); } this.resize(); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Products.RecordView * @alias SUGAR.App.view.views.BaseProductsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'BaseRecordView', /** * @inheritdoc */ delegateButtonEvents: function() { this.context.on('button:convert_to_quote:click', this.convertToQuote, this); this.context.on('editable:record:toggleEdit', this._toggleRecordEdit, this); this._super('delegateButtonEvents'); }, /** * Extracts the field names from the metadata for directly related views/panels. * @param {string} [module] Module name. */ _getDataFields: function() { return _.union(this._super('_getDataFields'), ['quote_locked_currency_rates']); }, /** * @inheritdoc */ _toggleRecordEdit: function() { this.setButtonStates(this.STATE.EDIT); }, /** * @inheritdoc */ cancelClicked: function() { this.context.trigger('record:cancel:clicked'); this._super('cancelClicked'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "WebLogicHooks":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ProductCategories":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ProductTypes":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ProductTemplates":{"fieldTemplates": { "base": { "pricing-formula": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Field that computes the logic for the pricing factor field * * @class View.Fields.Base.ProductTemplates.PricingFormulaField * @alias SUGAR.App.view.fields.BaseProductTemplatesPricingFormulaField * @extends View.Fields.Base.EnumField */ ({ // Pricing-formula FieldTemplate (base) /** * Where the core logic is at */ extendsFrom: 'EnumField', /** * Should we show the factor field on the front end */ showFactorField: false, /** * Valid formulas that we should show the factor field for. */ validFactorFieldFormulas: [ 'ProfitMargin', 'PercentageMarkup', 'PercentageDiscount' ], /** * Label for the factor field */ factorFieldLabel: '', /** * Value of the factor field */ factorValue: 0, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.before('render', function() { this.showFactorField = this.checkShouldShowFactorField(); this.factorFieldLabel = this.getFactorFieldLabel(); this.disableDiscountField(); this.factorValue = this.model.get('pricing_factor'); }, this); this.listenTo(this, 'render', function() { // only setup the formulas when the action is edit if (this.action == 'edit') { if (this.showFactorField) { // put the cursor int he factor field once this is rendered this.$el.find('.pricing-factor').focus(); } this.setupPricingFormula(); } }); }, /** * Listen for this field to change it's value, and when it does, we should re-render the field as it could have * the pricing_factor field visible */ bindDataChange: function() { this.listenTo(this.model, 'change:' + this.name, function() { // when it's changed, we need to re-render just in case we need to show the factor field if (!this.disposed) { this.render(); } }); }, /** * Override to remove default DOM change listener so we can listen for the pricing factor change if it's visible * * @inheritdoc */ bindDomChange: function() { if (this.showFactorField) { var $el = this.$('.pricing-factor'); $el.on('change', _.bind(function() { this.model.set('pricing_factor', $el.val()); }, this)); } // call the super just in case something ever gets put there this._super('bindDomChange'); }, /** * Override so we can stop listening to the pricing factor field if it's visible * * @inheritdoc */ unbindDom: function() { if (this.showFactorField) { this.$('.pricing-factor').off(); } // call the super this._super('unbindDom'); }, /** * Utility Method to check if we should show the factor field or not * @return {*|boolean} */ checkShouldShowFactorField: function() { return (this.model.has(this.name) && _.contains(this.validFactorFieldFormulas, this.model.get(this.name))); }, /** * Get the correct label for the field type */ getFactorFieldLabel: function() { if (this.model.has(this.name)) { switch (this.model.get(this.name)) { case 'ProfitMargin': return (this.action === 'edit' && this.view.action === 'list') ? 'LBL_POINTS_ABBR' : 'LBL_POINTS'; case 'PercentageMarkup': case 'PercentageDiscount': return (this.action === 'edit' && this.view.action === 'list') ? '%' : 'LBL_PERCENTAGE'; } } return ''; }, /** * Figure out which formula to setup based off the value from the model. */ setupPricingFormula: function() { if (this.model.has(this.name)) { switch (this.model.get(this.name)) { case 'ProfitMargin': this._setupProfitMarginFormula(); break; case 'PercentageMarkup': this._setupPercentageMarkupFormula(); break; case 'PercentageDiscount': this._setupPercentageDiscountFormula(); break; case 'IsList': this._setupIsListFormula(); break; default: var oldPrice = this.model.get('discount_price'); if (_.isUndefined(oldPrice) || _.isNaN(oldPrice)) { this.model.set('discount_price', ''); } break; } } }, /** * Profit Margin Formula * * ($cost_price * 100)/(100 - $points) * * @private */ _setupProfitMarginFormula: function() { var formula = function(cost_price, points) { return app.math.div(app.math.mul(cost_price, 100), app.math.sub(100, points)); }; this._costPriceFormula(formula); }, /** * Percent Markup * * $cost_price * (1 + ($percentage/100)) * * @private */ _setupPercentageMarkupFormula: function() { var formula = function(cost_price, percentage) { return app.math.mul(cost_price, app.math.add(1, app.math.div(percentage, 100))); }; this._costPriceFormula(formula); }, /** * Percent Discount from List Price * * $list_price - ($list_price * ($percentage/100)) * * @private */ _setupPercentageDiscountFormula: function() { var formula = function(list_price, percentage) { return app.math.sub(list_price, app.math.mul(list_price, app.math.div(percentage, 100))); }; this._costPriceFormula(formula, 'list_price'); }, /** * Utility Method to handle multiple formulas using the same listener for cost_price, just pass in a function * that handles the formula and accepts two params, cost_price and the pricing factor. * @param {Function} formula * @param {String} [field] What field to use in the listenTo, if undefined, it will default to cost_price * @private */ _costPriceFormula: function(formula, field) { field = field || 'cost_price' this.listenTo(this.model, 'change:' + field, function(model, price) { model.set('discount_price', formula(price, model.get('pricing_factor'))); }); this.listenTo(this.model, 'change:pricing_factor', function(model, pricing_factor) { model.set('discount_price', formula(model.get(field), pricing_factor)); }); // run this now just to make sure if default values are already set this.model.set('discount_price', formula(this.model.get(field), this.model.get('pricing_factor'))); }, /** * Code to handle when the pricing formula is IsList where discount_price is the same as list_price * * @private */ _setupIsListFormula: function() { this.listenTo(this.model, 'change:list_price', function(model, value) { model.set('discount_price', value); }); this.model.set('discount_price', this.model.get('list_price')); }, /** * Method to handle when the discount_price field should be disable or not. */ disableDiscountField: function() { if (this.model.has(this.name)) { var field = this.view.getField('discount_price'); if (field) { switch (this.model.get(this.name)) { case 'ProfitMargin': case 'PercentageMarkup': case 'PercentageDiscount': case 'IsList': field.setDisabled(true); break; default: field.setDisabled(false); break; } } } } }) } }} , "views": { "base": { "product-catalog-dashlet-drawer-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ProductTemplates.ProducCatalogDashletDrawerRecordView * @alias SUGAR.App.view.views.BaseProductTemplatesProductCatalogDashletDrawerRecordView * @extends View.Views.Base.RecordView */ ({ // Product-catalog-dashlet-drawer-record View (base) extendsFrom: 'BaseRecordView', /** * If this is initialized inside a create view */ isCreateView: undefined, /** * @inheritdoc */ initialize: function(options) { var i; var j; var panel; var field; var moduleName; var addBtn = _.find(options.meta.buttons, function(btn) { return btn.name === 'add_to_quote_button'; }); var removeAddBtn = false; var userACLs; var oppsConfig; var secondaryModule; var showOnViews; var layoutName; var routerFrags; // need to use router because if we're on Home or another module and use the megamenu // to create an Opp or Quote, it shows the previous module we're in, not the current. routerFrags = app.router.getFragment().split('/'); moduleName = routerFrags[0]; this.isCreateView = routerFrags[1] === 'create'; // check to see if there's an add button and if this module is not in the list // to show the add button if (addBtn) { let showOnModules = _.keys(addBtn.showOnModules); // only the list 'records' layout is empty layoutName = routerFrags[1] || 'records'; showOnViews = addBtn.showOnModules[moduleName]; if (!_.contains(showOnModules, moduleName)) { // if this module is not in the list of metadata 'showOnModules' array, remove it removeAddBtn = true; } if (!removeAddBtn) { if (!_.contains(showOnViews, layoutName)) { // if this view is not in the list of metadata 'showOnModules' views doublecheck // if layoutName is 36 characters long and we show on record then allow the add button, // otherwise remove it if (!(layoutName.length === 36 && _.contains(showOnViews, 'record'))) { // if this layoutName is an actual record ID hash removeAddBtn = true; } } } if (!removeAddBtn) { // we need to check other conditions to remove the add button oppsConfig = app.metadata.getModule('Opportunities', 'config'); userACLs = app.user.getAcls(); if (moduleName === 'Opportunities') { if (oppsConfig.opps_view_by === 'RevenueLineItems') { // if Opps+RLI mode, check ACLs on RLIs not Opps secondaryModule = 'RevenueLineItems'; } else { // if in Opps only mode, remove the add button removeAddBtn = true; } } else if (moduleName === 'Quotes') { secondaryModule = 'Products'; } if (_.has(userACLs[moduleName], 'edit') || _.has(userACLs[secondaryModule], 'access') || _.has(userACLs[secondaryModule], 'edit')) { // if the user doesn't have access to edit Opps or Quotes, // or user doesn't have access or edit priveleges for RLIs/QLIs, remove the add button removeAddBtn = true; } } let closestComponent = options.context.get('closestComponent'); if (!closestComponent || closestComponent.name === 'side-drawer' || closestComponent.name === 'dashlet-preview') { removeAddBtn = true; } else if (closestComponent.name === 'convert') { removeAddBtn = closestComponent.triggerBefore('productcatalog:preview:add:disable'); } if (removeAddBtn) { options.meta.buttons = _.without(options.meta.buttons, addBtn); } } options.name = 'record'; for (i = 0; i < options.meta.panels.length; i++) { panel = options.meta.panels[i]; for (j = 0; j < panel.fields.length; j++) { field = panel.fields[j]; field.readonly = true; } } this._super('initialize', [options]); }, /** * Overriding this function to just listen to the buttons on the record * * @inheritdoc */ delegateButtonEvents: function() { this.context.on('button:cancel_button:click', this._drawerCancelClicked, this); this.context.on('button:add_to_quote_button:click', this._drawerAddToQuoteClicked, this); }, /** * Handles when the Cancel button is clicked in the ProductCatalogDashlet drawer. * It just triggers the event that the tree should re-enable, and closes the drawer. * * @private */ _drawerCancelClicked: function() { app.controller.context.trigger(this.model.viewId + ':productCatalogDashlet:add:complete'); app.drawer.close(); }, /** * Handles when the Add To Quote button is clicked in the ProductCatalogDashlet drawer. * It strips out unnecessary ProductTemplate fields and sends the data to the context. * * @private */ _drawerAddToQuoteClicked: function() { var data = this.model.toJSON(); // copy Template's id and name to where the QLI expects them data.product_template_id = data.id; data.product_template_name = data.name; data.assigned_user_id = app.user.id; // remove ID/etc since we dont want Template ID to be the record id delete data.id; delete data.date_entered; delete data.date_modified; delete data.my_favorite; delete data.team_count; delete data.team_count_link; delete data.team_name; delete data.team_id; delete data.team_set_id; // close this drawer first, then trigger event app.drawer.close(); // need to trigger on app.controller.context because of contexts changing between // the PCDashlet, and Opps create being in a Drawer, or as its own standalone page // app.controller.context is the only consistent context to use if (this.isCreateView) { // immediately send event app.controller.context.trigger(this.model.viewId + ':productCatalogDashlet:add', data); } else { // any other view we need to wait for the drawer to close, then trigger the event let viewId = this.model.viewId; _.delay(() => { app.controller.context.trigger(`${viewId}:productCatalogDashlet:add`, data); }, 750); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "filterpanel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filterpanel Layout (base) extendsFrom: 'FilterpanelLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if (this.context.get('layout') === 'record') { var hasSubpanels = false, layouts = app.metadata.getModule(options.module, 'layouts'); if (layouts && layouts.subpanels && layouts.subpanels.meta) { hasSubpanels = (layouts.subpanels.meta.components.length > 0); } if (!hasSubpanels) { this.before('render', function() { return false; }, this); this.template = app.template.empty; this.$el.html(this.template()); } } } }) } }} , "datas": {} }, "ProductBundles":{"fieldTemplates": { "base": { "quote-data-actiondropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundles.QuoteDataActiondropdownField * @alias SUGAR.App.view.fields.BaseProductBundlesQuoteDataActiondropdownField * @extends View.Fields.Base.ActiondropdownField */ ({ // Quote-data-actiondropdown FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'BaseActiondropdownField', /** * @inheritdoc */ className: 'quote-data-actiondropdown', /** * Skipping ActionmenuField's override, just returning this.def.buttons * * @inheritdoc */ _getChildFieldsMeta: function() { return app.utils.deepCopy(this.def.buttons); }, /** * Overriding for quote-data-group-header in create view to display a specific template * * @inheritdoc */ _loadTemplate: function() { this._super('_loadTemplate'); if (this.view.name === 'quote-data-group-header' && this.view.isCreateView) { this.template = app.template.getField('quote-data-actiondropdown', 'list', this.model.module); } } }) }, "quote-data-editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundles.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseProductBundlesEditablelistbuttonField * @extends View.Fields.Base.BaseEditablelistbuttonField */ ({ // Quote-data-editablelistbutton FieldTemplate (base) extendsFrom: 'BaseEditablelistbuttonField', /** * @inheritdoc */ _render: function() { var syncedName; if (this.name === 'inline-save') { syncedName = this.model.getSynced('name'); if (this.model.get('name') !== syncedName) { this.changed = true; } } this._super('_render'); if (this.tplName === 'edit') { this.$el.closest('.left-column-save-cancel').addClass('higher'); } else { this.$el.closest('.left-column-save-cancel').removeClass('higher'); } }, /** * Overriding and not calling parent _loadTemplate as those are based off view/actions and we * specifically need it based off the modelView set by the parent layout for this row model * * @inheritdoc */ _loadTemplate: function() { this.tplName = this.model.modelView || 'list'; if (this.view.action === 'list' && _.indexOf(['edit', 'disabled'], this.action) < 0) { this.template = app.template.empty; } else { this.template = app.template.getField(this.type, this.tplName, this.module); } }, /** * Overriding cancelEdit so we can update the group name if this is coming from * the quote data group header * * @inheritdoc */ cancelEdit: function() { var modelModule = this.model.module; var modelId = this.model.cid; var syncedAttribs = this.model.getSynced(); if (this.isDisabled()) { this.setDisabled(false); } this.changed = false; if (this.view.name === 'quote-data-group-header') { // for cancel on group-header, revertAttributes doesn't reset the model if (this.model.get('name') !== syncedAttribs.name) { if (_.isUndefined(syncedAttribs.name)) { // if name was undefined, unset name this.model.unset('name'); } else { // if name was defined or '', set back to that this.model.set('name', syncedAttribs.name); } } } else { this.model.revertAttributes(); } this.view.clearValidationErrors(); this.view.toggleRow(modelModule, modelId, false); // trigger a cancel event across the view layout so listening components // know the changes made in this row are being reverted if (this.view.layout) { this.view.layout.trigger('editablelist:' + this.view.name + ':cancel', this.model); } }, /** * Overriding cancelClicked to trigger an event if this is a * create view or the group was just saved * * @inheritdoc */ cancelClicked: function() { let justSaved = this.model.getSynced('_justSaved') || false; let justGroupedItems = (this.context.parent && this.context.parent.get('_justGroupedItems')) || false; let itemsInGroup = this.model.get('product_bundle_items') || []; if (this.view.isCreateView || (justSaved && itemsInGroup.length === 0) || justGroupedItems) { this.view.layout.trigger('editablelist:' + this.view.name + ':create:cancel', this.model); } else { this.cancelEdit(); } }, /** * Called after the save button is clicked and all the fields have been validated, * triggers an event for * * @inheritdoc */ _save: function() { this.view.layout.trigger('editablelist:' + this.view.name + ':saving', true); this._saveRowModel(); }, /** * Saves the row's model * * @private */ _saveRowModel: function() { var self = this; var oldModelId = this.model.cid; var quoteModel = this.context.get('parentModel'); var successCallback = function(data, request) { self.changed = false; self.model.modelView = 'list'; if (!_.isEmpty(data.related_record)) { self.model.setSyncedAttributes(data.related_record); self.model.set(data.related_record); } if (self.view.layout) { self.view.layout.trigger('editablelist:' + self.view.name + ':save', self.model, oldModelId); self._fetchParentModel(); } }; var options = { success: successCallback, error: function(error) { if (error.status === 409) { app.utils.resolve409Conflict(error, self.model, function(model, isDatabaseData) { if (model) { if (isDatabaseData) { successCallback(model); } else { self._save(); } } }); } }, complete: function() { // remove this model from the list if it has been unlinked if (self.model.get('_unlinked')) { self.collection.remove(self.model, {silent: true}); self.collection.trigger('reset'); self.view.render(); } else { self.setDisabled(false); } }, apiOptions: { headers: { 'X-TIMESTAMP': self.model.get('date_modified') } }, //Show alerts for this request showAlerts: { 'process': true, 'success': { messages: app.lang.get('LBL_RECORD_SAVED', self.module) } }, relate: this.model.link ? true : false }; options = _.extend({}, options, this.getCustomSaveOptions(options)); app.api.relationships('update', 'Quotes', { id: quoteModel.get('id'), link: 'product_bundles', relatedId: this.model.get('id'), related: { name: this.model.get('name') } }, null, options, options.apiOptions); } }) }, "quote-footer-input": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundles.QuoteFooterInputField * @alias SUGAR.App.view.fields.BaseProductBundlesQuoteFooterInputField * @extends View.Fields.Base.Field */ ({ // Quote-footer-input FieldTemplate (base) /** * The value dollar amount */ value_amount: undefined, /** * The value percent amount */ value_percent: undefined, /** * @inheritdoc */ format: function(value) { if (!value) { this.value_amount = app.currency.formatAmountLocale('0'); this.value_percent = '0%'; } } }) }, "quote-group-title": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundles.QuoteGroupTitleField * @alias SUGAR.App.view.fields.BaseProductBundlesQuoteGroupTitleField * @extends View.Fields.Base.Field */ ({ // Quote-group-title FieldTemplate (base) /** * Any additional CSS classes that need to be applied to the field */ css_class: undefined, /** * @inheritdoc */ initialize: function(options) { this.css_class = options.def.css_class || ''; this._super('initialize', [options]); } }) } }} , "views": { "base": { "quote-data-group-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ProductBundles.QuoteDataGroupHeaderView * @alias SUGAR.App.view.views.BaseProductBundlesQuoteDataGroupHeaderView * @extends View.Views.Base.View */ ({ // Quote-data-group-header View (base) /** * @inheritdoc */ events: { 'click [name="create_qli_button"]': '_onCreateQLIBtnClicked', 'click [name="create_comment_button"]': '_onCreateCommentBtnClicked', 'click [name="edit_bundle_button"]': '_onEditBundleBtnClicked', 'click [name="delete_bundle_button"]': '_onDeleteBundleBtnClicked' }, /** * @inheritdoc */ plugins: [ 'MassCollection', 'Editable', 'ErrorDecoration' ], /** * Array of fields to use in the template */ _fields: undefined, /** * The colspan value for the list */ listColSpan: 0, /** * The CSS class for the save icon */ saveIconCssClass: '.group-loading-icon', /** * How many times the group has been called to start or stop saving */ groupSaveCt: undefined, /** * Object containing the row's fields */ rowFields: {}, /** * Array of left column fields */ leftColumns: undefined, /** * Array of left column fields */ leftSaveCancelColumn: undefined, /** * If this is the first time the view has rendered or not */ isFirstRender: undefined, /** * If this layout is currently in the /create view or not */ isCreateView: undefined, /** * @inheritdoc */ initialize: function(options) { // make sure we're using the layout's model options.model = options.model || options.layout.model; this.listColSpan = options.layout.listColSpan; // use the same massCollection from the Quotes QuoteDataListHeaderView var quoteDataListHeaderComp; if (options.layout && options.layout.layout) { quoteDataListHeaderComp = options.layout.layout.getComponent('quote-data-list-header'); if (quoteDataListHeaderComp) { options.context.set('mass_collection', quoteDataListHeaderComp.massCollection); } } this._super('initialize', [options]); this.isCreateView = this.context.parent.get('create') || false; this.isFirstRender = true; this.viewName = 'list'; this.action = 'list'; this._fields = _.flatten(_.pluck(this.meta.panels, 'fields')); this.toggledModels = {}; this.leftColumns = []; this.leftSaveCancelColumn = []; this.addMultiSelectionAction(); // ninjastuff this.el = this.layout.el; this.setElement(this.el); this.groupSaveCt = 0; this.layout.on('quotes:group:save:start', this._onGroupSaveStart, this); this.layout.on('quotes:group:save:stop', this._onGroupSaveStop, this); this.layout.on('editablelist:' + this.name + ':save', this.onSaveRowEdit, this); this.layout.on('editablelist:' + this.name + ':saving', this.onSavingRow, this); this.layout.on('editablelist:' + this.name + ':create:cancel', this._onDeleteBundleBtnClicked, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); // set row fields after rendering to prep if we need to toggle rows this._setRowFields(); if (!_.isEmpty(this.toggledModels)) { _.each(this.toggledModels, function(model, modelId) { this.toggleRow(model.module, modelId, true); }, this); } // on the first header row render, if this model was _justSaved // we want to toggle the row to edit mode adding this to toggledModels if (this.isFirstRender && this.model.has('_justSaved')) { this.model.unset('_justSaved'); this.isFirstRender = false; this.toggleRow(this.model.module, this.model.cid, true); } }, /** * Handles displaying the loading icon when a group starts saving * * @private */ _onGroupSaveStart: function() { this.groupSaveCt++; this.$(this.saveIconCssClass).show(); }, /** * Handles hiding the loading icon when a group save is complete * * @private */ _onGroupSaveStop: function() { this.groupSaveCt--; if (this.groupSaveCt === 0) { this.$(this.saveIconCssClass).hide(); } if (this.groupSaveCt < 0) { this.groupSaveCt = 0; } }, /** * Handles when the create Quoted Line Item button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateQLIBtnClicked: function(evt) { this.layout.trigger('quotes:group:create:qli', 'products'); }, /** * Handles when the create Comment button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onCreateCommentBtnClicked: function(evt) { this.layout.trigger('quotes:group:create:note', 'product_bundle_notes'); }, /** * Handles when the edit Group button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onEditBundleBtnClicked: function(evt) { var $tbodyEl = $(evt.target).closest('tbody'); var bundleId = $tbodyEl.data('group-id'); this.toggleRow(this.model.module, bundleId, true); }, /** * Handles when the delete Group button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onDeleteBundleBtnClicked: function(evt) { this.context.parent.trigger('quotes:group:delete', this.layout); }, /** * Toggle editable selected row's model fields. * * @param {string} rowModule The row model's module. * @param {string} rowModelId The row model's ID * @param {boolean} isEdit True for edit mode, otherwise toggle back to list mode. */ toggleRow: function(rowModule, rowModelId, isEdit) { var toggleModel; var row; if (isEdit) { toggleModel = this.model; toggleModel.modelView = 'edit'; this.toggledModels[rowModelId] = toggleModel; } else { if (this.toggledModels[rowModelId]) { this.toggledModels[rowModelId].modelView = 'list'; } delete this.toggledModels[rowModelId]; } row = this.$('tr[name=' + rowModule + '_' + rowModelId + ']'); row.toggleClass('tr-inline-edit', isEdit); this.toggleFields(this.rowFields[rowModelId], isEdit); if (isEdit) { // make sure row is not sortable on edit row .addClass('not-sortable') .removeClass('sortable ui-sortable'); this.context.trigger('list:editgroup:fire'); } }, /** * Set, or reset, the collection of fields that contains each row. * * This function is invoked when the view renders. It will update the row * fields once the `Pagination` plugin successfully fetches new records. * * @private */ _setRowFields: function() { this.rowFields = {}; _.each(this.fields, function(field) { if (field.model && field.model.cid && _.isUndefined(field.parent)) { this.rowFields[field.model.cid] = this.rowFields[field.model.cid] || []; this.rowFields[field.model.cid].push(field); } }, this); }, /** * Replaces the model of this view with the given one * * @param {Bean} model the new Product Bundles model to use for this view */ switchModel: function(model) { this.model = model; }, /** * Adds the left column fields */ addMultiSelectionAction: function() { _.each(this.meta.buttons, function(button) { this.leftColumns.push(button); }, this); this.leftSaveCancelColumn.push({ 'type': 'fieldset', 'label': '', 'sortable': false, 'fields': [{ type: 'quote-data-editablelistbutton', label: '', tooltip: 'LBL_CANCEL_BUTTON_LABEL', name: 'inline-cancel', icon: 'sicon-close', css_class: 'btn-link btn-invisible inline-cancel ellipsis_inline' }] }); // if this is the create view, do not add a save button if (this.isCreateView) { this.leftSaveCancelColumn[0].fields.push({ type: 'quote-data-actiondropdown', label: '', tooltip: 'LBL_SAVE_BUTTON_LABEL', name: 'create-dropdown-editmode', icon: 'sicon-plus', css_class: 'ellipsis_inline', no_default_action: true, buttons: [{ type: 'button', icon: 'sicon-plus', name: 'create_qli_button', label: 'LBL_CREATE_QLI_BUTTON_LABEL', acl_action: 'create', tooltip: 'LBL_CREATE_QLI_BUTTON_TOOLTIP' }, { type: 'button', icon: 'sicon-plus', name: 'create_comment_button', label: 'LBL_CREATE_COMMENT_BUTTON_LABEL', acl_action: 'create', tooltip: 'LBL_CREATE_COMMENT_BUTTON_TOOLTIP' }] }); } else { this.leftSaveCancelColumn[0].fields.push({ type: 'quote-data-editablelistbutton', label: '', tooltip: 'LBL_SAVE_BUTTON_LABEL', name: 'inline-save', icon: 'sicon-check-circle', css_class: 'btn-link btn-invisible inline-save ellipsis_inline' }); } }, /** * Handles when a row is saved. * * @param {Data.Bean} rowModel */ onSaveRowEdit: function(rowModel) { // Quote groups always use the cid of the model var modelId = rowModel.cid; var modelModule = rowModel.module; this.toggleCancelButton(false); this.toggleRow(modelModule, modelId, false); }, /** * Toggles the cancel button disabled or not * * @param {boolean} disable If we should disable the button or not */ toggleCancelButton: function(disable) { var cancelBtn = _.find(this.fields, function(field) { return field.name == 'inline-cancel'; }); if (cancelBtn) { cancelBtn.setDisabled(disable); } }, /** * Handles when the row is being saved but has not been saved fully yet * * @param {boolean} disableCancelBtn If we should disable the button or not */ onSavingRow: function(disableCancelBtn) { // todo: SFA-4541 needs to add code in here to toggle fields to readonly this.toggleCancelButton(disableCancelBtn); } }) }, "quote-data-group-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ProductBundles.QuoteDataGroupListView * @alias SUGAR.App.view.views.BaseProductBundlesQuoteDataGroupListView * @extends View.Views.Base.View */ ({ // Quote-data-group-list View (base) /** * @inheritdoc */ events: { 'click [name="edit_row_button"]': '_onEditRowBtnClicked', 'click [name="delete_row_button"]': '_onDeleteRowBtnClicked' }, /** * @inheritdoc */ plugins: [ 'Editable', 'ErrorDecoration', 'MassCollection', 'SugarLogic', 'QuotesLineNumHelper' ], /** * @inheritdoc */ className: 'quote-data-group-list', /** * Array of fields to use in the template */ _fields: undefined, /** * The colspan value for the list */ listColSpan: 0, /** * The colspan value for empty rows listColSpan + 1 since no left column */ emptyListColSpan: 0, /** * Array of left column fields */ leftColumns: undefined, /** * Array of left column fields */ leftSaveCancelColumn: undefined, /** * List of current inline edit models. */ toggledModels: null, /** * Object containing the row's fields */ rowFields: {}, /** * ProductBundleNotes QuoteDataGroupList metadata */ pbnListMetadata: undefined, /** * QuotedLineItems QuoteDataGroupList metadata */ qliListMetadata: undefined, /** * ProductBundleNotes Description field metadata */ pbnDescriptionMetadata: undefined, /** * Track all the SugarLogic Contexts that we create for each record in bundle * * @type {Object} */ sugarLogicContexts: {}, /** * Track the module dependencies for the line item, so we dont have to fetch them every time * * @type {Object} */ moduleDependencies: {}, /** * If this QuoteDataGroupList is the default group list view, or regular header/footer group view */ isDefaultGroupList: undefined, /** * If this view is currently in the /create view or not */ isCreateView: undefined, /** * If this view is in the /create view coming from Opportunities Convert to Quote */ isOppsConvert: undefined, /** * In Convert to Quote, if the RLI models have been added to the Quote yet */ addedConvertModels: undefined, /** * CSS Classes for sortable rows */ sortableCSSClass: 'sortable ui-sortable', /** * CSS Classes for non-sortable rows */ nonSortableCSSClass: 'not-sortable', /** * @inheritdoc */ initialize: function(options) { var parentModelModule; this.pbnListMetadata = app.metadata.getView('ProductBundleNotes', 'quote-data-group-list'); this.qliListMetadata = app.metadata.getView('Products', 'quote-data-group-list'); this.pbnDescriptionMetadata = _.find(this.pbnListMetadata.panels[0].fields, function(field) { return field.name === 'description'; }, this); // make sure we're using the layout's model options.model = options.model || options.layout.model; // get the product_bundle_items collection from the model options.collection = options.model.get('product_bundle_items'); // use the same massCollection from the Quotes QuoteDataListHeaderView var quoteDataListHeaderComp; if (options.layout && options.layout.layout) { quoteDataListHeaderComp = options.layout.layout.getComponent('quote-data-list-header'); if (quoteDataListHeaderComp) { options.context.set('mass_collection', quoteDataListHeaderComp.massCollection); } } this.listColSpan = options.layout.listColSpan; this.emptyListColSpan = this.listColSpan + 1; this._super('initialize', [options]); this.isDefaultGroupList = this.model.get('default_group'); this.isCreateView = this.context.parent.get('create') || false; parentModelModule = this.context.parent.get('parentModel') ? this.context.parent.get('parentModel').get('_module') : ''; this.isOppsConvert = this.isCreateView && this.context.parent.get('convert') && (parentModelModule == 'RevenueLineItems' || parentModelModule == 'Opportunities') && this.context.parent.get('fromLink') != 'quotes'; this.addedConvertModels = this.context.parent.get('addedConvertModels') || false; this.action = 'list'; this.viewName = this.isCreateView ? 'edit' : 'list'; // combine qliListMetadata's panels into this.meta this.meta = _.extend(this.meta, this.qliListMetadata); this._fields = _.flatten(_.pluck(this.qliListMetadata.panels, 'fields')); this.toggledModels = {}; this.leftColumns = []; this.leftSaveCancelColumn = []; this.addMultiSelectionAction(); this.events = _.extend({ 'hidden.bs.dropdown .actions': 'resetDropdownDelegate', 'shown.bs.dropdown .actions': 'delegateDropdown' }, this.events); /** * Due to BackboneJS, this view would have a wrapper tag around it e.g. QuoteDataGroupHeader.tagName "tr" * so this would have also been wrapped in div/tr whatever the tagName was for the view. * I am setting this.el to be the Layout's el (QuoteDataGroupLayout) which is a tbody element. * In the render function I am then manually appending this list of records template * after the group header tr row */ this.el = this.layout.el; this.setElement(this.el); this.isEmptyGroup = this.collection.length === 0; // For each item in the collection, setup SugarLogic this._setupSugarLogic(); // listen directly on the parent QuoteDataGroupLayout this.layout.on('quotes:group:create:qli', this.onAddNewItemToGroup, this); this.layout.on('quotes:group:create:note', this.onAddNewItemToGroup, this); this.layout.on('quotes:sortable:over', this._onSortableGroupOver, this); this.layout.on('quotes:sortable:out', this._onSortableGroupOut, this); this.layout.on('editablelist:' + this.name + ':cancel', this.onCancelRowEdit, this); this.layout.on('editablelist:' + this.name + ':save', this.onSaveRowEdit, this); this.layout.on('editablelist:' + this.name + ':saving', this.onSavingRow, this); this.context.parent.on('quotes:collections:all:checked', this.onAllChecked, this); this.context.parent.on('quotes:collections:not:all:checked', this.onNotAllChecked, this); this.collection.on('add remove', this.onNewItemChanged, this); }, /** * Initializes the SugarLogic contexts for this view * * @private */ _setupSugarLogic: function() { var collections = this.model.fields.product_bundle_items.links; _.each(collections, function(link) { var collection = this.model.getRelatedCollection(link); if (collection) { this.setupSugarLogicForModelOrCollection(collection); } }, this); }, /** * handler for when the select all checkbox is checked */ onAllChecked: function() { //iterate over all of the masscollection checkboxes and check the ones that are unchecked. _.each(this.$('div.checkall input'), function(item) { var $item = $(item); //only trigger if the item isn't checked. if (!$item.prop('checked')) { $item.trigger('click'); } }); }, /** * handler for when the select all checkbox is unchecked */ onNotAllChecked: function() { //iterate over all of the masscollection checkboxes and uncheck the ones that are checked. _.each(this.$('div.checkall input'), function(item) { var $item = $(item); //only trigger if the item IS checked. if ($item.prop('checked')) { $item.trigger('click'); } }); }, /** * Resets the dropdown css * @param e */ resetDropdownDelegate: function(e) { var $b = this.$(e.currentTarget).first(); $b.parent().closest('.action-button-wrapper').removeClass('open'); }, /** * Fixes z-index for dropdown * @param e */ delegateDropdown: function(e) { var $buttonGroup = this.$(e.currentTarget).first(); // add open class to parent list to elevate absolute z-index for iOS $buttonGroup.parent().closest('.action-button-wrapper').addClass('open'); }, /** * Load and cache SugarLogic dependencies for a module * * @param {Data.Bean} model * @return {Array} * @private */ _getSugarLogicDependenciesForModel: function(model) { var module = model.module; if (_.isUndefined(this.moduleDependencies[module])) { var dependencies; var moduleMetadata; //TODO: These dependencies would normally be filtered by view action. Need to make that logic // external from the Sugarlogic plugin. Probably somewhere in the SidecarExpressionContext class... // first get the module from the metadata moduleMetadata = app.metadata.getModule(module) || {}; // load any dependencies found there dependencies = moduleMetadata.dependencies || []; // now lets check the record view to see if it has any local ones on it. if (moduleMetadata.views && moduleMetadata.views.record) { var recordMetadata = moduleMetadata.views.record.meta; if (!_.isUndefined(recordMetadata.dependencies)) { dependencies = dependencies.concat(recordMetadata.dependencies); } } // cache the results so we don't have to do this expensive lookup any more this.moduleDependencies[module] = dependencies; } return this.moduleDependencies[module]; }, /** * Setup dependencies for a specific model. * * @param {Data.Bean} model * @param {Data.Collection} collection * @param {Object} options */ setupSugarLogicForModelOrCollection: function(modelOrCollection) { var slContext; var isCollection = (modelOrCollection instanceof app.data.beanCollection); var dependencies = this._getSugarLogicDependenciesForModel(modelOrCollection); if (_.size(dependencies) > 0) { slContext = new SUGAR.expressions.SidecarExpressionContext( this, isCollection ? new modelOrCollection.model() : modelOrCollection, isCollection ? modelOrCollection : false ); slContext.initialize(dependencies); var id = isCollection ? modelOrCollection.module : modelOrCollection.get('id') || modelOrCollection.cid; this.sugarLogicContexts[id] = slContext; } }, /** * Handler for when a new QLI/Note row has been added and then canceled * * @param {Data.Bean} rowModel The row collection model that was created and now canceled */ onCancelRowEdit: function(rowModel) { var rowId; if (rowModel.isNew()) { rowId = rowModel.cid; this.collection.remove(rowModel, {silent: true}); const $relatedRow = this.$('tr[name="' + rowModel.module + '_' + rowModel.cid + '"]'); $relatedRow.remove(); if (!_.isUndefined(this.sugarLogicContexts[rowId])) { // cleanup any sugarlogic contexts this.sugarLogicContexts[rowId].dispose(); } // if we're showing line numbers, and the model we canceled was a Product if (this.showLineNums && rowModel.module === 'Products') { // reset the line_num count on the collection from QuotesLineNumHelper plugin this.resetGroupLineNumbers(this.model.cid, this.collection); } } this.onNewItemChanged(); }, /** * Handles when a row is saved. Since newly added (but not saved) rows have temporary * id's assigned to them, this is needed to go back and fix row id html attributes and * also resets the rowFields with the new model's ID so rows toggle properly * * @param {Data.Bean} rowModel */ onSaveRowEdit: function(rowModel) { var modelId = rowModel.cid; var modelModule = rowModel.module; var quoteId = rowModel.get('quote_id'); var accountId = rowModel.get('account_id'); var productId = rowModel.get('id'); var quoteModel = this.context.get('parentModel'); this.toggleCancelButton(false, rowModel.cid); this.toggleRow(modelModule, modelId, false); this.onNewItemChanged(); if (quoteModel && rowModel.module === 'Products') { // when a new row is added if it does not have quote_id already, set it if (_.isEmpty(quoteId)) { quoteId = quoteModel.get('id'); app.api.relationships('create', 'Products', { id: productId, link: 'quotes', relatedId: quoteId, related: { quote_id: quoteId } }, null, { success: _.bind(this._updateFromRelationshipCall, this, true) }); } // when a new row is added if it does not have account_id already, set it if (_.isEmpty(accountId)) { accountId = quoteModel.get('billing_account_id'); if (accountId) { app.api.relationships('create', 'Products', { id: productId, link: 'account_link', relatedId: accountId, related: { account_id: accountId } }, null, { success: _.bind(this._updateFromRelationshipCall, this, false) }); } } } }, /** * Updates the item model and Quote model based on Relationship API calls * * @param {boolean} updateQuote If we should update the Quote record or not * @param {Object} response The API Data response * @private */ _updateFromRelationshipCall: function(updateQuote, response) { var record = response.record; var relatedRecord = response.related_record; var pbItems = this.model.get('product_bundle_items'); var quoteModel = this.context.get('parentModel'); _.each(pbItems.models, function(itemModel) { if (itemModel.get('id') === record.id) { itemModel.setSyncedAttributes(record); itemModel.set(record); } }, this); if (updateQuote && quoteModel) { quoteModel.setSyncedAttributes(relatedRecord); quoteModel.set(relatedRecord); } }, /** * Handles when the row is being saved but has not been saved fully yet * * @param {boolean} disableCancelBtn If we should disable the button or not * @param {string} rowModelCid The model.cid of the row that is saving */ onSavingRow: function(disableCancelBtn, rowModelCid) { // todo: SFA-4541 needs to add code in here to toggle fields to readonly this.toggleCancelButton(disableCancelBtn, rowModelCid); }, /** * Replaces the model of this view with the given one * * @param {Bean} model the new Product Bundles model to use for this view */ switchModel: function(model) { // Clear listeners attached to the old model this.stopSugarLogic(); _.each(this.sugarLogicContexts, function(slContext) { slContext.dispose(); }); this.sugarLogicContexts = {}; this.moduleDependencies = {}; // Set the new model and collection this.model = model; this.collection = this.model.get('product_bundle_items'); this.collection.on('add remove', this.onNewItemChanged, this); this.isEmptyGroup = this.collection.length === 0; this.isDefaultGroupList = this.model.get('default_group'); // Re-initialize SugarLogic for the new model and collection this.startSugarLogic(); this._setupSugarLogic(); // Update the toggledModels list to make sure the mappings use the new models var newToggledModels = {}; _.each(this.toggledModels, function(model, cid) { // Untoggle the old model, otherwise if a record was in inline edit // mode, the parent Quote record will always think it's in that mode this.context.parent.trigger('quotes:item:toggle', false, cid); var newItem = _.find(this.model.get('product_bundle_items').models, function(newModel) { return model.id === newModel.id; }); if (!_.isEmpty(newItem)) { newToggledModels[newItem.cid] = newItem; }; }, this); this.toggledModels = newToggledModels; // Reset the group line numbers this.resetGroupLineNumbers(this.model.cid, this.collection); }, /** * Toggles the cancel button disabled or not * * @param {boolean} disable If we should disable the button or not * @param {string} rowModelCid The model.cid of the row that needs its cancel button toggled */ toggleCancelButton: function(disable, rowModelCid) { var cancelBtn = _.find(this.fields, function(field) { return field.name == 'inline-cancel' && field.model.cid === rowModelCid; }); if (cancelBtn) { cancelBtn.setDisabled(disable); } }, /** * Called when a group's Create QLI or Create Note button is clicked * * @param {Data.Bean} groupModel The ProductBundle model * @param {Object} prepopulateData Any data to prepopulate the model with - coming from Opps Convert * @param {string} linkName The link name of the new item to create: products or product_bundle_notes */ onAddNewItemToGroup: function(linkName, prepopulateData) { var relatedModel = app.data.createRelatedBean(this.model, null, linkName); var quoteModel = this.context.get('parentModel'); var maxPositionModel; var position = 0; var $relatedRow; var moduleName = linkName === 'products' ? 'Products' : 'ProductBundleNotes'; var groupLineNumObj; // these quoteModel values will be overwritten if prepopulateData // already has currency_id or base_rate already set var currencyId = quoteModel.get('currency_id'); var baseRate = quoteModel.get('base_rate'); prepopulateData = prepopulateData || {}; if (this.collection.length) { this.collection.models.map((model, i) => { let modelPosition = model.get('position'); if (!_.isNumber(modelPosition)) { model.set('position', i); } }); // get the model with the highest position maxPositionModel = _.max(this.collection.models, function(model) { return model.get('position'); }); // get the position of the highest model's position and add one to it position = maxPositionModel.get('position') + 1; } // if the data has a _module, remove it if (!_.isEmpty(prepopulateData)) { delete prepopulateData._module; if (moduleName === 'Products' && prepopulateData.product_template_id) { var metadataFields = app.metadata.getModule('Products', 'fields'); // getting the fields from metadata of the module and mapping them to prepopulateData if (metadataFields && metadataFields.product_template_name && metadataFields.product_template_name.populate_list) { _.each(metadataFields.product_template_name.populate_list, function(val, key) { prepopulateData[val] = prepopulateData[key]; }, this); } } } if (this.showLineNums && relatedModel.module === 'Products') { // get the line_num count object from QuotesLineNumHelper plugin groupLineNumObj = this.getGroupLineNumCount(this.model.cid); // get the new line number to be set on modelData prepopulateData.line_num = groupLineNumObj.ct++; } // defers to prepopulateData let modelData = _.extend({ _module: moduleName, _link: linkName, position: position, currency_id: currencyId, base_rate: baseRate, assigned_user_id: app.user.id, assigned_user_name: app.user.get('full_name'), quote_id: quoteModel.get('id') }, prepopulateData); relatedModel.module = moduleName; // set a few items on the model relatedModel.set(modelData); // tell the currency field, not to set the default currency relatedModel.ignoreUserPrefCurrency = true; // this model's fields should be set to render relatedModel.modelView = 'edit'; // add model to toggledModels to be toggled next render this.toggledModels[relatedModel.cid] = relatedModel; //If related model has service duration and unit fields, //add a custom service duration field to relatedModel if (!_.isUndefined(relatedModel.fields.service_duration_value) && !_.isUndefined(relatedModel.fields.service_duration_unit)) { var durationField = { 'name': 'service_duration', 'type': 'fieldset', 'css_class': 'service-duration-field', 'label': 'LBL_SERVICE_DURATION', 'inline': true, 'show_child_labels': false, 'fields': [ relatedModel.fields.service_duration_value, relatedModel.fields.service_duration_unit, ], 'related_fields': [ 'service_start_date', 'service_end_date', 'renewable', 'service', ], }; relatedModel.fields.service_duration = durationField; } // Adding a new model to the collection without trigger the render this.collection.add(relatedModel, {silent: true}); // Get new row as DOM element by rendering template (with store the list of fields before and after) const fieldsBefore = _.keys(this.fields); let rowTemplate = Handlebars.helpers.partial('row', this, {}, { data: {}, hash: { view: this, fields: this._fields, groupId: this.model.cid, listColSpan: this.listColSpan, module: relatedModel.module, model: relatedModel, } }); const fieldsAfter = _.keys(this.fields); // Adding a new row to the group within DOM let groupHeader = this.$el.find('.quote-data-group-header'); groupHeader.length ? groupHeader.after(rowTemplate.string) : this.$el.prepend(rowTemplate.string); // Render new fields for a row const recentFields = _.difference(fieldsAfter, fieldsBefore); _.each(recentFields, field => { const fieldController = this.fields[field]; const fieldEl = this.$(`span[sfuuid=${field}]`); if (fieldController && fieldEl.length) { this._renderField(fieldController, fieldEl); } }); this._setRowFields(); this.toggleRow(relatedModel.module, relatedModel.cid, true); $relatedRow = this.$('tr[name="' + relatedModel.module + '_' + relatedModel.id + '"]'); if ($relatedRow.length) { if (this.isCreateView) { $relatedRow.addClass(this.sortableCSSClass); } else { $relatedRow.addClass(this.nonSortableCSSClass); } } this.onNewItemChanged(); }, /** * Handles updating if we should show the empty row when QLI/Notes have * been created or canceled before saving */ onNewItemChanged: function() { this.isEmptyGroup = this.collection.length === 0; this.toggleEmptyRow(this.isEmptyGroup); }, /** * Handles when this group receives a sortover event that the user * has dragged an item into this group * * @param {jQuery.Event} evt The jQuery sortover event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onSortableGroupOver: function(evt, ui) { // When entering a new group, always hide the empty row this.toggleEmptyRow(false); }, /** * Handles when this group receives a sortout event that the user has * dragged an item out of this group * * @param {jQuery.Event} evt The jQuery sortout event * @param {Object} ui The jQuery Sortable UI Object * @private */ _onSortableGroupOut: function(evt, ui) { var isSenderNull = _.isNull(ui.sender); var isSenderSameGroup = isSenderNull || ui.sender.length && ui.sender.get(0) === this.el; // if the group was originally empty, show the empty row // if the group was not empty and had more than one row in it, hide the empty row var showEmptyRow = this.isEmptyGroup; // if there is only one item in this group, and the out event happens on a group that is the line item's // original group, and the existing single row is currently hidden, // set showEmptyRow = true so we show the Click + message if (this.collection.length === 1 && isSenderSameGroup && $(ui.item.get(0)).css('display') === 'none') { showEmptyRow = true; } this.toggleEmptyRow(showEmptyRow); }, /** * Toggles showing and hiding the empty-row message row * * @param {boolean} showEmptyRow True to show the empty row, false to hide it */ toggleEmptyRow: function(showEmptyRow) { if (showEmptyRow) { this.$('.empty-row').removeClass('hidden'); } else { this.$('.empty-row').addClass('hidden'); } }, /** * @inheritdoc */ render: function() { this._super('render'); // update isEmptyGroup after render and make sure we toggle the row properly this.isEmptyGroup = this.collection.length === 0; this.toggleEmptyRow(this.isEmptyGroup); }, /** * Overriding _renderHtml to specifically place this template after the * quote data group header * * @inheritdoc */ _renderHtml: function() { var $el = this.$('tr.quote-data-group-header'); var $trs; if ($el.length) { $trs = this.$('tr.quote-data-group-list'); if ($trs.length) { // if there are already quote-data-group-list table rows remove them $trs.remove(); } $el.after(this.template(this)); } else { this.$el.html(this.template(this)); } }, /** * @inheritdoc */ _render: function() { var qliModels; this._super('_render'); // set row fields after rendering to prep if we need to toggle rows this._setRowFields(); // if this is the create view, and we're coming from Opps convert to Quote, // and we have not added the RLI models if (this.isCreateView && this.isOppsConvert && !this.addedConvertModels) { qliModels = this.context.parent.get('relatedRecords'); _.each(qliModels, function(qliModel) { this.onAddNewItemToGroup('products', qliModel.toJSON()); }, this); //be sure to set this on the parent as well so new groups don't try to do this. this.context.parent.set('addedConvertModels', true); this.addedConvertModels = true; } if (!_.isEmpty(this.toggledModels)) { _.each(this.toggledModels, function(model, modelId) { this.toggleRow(model.module, modelId, true); }, this); } }, /** * Handles when the Delete button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onEditRowBtnClicked: function(evt) { var row = this.isolateRowParams(evt); if (!row.id || !row.module) { return false; } this.toggleRow(row.module, row.id, true); }, /** * Handles when the Delete button is clicked * * @param {MouseEvent} evt The mouse click event * @private */ _onDeleteRowBtnClicked: function(evt) { var row = this.isolateRowParams(evt); if (!row.id || !row.module) { return false; } app.alert.show('confirm_delete', { level: 'confirmation', title: app.lang.get('LBL_ALERT_TITLE_WARNING') + ':', messages: [app.lang.get('LBL_ALERT_CONFIRM_DELETE')], onConfirm: _.bind(function() { app.alert.show('deleting_line_item', { level: 'info', messages: [app.lang.get('LBL_ALERT_DELETING_ITEM', 'ProductBundles')] }); this._onDeleteRowModelFromList(this.collection.get(row.id)); }, this) }); }, /** * Called when deleting a row is confirmed, this removes the model * from the collection and resets the group's line numbers * * @param {Data.Bean} deletedRowModel The model being deleted * @private */ _onDeleteRowModelFromList: function(deletedRowModel) { deletedRowModel.destroy({ success: _.bind(function() { app.alert.dismiss('deleting_line_item'); app.alert.show('deleted_line_item', { level: 'success', autoClose: true, messages: app.lang.get('LBL_DELETED_LINE_ITEM_SUCCESS_MSG', 'ProductBundles') }); }, this) }); this.layout.trigger('quotes:line_nums:reset', this.layout.groupId, this.layout.collection); }, /** * Parse out a row module and ID * * @param {MouseEvent} evt The mouse click event * @private */ isolateRowParams: function(evt) { var $ulEl = $(evt.target).closest('ul'); var rowParams = {}; if ($ulEl.length) { rowParams.module = $ulEl.data('row-module'); rowParams.id = $ulEl.data('row-model-id'); } return rowParams; }, /** * Toggle editable selected row's model fields. * * @param {string} rowModule The row model's module. * @param {string} rowModelId The row model's ID * @param {boolean} isEdit True for edit mode, otherwise toggle back to list mode. */ toggleRow: function(rowModule, rowModelId, isEdit) { var toggleModel; var $row; this.context.parent.trigger('quotes:item:toggle', isEdit, rowModelId); toggleModel = this.collection.find(function(model) { return (model.cid == rowModelId || model.id == rowModelId); }); if (isEdit) { if (_.isUndefined(toggleModel)) { // its not there any more, so remove it from the toggledModels and return out from this method delete this.toggledModels[rowModelId]; return; } else { toggleModel.modelView = 'edit'; this.toggledModels[rowModelId] = toggleModel; } } else { if (this.toggledModels[rowModelId]) { this.toggledModels[rowModelId].modelView = 'list'; } delete this.toggledModels[rowModelId]; } $row = this.$('tr[name=' + rowModule + '_' + rowModelId + ']'); $row.toggleClass('tr-inline-edit', isEdit); this.toggleFields(this.rowFields[rowModelId], isEdit); if (isEdit) { //disable drag/drop for this row $row.addClass('not-sortable'); $row.removeClass('ui-sortable'); // Since the act of toggling the fields to "edit" mode is deferred // (see toggleFields in Editable.js), SugarLogic must also be deferred // until that act is complete. Otherwise, SetValue actions cannot take // place as the fields are not yet in edit mode. _.defer(function(context, toggleModel) { context.trigger('list:editrow:fire', toggleModel); }, this.context, toggleModel); } else if ($row.hasClass('not-sortable')) { // if this is not edit mode and row still has not-sortable (from being a brand new row) // then remove the not-sortable and add the sortable classes $row.removeClass('not-sortable'); $row.addClass('sortable ui-sortable'); //since this is a new row, we also need to set the record-id attribute on the row $row.attr('record-id', toggleModel.get('id')); } }, /** * Set, or reset, the collection of fields that contains each row. * * This function is invoked when the view renders. It will update the row * fields once the `Pagination` plugin successfully fetches new records. * * @private */ _setRowFields: function() { this.rowFields = {}; _.each(this.fields, function(field) { if (field.model && field.model.cid && _.isUndefined(field.parent)) { this.rowFields[field.model.cid] = this.rowFields[field.model.cid] || []; this.rowFields[field.model.cid].push(field); } }, this); }, /** * Overriding to allow panels to come from whichever module was passed in * * @inheritdoc */ getFieldNames: function(module) { var fields = []; var panels; module = module || this.context.get('module'); if (module === 'Quotes' || module === 'Products') { panels = _.clone(this.qliListMetadata.panels); } else if (module === 'ProductBundleNotes') { panels = _.clone(this.pbnListMetadata.panels); } if (panels) { fields = _.reduce(_.map(panels, function(panel) { var nestedFields = _.flatten(_.compact(_.pluck(panel.fields, 'fields'))); return _.pluck(panel.fields, 'name').concat( _.pluck(nestedFields, 'name')).concat( _.flatten(_.compact(_.pluck(panel.fields, 'related_fields')))); }), function(memo, field) { return memo.concat(field); }, []); } fields = _.compact(_.uniq(fields)); var fieldMetadata = app.metadata.getModule(module, 'fields'); if (fieldMetadata) { // Filter out all fields that are not actual bean fields fields = _.reject(fields, function(name) { return _.isUndefined(fieldMetadata[name]); }); // we need to find the relates and add the actual id fields var relates = []; _.each(fields, function(name) { if (fieldMetadata[name].type == 'relate') { relates.push(fieldMetadata[name].id_name); } else if (fieldMetadata[name].type == 'parent') { relates.push(fieldMetadata[name].id_name); relates.push(fieldMetadata[name].type_name); } if (_.isArray(fieldMetadata[name].fields)) { relates = relates.concat(fieldMetadata[name].fields); } }); fields = _.union(fields, relates); } return fields; }, /** * Adds the left column fields */ addMultiSelectionAction: function() { var _generateMeta = function(buttons, disableSelectAllAlert) { return { 'type': 'fieldset', 'fields': [ { 'type': 'quote-data-actionmenu', 'buttons': buttons || [], 'disable_select_all_alert': !!disableSelectAllAlert } ], 'value': false, 'sortable': false }; }; var buttons = this.meta.selection.actions; var disableSelectAllAlert = !!this.meta.selection.disable_select_all_alert; this.leftColumns.push(_generateMeta(buttons, disableSelectAllAlert)); this.leftSaveCancelColumn.push({ 'type': 'fieldset', 'label': '', 'sortable': false, 'fields': [{ type: 'quote-data-editablelistbutton', label: '', tooltip: 'LBL_CANCEL_BUTTON_LABEL', name: 'inline-cancel', icon: 'sicon-close', css_class: 'btn-invisible inline-cancel ellipsis_inline' }] }); // if this is the create view, do not add a save button if (!this.isCreateView) { this.leftSaveCancelColumn[0].fields.push({ type: 'quote-data-editablelistbutton', label: '', tooltip: 'LBL_SAVE_BUTTON_LABEL', name: 'inline-save', icon: 'sicon-check-circle', css_class: 'btn-invisible inline-save ellipsis_inline' }); } }, /** * @inheritdoc */ _dispose: function() { if (this.context && this.context.parent) { this.context.parent.off('quotes:collections:all:checked', null, this); this.context.parent.off('quotes:collections:not:all:checked', null, this); } _.each(this.sugarLogicContexts, function(slContext) { slContext.dispose(); }); this._super('_dispose'); this.rowFields = null; this.sugarLogicContexts = {}; this.moduleDependencies = {}; } }) }, "quote-data-group-footer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ProductBundles.QuoteDataGroupFooterView * @alias SUGAR.App.view.views.BaseProductBundlesQuoteDataGroupFooterView * @extends View.Views.Base.View */ ({ // Quote-data-group-footer View (base) /** * The colspan value for the list */ listColSpan: 0, /** * Array of fields to use in the template */ _fields: undefined, /** * @inheritdoc */ initialize: function(options) { var groupId; options.model = options.model || options.layout.model; // +1 to colspan since there are no leftColumns in the footer this.listColSpan = options.layout.listColSpan + 1; this._super('initialize', [options]); this._fields = _.flatten(_.pluck(this.meta.panels, 'fields')); // ninjastuff this.el = this.layout.el; this.setElement(this.el); }, /** * Replaces the model of this view with the given one * * @param {Bean} model the new Product Bundles model to use for this view */ switchModel: function(model) { this.model = model; }, /** * Overriding _renderHtml to specifically place this template after the * quote data group list rows * * @inheritdoc */ _renderHtml: function() { var $els = this.$('tr.quote-data-group-list'); if ($els.length) { // get the last table row with class quote-data-group-list and place // this template after it quote-data-group-header $(_.last($els)).after(this.template(this)); } else { // the list is empty so just add the footer after the header $(this.$('tr.quote-data-group-header')).after(this.template(this)); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "quote-data-group": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ProductBundles.QuoteDataGroupLayout * @alias SUGAR.App.view.layouts.BaseProductBundlesQuoteDataGroupLayout * @extends View.Views.Base.Layout */ ({ // Quote-data-group Layout (base) /** * @inheritdoc */ tagName: 'tbody', /** * @inheritdoc */ className: 'quote-data-group', /** * The colspan value for the list */ listColSpan: 0, /** * This is the ProductBundle ID from the model set here on the component * for easier access by parent layouts */ groupId: undefined, /** * The Quote Data Group Header view added to this layout * @type View.Views.Base.ProductBundles.QuoteDataGroupHeaderView */ quoteDataGroupHeader: undefined, /** * The Quote Data Group List view added to this layout * @type View.Views.Base.ProductBundles.QuoteDataGroupListView */ quoteDataGroupList: undefined, /** * The Quote Data Group Footer view added to this layout * @type View.Views.Base.ProductBundles.QuoteDataGroupFooterView */ quoteDataGroupFooter: undefined, /** * @inheritdoc */ initialize: function(options) { if (options.model.get('default_group')) { // for the default group, we only want the quote-data-group-list component options.meta = _.clone(options.meta); options.meta.components = [{ view: 'quote-data-group-list' }]; } this._super('initialize', [options]); // set the groupID to the model ID this.groupId = this.model.cid; // Initialize the collection this._initCollection(); var listMeta = app.metadata.getView('Products', 'quote-data-group-list'); if (listMeta && listMeta.panels && listMeta.panels[0].fields) { this.listColSpan = listMeta.panels[0].fields.length; } }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:product_bundle_items', this.render, this); // listen for the currency id to change on the parent record this.context.parent.get('model').on('change:currency_id', function(model, value, options) { this.model.set({ currency_id: model.get('currency_id'), base_rate: model.get('base_rate') }); this._render(); }, this); }, /** * Sets the collection of bundle line items and sorts its models based on * their position attribute, so that line items in the group will show up * in the correct order on the Quotes worksheet * * @private */ _initCollection: function() { // Set the collection to the bundle items collection this.collection = this.model.get('product_bundle_items'); // Sort the collection by model position this.collection.comparator = function(model) { return model.get('position'); }; this.collection.sort(); }, /** * @inheritdoc */ _render: function() { // The block prevents multiple unnecessary renderings of all groups (which "freezes" the UI, // especially when there are many records in the group) when only one group is being edited. const editGroupRecId = this.layout ? this.layout.model.get('editGroupRecId') : null; if (editGroupRecId && this.model.get('id') != editGroupRecId && this.groupId != this.layout.defaultGroupId) { return; } this._super('_render'); // add the group id to the bundle level tbody this.$el.attr('data-group-id', this.groupId); this.$el.data('group-id', this.groupId); this.$el.attr('data-record-id', this.model.id); this.$el.data('record-id', this.model.id); // set the product bundle ID on all the QLI/Notes rows this.$('tr.quote-data-group-list').attr('data-group-id', this.groupId); this.$('tr.quote-data-group-list').data('group-id', this.groupId); }, /** * Adds a row model to this layout's collection and, if the row is in edit mode, it adds * the row model to the QuoteDataGroupListView's toggledModels object * * @param {Data.Bean} model The row model that needs to be added to the collection * @param {boolean} isRowInEdit Is the row currently in edit mode? */ addRowModel: function(model, isRowInEdit) { if (isRowInEdit) { this.quoteDataGroupList.toggledModels[model.cid] = model; } this.collection.add(model, { at: model.get('position') }); }, /** * Removes a row model from this layout's collection and, if the row is in edit mode, it removes * the row model from the QuoteDataGroupListView's toggledModels object * * @param {Data.Bean} model The row model that needs to be removed from the collection * @param {boolean} isRowInEdit Is the row currently in edit mode? */ removeRowModel: function(model, isRowInEdit) { var modelId; if (isRowInEdit) { modelId = model.get('id'); if (this.quoteDataGroupList.toggledModels[modelId]) { delete this.quoteDataGroupList.toggledModels[modelId]; } if (this.quoteDataGroupList.toggledModels[model.cid]) { delete this.quoteDataGroupList.toggledModels[model.cid]; } } this.collection.remove(model); }, /** * Switches the model of this group and its components * * @param {Bean} model the new Product Bundles model to use for this group */ switchModel: function(model) { // Dispose the old model this.model.dispose(); // Replace the old model and collection on the layout this.model = model; this.model.on('change:product_bundle_items', this.render, this); this._initCollection(); // Set the group ID of this group layout to be the new model's cid this.groupId = this.model.cid; // Replace the model and collection on the inner views this.quoteDataGroupList.switchModel(model); if (!this.model.get('default_group')) { this.quoteDataGroupHeader.switchModel(model); this.quoteDataGroupFooter.switchModel(model); } }, /** * Gets a reference to the QuoteDataGroupList being added to the layout * * @inheritdoc */ addComponent: function(component, def) { this._super('addComponent', [component, def]); if (component.name === 'quote-data-group-list') { this.quoteDataGroupList = component; } else if (component.name === 'quote-data-group-footer') { this.quoteDataGroupFooter = component; } else if (component.name === 'quote-data-group-header') { this.quoteDataGroupHeader = component; } }, /** * Unsets a reference to the QuoteDataGroupList being removed from the layout * * @inheritdoc */ removeComponent: function(component) { this._super('removeComponent', [component]); if (component.name === 'quote-data-group-list') { this.quoteDataGroupList = null; } else if (component.name === 'quote-data-group-footer') { this.quoteDataGroupFooter = null; } else if (component.name === 'quote-data-group-header') { this.quoteDataGroupHeader = null; } }, /** * @inheritdoc */ _dispose: function() { var model; this.quoteDataGroupHeader = null; this.quoteDataGroupList = null; this.quoteDataGroupFooter = null; if (this.context && this.context.parent && this.context.parent.has('model')) { model = this.context.parent.get('model'); model.off('change:currency_id', null, this); } this._super('_dispose'); } }) } }} , "datas": {} }, "ProductBundleNotes":{"fieldTemplates": { "base": { "quote-data-editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundleNotes.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseProductBundleNotesEditablelistbuttonField * @extends View.Fields.Base.BaseEditablelistbuttonField */ ({ // Quote-data-editablelistbutton FieldTemplate (base) extendsFrom: 'BaseEditablelistbuttonField', /** * Overriding EditablelistbuttonField's Events with mousedown instead of click */ events: { 'mousedown [name=inline-save]': 'saveClicked', 'mousedown [name=inline-cancel]': 'cancelClicked' }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (_.isUndefined(this.changed) && this.model.isNew()) { // when adding additional items to the list, causing additional renders, // this.changed gets set undefined on re-initialize, so we need to make sure // if this is an unsaved model and this.changed is undefined, that we set changed true this.changed = true; } if (this.tplName === 'edit') { this.$el.closest('.left-column-save-cancel').addClass('higher'); } else { this.$el.closest('.left-column-save-cancel').removeClass('higher'); } }, /** * Overriding and not calling parent _loadTemplate as those are based off view/actions and we * specifically need it based off the modelView set by the parent layout for this row model * * @inheritdoc */ _loadTemplate: function() { this.tplName = this.model.modelView || 'list'; if (this.view.action === 'list' && _.indexOf(['edit', 'disabled'], this.action) < 0) { this.template = app.template.empty; } else { this.template = app.template.getField(this.type, this.tplName, this.module); } }, /** * @inheritdoc */ cancelEdit: function() { if (this.isDisabled()) { this.setDisabled(false); } this.changed = false; this.model.revertAttributes(); this.view.clearValidationErrors(); // this is the only line I had to change this.view.toggleRow(this.model.module, this.model.cid, false); // trigger a cancel event across the view layout so listening components // know the changes made in this row are being reverted if (this.view.layout) { this.view.layout.trigger('editablelist:' + this.view.name + ':cancel', this.model); } }, /** * Called after the save button is clicked and all the fields have been validated, * triggers an event for * * @inheritdoc */ _save: function() { this.view.layout.trigger('editablelist:' + this.view.name + ':saving', true, this.model.cid); if (this.view.model.isNew()) { this.view.context.parent.trigger('quotes:defaultGroup:save', _.bind(this._saveRowModel, this)); } else { this._saveRowModel(); } }, /** * Saves the row's model * * @private */ _saveRowModel: function() { var self = this; var oldModelId = this.model.id || this.model.cid; var successCallback = function(model) { self.changed = false; model.modelView = 'list'; if (self.view.layout) { self.view.layout.trigger('editablelist:' + self.view.name + ':save', model, oldModelId); self._fetchParentModel(); } }; var options = { success: successCallback, error: function(model, error) { if (error.status === 409) { app.utils.resolve409Conflict(error, self.model, function(model, isDatabaseData) { if (model) { if (isDatabaseData) { successCallback(model); } else { self._save(); } } }); } }, complete: function() { // remove this model from the list if it has been unlinked if (self.model.get('_unlinked')) { self.collection.remove(self.model, {silent: true}); self.collection.trigger('reset'); self.view.render(); } else { self.setDisabled(false); } }, lastModified: self.model.get('date_modified'), //Show alerts for this request showAlerts: { 'process': true, 'success': { messages: app.lang.get('LBL_RECORD_SAVED', self.module) } }, relate: this.model.link ? true : false }; options = _.extend({}, options, this.getCustomSaveOptions(options)); this.model.save({}, options); }, /** * @inheritdoc */ _validationComplete: function(isValid) { if (!isValid) { this.setDisabled(false); return; } // also need to make sure the model.changed is empty as well if (!this.changed && !this.model.changed) { this.cancelEdit(); return; } this._save(); } }) }, "quote-data-actionmenu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundleNotes.QuoteDataActionmenuField * @alias SUGAR.App.view.fields.BaseProductBundleNotesQuoteDataActionmenuField * @extends View.Fields.Base.ActionmenuField */ ({ // Quote-data-actionmenu FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'ActionmenuField', /** * Skipping ActionmenuField's override, just returning this.def.buttons * * @inheritdoc */ _getChildFieldsMeta: function() { return app.utils.deepCopy(this.def.buttons); }, /** * Triggers massCollection events to the context.parent * * @inheritdoc */ toggleSelect: function(checked) { var event = !!checked ? 'mass_collection:add' : 'mass_collection:remove'; this.model.selected = !!checked; this.context.parent.trigger(event, this.model); } }) }, "textarea": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ProductBundleNotes.TextareaField * @alias SUGAR.App.view.fields.BaseProductBundleNotesTextareaField * @extends View.Fields.Base.BaseTextareaField */ ({ // Textarea FieldTemplate (base) extendsFrom: 'BaseTextareaField', /** * Having to override because we do want it to go to edit in the list * contrary to everywhere else in the app * * @inheritdoc */ setMode: function(name) { // skip textarea's setMode and call straight to Field.setMode app.view.Field.prototype.setMode.call(this, name); } }) } }} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Reports":{"fieldTemplates": { "base": { "drillthrough-labels": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Reports.DrillthroughLabelsField * @alias SUGAR.App.view.fields.BaseReportsDrillthroughLabelsField * @extends View.Fields.Base.BaseField */ ({ // Drillthrough-labels FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.context.on('refresh:drill:labels', this.render, this); }, /** * @override We want to grab the data from the context, not the model */ format: function(value) { var params = this.context.get('dashConfig'); var reportDef = this.context.get('reportData'); var chartModule = this.context.get('chartModule'); var filterDef = this.context.get('filterDef'); var filterFields = _.flatten(_.map(filterDef, function(filter) { return _.keys(filter); })); var groupDefs = _.filter(reportDef.group_defs, function(groupDef) { var groupField = groupDef.table_key + ':' + groupDef.name; return _.contains(filterFields, groupField); }); if (groupDefs.length > 0) { var group = SUGAR.charts.getFieldDef(groupDefs[0], reportDef); var module = group.custom_module || group.module || chartModule; this.groupName = app.lang.get(group.vname, module) + ': '; this.groupValue = params.groupLabel; } if (groupDefs.length > 1) { var series = SUGAR.charts.getFieldDef(groupDefs[1], reportDef); var module = series.custom_module || series.module || chartModule; this.seriesName = app.lang.get(series.vname, module) + ': '; this.seriesValue = params.seriesLabel; } // returns nothing return value; } }) }, "orientation-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Reports.OrientationWidgetField * @alias SUGAR.App.view.fields.BaseReportsOrientationWidgetField * @extends View.Views.Base.Field */ ({ // Orientation-widget FieldTemplate (base) events: { 'click [data-action="change-orientation"]': 'changeOrientation', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(); this._super('initialize', [options]); this._registerEvents(); }, /** * Before init properties handling * * @param {Object} options */ _beforeInit: function(options) { this.HORIZONTAL = 'horizontal'; this.VERTICAL = 'vertical'; this._orientation = this.HORIZONTAL; this.ORIENTATION_DEPENDENCY = { horizontal: this.VERTICAL, vertical: this.HORIZONTAL, }; }, /** * Register related events */ _registerEvents: function() { this.listenTo(this.context, 'report-layout-config-retrieved', this.setOrientation); this.listenTo(this.context, 'orientation-visibility-change', this.updateVisibilityByWidget); this.listenTo(this.context, 'toggle-orientation-buttons', this.updateVisibilityByWidget); }, /** * Change buttons visibility */ _updateVisibility: function() { this.$(`#${this.HORIZONTAL}`).toggleClass('active', this._orientation === this.HORIZONTAL); this.$(`#${this.VERTICAL}`).toggleClass('active', this._orientation === this.VERTICAL); }, /** * Update visibility by widget * * Updates DOM elements triggered by visiblity widget changes */ updateVisibilityByWidget: function(toggle) { const resizeConfig = this.context.get('resizeConfig') || {}; let orientationWidgetActive = false; if (resizeConfig.hidden === false) { orientationWidgetActive = true; } if (_.isBoolean(toggle) && resizeConfig.hidden === false) { orientationWidgetActive = toggle; } const horizontalWidgetEl = this.$(`#${this.HORIZONTAL}`); const verticalWidgetEl = this.$(`#${this.VERTICAL}`); if (orientationWidgetActive) { const widgetToActivate = this.$(`#${this._orientation}`); widgetToActivate.toggleClass('active', true); const widgetToDeactivate = this.$(`#${this.ORIENTATION_DEPENDENCY[this._orientation]}`); widgetToDeactivate.toggleClass('active', false); } else { horizontalWidgetEl.toggleClass('active', false); verticalWidgetEl.toggleClass('active', false); } horizontalWidgetEl.toggleClass('disabled', !orientationWidgetActive); horizontalWidgetEl.prop('disabled', !orientationWidgetActive); verticalWidgetEl.toggleClass('disabled', !orientationWidgetActive); verticalWidgetEl.prop('disabled', !orientationWidgetActive); }, /** * Change widget buttons visibility * * @param {jQuery} e */ changeOrientation: function(e) { if (e.currentTarget.id === this._orientation) { return; } this._orientation = e.currentTarget.id; const resizeConfig = this.context.get('resizeConfig'); let filtersActive = !!this.context.get('filtersActive'); if (resizeConfig && resizeConfig.filtersActive === false) { filtersActive = false; } const config = { direction: this._orientation, hidden: false, firstScreenRatio: '50%', filtersActive }; this._updateVisibility(); this.context.trigger('split-screens-config-change', config, true); this.context.trigger('split-screens-orientation-change', config); this.context.trigger('split-screens-resized', config); this.context.trigger('container-resizing'); }, /** * Set the visibility state of buttons * * @param {Object} config */ setOrientation: function(config) { this._orientation = config.direction || this.HORIZONTAL; this.updateVisibilityByWidget(); }, }) }, "drillthrough-collection-count": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * DrillthroughCollectionCountField is a field for Reports to set total in drillthrough drawer headerpane. * * @class View.Fields.Base.Reports.DrillthroughCollectionCountField * @alias SUGAR.App.view.fields.BaseReportsDrillthroughCollectionCountField * @extends View.Fields.Base.CollectionCountField */ ({ // Drillthrough-collection-count FieldTemplate (base) extendsFrom: 'CollectionCountField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'collection-count'; this.strictFetchCount = true; this._registerEvents(); }, /** * Register panel related events */ _registerEvents: function() { this.listenTo(this.context, 'drawer:reports:list:updated', this._resetCachedCount, this); }, /** * Reset total cached count each time the list has changed */ _resetCachedCount: function() { this.cachedCount = null; }, /** * @inheritdoc * * Calls ReportsApi to get collection count. */ fetchCount: function() { if (_.isNull(this.collection.total)) { app.alert.show('fetch_count', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false }); } var filterDef = this.context.get('filterDef'); var useSavedFilters = this.context.get('useSavedFilters') || false; var params = {group_filters: filterDef, use_saved_filters: useSavedFilters}; var reportId = this.context.get('reportId'); var url = app.api.buildURL('Reports', 'record_count', {id: reportId}, params); app.api.call('read', url, null, { success: _.bind(function(data) { this.collection.total = parseInt(data.record_count, 10); if (!this.disposed) { this.updateCount(); } }, this), complete: function() { app.alert.dismiss('fetch_count'); } }); }, }) }, "chart-type": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Reports.ChartTypeField * @alias SUGAR.App.view.fields.BaseReportsChartTypeField * @extends View.Fields.Base.BaseField */ ({ // Chart-type FieldTemplate (base) extendsFrom: 'BaseField', /** * The mapping for each of the chart types */ mapping: { none: 'LBL_NO_CHART', hBarF: 'LBL_HORIZ_BAR', hGBarF: 'LBL_HORIZ_GBAR', vBarF: 'LBL_VERT_BAR', vGBarF: 'LBL_VERT_GBAR', pieF: 'LBL_PIE', funnelF: 'LBL_FUNNEL', lineF: 'LBL_LINE', donutF: 'LBL_DONUT', treemapF: 'LBL_TREEMAP', }, /** * Gets the correct mapping for the DB value * * @param {string} value The value from the server * @return {string} The mapped and translated value */ format: function(value) { return app.lang.get(this.mapping[value], this.module); } }) }, "visibility-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Reports.VisibilityWidgetField * @alias SUGAR.App.view.fields.BaseReportsVisibilityWidgetField * @extends View.Views.Base.Field */ ({ // Visibility-widget FieldTemplate (base) events: { 'click [data-action="change-visibility"]': 'changeVisibility', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(); this._super('initialize', [options]); this._registerEvents(); }, /** * Before init properties handling * * @param {Object} options */ _beforeInit: function(options) { this.FILTERS = 'filters'; this.TABLE = 'table'; this.CHART = 'chart'; this.SCREENS_MAPPING = { chart: 'firstScreen', table: 'secondScreen', }; this.SCREENS_DEPENDENCY = { table: this.CHART, chart: this.TABLE, }; this._canDisplayTable = true; this._canDisplayFilters = true; this._numberOfFilters = 0; this._widgetsVisibility = { filters: { onScreen: false, interactable: false, }, table: { onScreen: false, interactable: false, }, chart: { onScreen: false, interactable: false, }, }; }, /** * Register related events */ _registerEvents: function() { this.listenTo(this.context, 'report-layout-config-retrieved', this.setVisibilityState); this.listenTo(this.context, 'split-screens-orientation-change', this.setVisibilityState); this.listenTo(this.context, 'filters-container-content-loaded', this.setVisibilityState); }, /** * Update Widgets Visibility */ _updateWidgetsVisibility: function() { _.each(this._widgetsVisibility, function updateVisibility(visibility, widgetId) { const widgetEl = this.$(`#${widgetId}`); const hasContainerFn = `_has${app.utils.capitalize(widgetId)}`; const canShowContainer = this[hasContainerFn](); widgetEl.toggleClass('active', visibility.onScreen); widgetEl.toggleClass('disabled', !visibility.interactable || !canShowContainer); widgetEl.prop('disabled', !visibility.interactable || !canShowContainer); this._updateTooltipButton(widgetEl); }, this); this._updateNumberOfFilters(); }, /** * Update the displayed number of filters */ _updateNumberOfFilters: function() { const formattedFiltersNumber = this._getNumberOfFiltersToDisplay(); const filtersBadgeEl = this.$('[data-container="filters-badge"]'); if (formattedFiltersNumber > 0 || _.isString(formattedFiltersNumber)) { filtersBadgeEl.toggleClass('hidden', false); filtersBadgeEl.text(formattedFiltersNumber); } }, /** * Update tooltip for disabled buttons * * @param {Object} widgetEl */ _updateTooltipButton: function(widgetEl) { if (this.model.get('report_type') === 'tabular') { this._updateTooltipButtonsForTabular(); return; } if (widgetEl.is(':disabled')) { widgetEl.css('pointer-events', 'none'); widgetEl.parent().attr('data-original-title', app.lang.get('LBL_ONE_VIEW_REQUIRED', this.module)); widgetEl.parent().css('cursor', 'no-drop'); } else { widgetEl.css('pointer-events', 'unset'); widgetEl.parent().css('cursor', 'pointer'); widgetEl.parent().attr('data-original-title', widgetEl.data('originalTitle')); } }, /** * Update tooltip buttons for rows and columns * * Rows and columns report don't have charts so we need to update tooltips accordingly * */ _updateTooltipButtonsForTabular: function() { let chartEl = this.$('#chart'); let tableEl = this.$('#table'); tableEl.css('pointer-events', 'none'); chartEl.css('pointer-events', 'none'); chartEl.parent().attr('data-original-title', app.lang.get('LBL_RC_NO_CHART', this.module)); chartEl.parent().css('cursor', 'no-drop'); }, /** * Get a formatted display number * * @return {string} */ _getNumberOfFiltersToDisplay: function() { const maxNumberOfFiltersDisplayed = 9; const maxNumberOfFiltersLabel = '9+'; let formattedFiltersNumber = this._numberOfFilters; if (this._numberOfFilters > maxNumberOfFiltersDisplayed) { formattedFiltersNumber = maxNumberOfFiltersLabel; } return formattedFiltersNumber; }, /** * Update the visibility State * * @param {string} widgetId * @param {Object} visibility */ _updateVisibilityState: function(widgetId, visibility) { this._widgetsVisibility[widgetId] = _.extend({}, this._widgetsVisibility[widgetId], visibility); if (widgetId === this.FILTERS) { this.context.set('filtersActive', visibility); } const dependentScreenId = this.SCREENS_DEPENDENCY[widgetId]; if (dependentScreenId) { this._widgetsVisibility[dependentScreenId].interactable = visibility.onScreen; } this._updateWidgetsVisibility(); }, /** * Check if chart is available * * @return {boolean} */ _hasChart: function() { return this.model.get('chart_type') !== 'none' && this.model.get('report_type') !== 'tabular'; }, /** * Check if table is available * * @return {boolean} */ _hasTable: function() { return this._canDisplayTable; }, /** * Check if filters is available * * @return {boolean} */ _hasFilters: function() { return this._canDisplayFilters; }, /** * Convert config to match resizable split screens config * * @return {Object} */ _getConvertedConfig: function() { let hidden = false; if (!this._widgetsVisibility[this.TABLE].onScreen) { hidden = this.SCREENS_MAPPING[this.TABLE]; } if (!this._widgetsVisibility[this.CHART].onScreen) { hidden = this.SCREENS_MAPPING[this.CHART]; } const config = { hidden, firstScreenRatio: '50%', filtersActive: this._widgetsVisibility[this.FILTERS].onScreen, direction: this.context.get('resizeConfig') ? this.context.get('resizeConfig').direction : '', }; return config; }, /** * Change widget buttons visibility * * @param {jQuery} e */ changeVisibility: function(e) { this._updateVisibilityState(e.currentTarget.id, { onScreen: !this._widgetsVisibility[e.currentTarget.id].onScreen, }); const config = this._getConvertedConfig(); this.context.trigger('split-screens-config-change', config, true); this.context.trigger('split-screens-visibility-change', config); this.context.trigger('split-screens-resized', config); this.context.trigger('orientation-visibility-change', config.hidden === false); this.context.trigger('container-resizing'); }, /** * Set the visibility state * * @param {Object} options */ setVisibilityState: function(options) { const wV = this._widgetsVisibility; const config = _.extend({}, this.context.get('resizeConfig'), options); this._numberOfFilters = config.numberOfFilters || this._numberOfFilters || 0; let filtersOnScreen = config.filtersActive || false; if (_.isUndefined(config.filtersActive) && this._numberOfFilters > 0) { filtersOnScreen = true; } this.context.set('filtersActive', filtersOnScreen); const tableOnScreen = config.hidden !== this.SCREENS_MAPPING[this.TABLE] || wV.table.onScreen; const chartOnScreen = config.hidden !== this.SCREENS_MAPPING[this.CHART] || wV.chart.onScreen; this._widgetsVisibility = { filters: { onScreen: filtersOnScreen, interactable: this._hasFilters(), }, table: { onScreen: tableOnScreen || !this._hasChart(), interactable: this._hasChart() && chartOnScreen, }, chart: { onScreen: this._hasChart() && chartOnScreen, interactable: this._hasChart() && tableOnScreen, }, }; this._updateWidgetsVisibility(); }, }) }, "shareaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Reports.ShareactionField * @alias SUGAR.App.view.fields.BaseReportsShareactionField * @extends View.Fields.Base.BaseField */ ({ // Shareaction FieldTemplate (base) /** * @inheritdoc */ _getShareParams: function(model) { var parentShareParams = this._super('_getShareParams', [model]); return _.extend({}, parentShareParams, { url: app.utils.getSiteUrl() + '#' + app.router.buildRoute('Reports', model.get('id')), }); }, }) } }} , "views": { "base": { "report-export-modal": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ReportsReportExportView * @alias SUGAR.App.view.views.BaseReportsReportExportModalView * @extends View.View */ ({ // Report-export-modal View (base) events: { 'click .close': 'closeModal', 'click .export-pdf': 'exportToPdf', 'click .export-csv': 'exportToCsv', }, /** * @inheritdoc */ initialize: function(options) { if (!this.plugins) { this.plugins = []; } if (!_.contains(this.plugins, 'ReportExport')) { this.plugins.push('ReportExport'); } this._super('initialize', [options]); }, /** * Open Export Modal */ openModal: function() { this.render(); let modalEl = this.$('[data-content=report-export-modal]'); modalEl.modal({ backdrop: 'static' }); modalEl.modal('show'); modalEl.on('hidden.bs.modal', _.bind(function handleModalClose() { this.$('[data-content=report-export-modal]').remove(); }, this)); }, /** * Close the modal and destroy it */ closeModal: function() { this.dispose(); }, /** * @inheritdoc */ _dispose: function() { this.$('[data-content=report-export-modal]').remove(); $('.modal-backdrop').remove(); this._super('_dispose'); }, }) }, "report-filters": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportFiltersView * @alias SUGAR.App.view.views.BaseReportsReportFiltersView * @extends View.Views.Base.View */ ({ // Report-filters View (base) className: 'report-filters-panel contents h-full', plugins: ['ReportsPanel'], events: { 'click [data-action="apply-runtime-filters"]': 'applyRuntimeFilters', }, /** * Before init properties */ _beforeInit: function() { this._reportData = app.data.createBean(); this._runtimeFilters = {}; this._runtimeFiltersDef = {}; this.dataType = 'filters'; this.RECORD_NOT_FOUND_ERROR_CODE = 404; this.SERVER_ERROR_CODES = [500, 502, 503, 504]; }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'runtime:filter:changed', this.runtimeFilterChanged, this); this.listenTo(this.context, 'runtime:filter:broken', this.disableApplyRuntimeFiltersButtons, this); this.listenTo(this.context, 'reset:to:default:filters', this.resetToDefaultFilters, this); this.listenTo(this.context, 'copy:filters:to:clipboard', this.copyFiltersToClipboard, this); this.listenTo(this.context, 'dashboard-filters-meta-ready', this.integrateDashboardFilters); }, /** * Reset filters to default ones */ resetToDefaultFilters: function() { this._loadReportData(true, _.bind(this._updateReportFilters, this)); }, /** * Update filters */ _updateFilters: function() { const bypassFiltersSync = this.options.bypassFiltersSync; if (bypassFiltersSync) { this._notifyFiltersChanged(); } else { this._updateReportFilters(); } }, /** * Notify filters changed */ _notifyFiltersChanged: function() { this.$('button[data-action="apply-runtime-filters"]').prop('disabled', true); this.context.trigger('reports:filters:changed', this._runtimeFiltersDef); }, /** * Update Report's Cache filters */ _updateReportFilters: function() { const reportId = this.model.get('id'); const url = app.api.buildURL('Reports/' + reportId + '/updateReportFilters'); app.api.call('create', url, { runtimeFilters: this._runtimeFiltersDef, }, { success: _.bind(this.notifyRuntimeFiltersUpdated, this), }); }, /** * Inform everyone that the filters have been updated */ notifyRuntimeFiltersUpdated: function() { this.context.trigger('runtime:filters:updated', this._runtimeFiltersDef); this.$('button[data-action="apply-runtime-filters"]').prop('disabled', true); }, /** * Deactivate filters that are already a part of a dashboard filter * * @param {Array} filtersAffected */ integrateDashboardFilters: function(filtersAffected) { _.each(this._runtimeFilters, (runtimeController) => { runtimeController.enableRuntimeFilter(); _.each(filtersAffected, (affectedFilter) => { if (runtimeController._filterData.name === affectedFilter.fieldName && runtimeController._filterData.table_key === affectedFilter.tableKey) { runtimeController.disableRuntimeFilter(); } }); }); }, /** * Copy filters text to clipboard */ copyFiltersToClipboard: function() { const textToCopy = JSON.stringify(this._runtimeFiltersDef); const successCopyAlertData = { level: 'success', messages: app.lang.get('LBL_RUNTIME_FILTERS_COPIED'), autoClose: true, }; // navigator clipboard api needs a secure context (https) if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(textToCopy); app.alert.show('runtime-filter-copied', successCopyAlertData); } else { let textarea = document.createElement('textarea'); textarea.textContent = textToCopy; textarea.style.position = 'fixed'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); app.alert.show('runtime-filter-copied', successCopyAlertData); } catch (ex) { console.warn('Copy to clipboard failed.', ex); } finally { document.body.removeChild(textarea); } } }, /** * Return the report data */ getReportData: function() { return this._reportData; }, /** * Return the runtime filters */ getRuntimeFilters: function() { return this._runtimeFiltersDef; }, /** * Return the raw runtime filters */ getRawRuntimeFilters: function() { return this._runtimeFilters; }, /** * Notify everyone that we have new filters */ applyRuntimeFilters: function() { if (this._canApplyFilters()) { this.$('.apply-filters-btn').addClass('disabled').attr('disabled', true); this._reportData.set('filtersDef', app.utils.deepCopy(this._runtimeFiltersDef)); this._updateFilters(); } else { app.alert.show('runtime-filter-invalid', { level: 'warning', messages: app.lang.get('LBL_RUNTIME_FILTERS_INVALID'), autoClose: true, }); return; } }, /** * Update filters def * * @param {Object} data */ runtimeFilterChanged: function(data) { this.$('.apply-filters-btn').removeClass('disabled').removeAttr('disabled'); this._updateFilterDefinition(this._runtimeFiltersDef, data); }, /** * Disable button */ disableApplyRuntimeFiltersButtons: function() { this.$('.apply-filters-btn').addClass('disabled').attr('disabled', true); }, /** * Check if all the filter values are valid * * @return {boolean} */ _canApplyFilters: function() { let canApply = true; _.each(this._runtimeFilters, function isValid(runtimeWidget) { if (!runtimeWidget.isValid()) { canApply = false; } }, this); return canApply; }, /** * Get the report and filters data * * @param {boolean} ignoreSavedFilters * @param {Function} callback */ _loadReportData: function(ignoreSavedFilters, callback) { const useSavedFilters = ignoreSavedFilters ? 'false' : 'true'; const reportId = this.model.get('id'); const url = app.api.buildURL('Reports/' + reportId + '/filter?use_saved_filters=' + useSavedFilters); app.api.call('read', url, null, { success: _.bind(this._storeReportData, this, callback), error: _.bind(this._failedLoadReportData, this), }); }, /** * Setup preview widget view */ _setupPreviewReportPanel: function() { this._storeReportData(false, this.context.get('previewData').filtersData); }, /** * Handle the report failed * * @param {Error} error */ _failedLoadReportData: function(error) { if (this.disposed) { return; } this._showEmptyFilters(true, true); this.context.trigger('report:data:filters:loaded', false, this.dataType); let reportModel = this.context.get('model'); if (!reportModel.get('report_type') && this.layout) { reportModel = this.layout.model; } let showErrorAlert = error && _.isString(error.message); // don't show no access alert for dashlet if (error && reportModel.get('filter') && _.has(error, 'status') && error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { showErrorAlert = false; } if (showErrorAlert) { app.alert.show('failed_to_load_report', { level: 'error', messages: error.message, autoClose: true, }); } // don't show alert for dashlets if (!reportModel.get('filter')) { const message = app.utils.tryParseJSONObject(error.responseText); let errorMessage = message ? message.error_message : error.responseText; const targetReportId = reportModel.get('id') || reportModel.get('report_id'); if (_.isEmpty(errorMessage) || error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { errorMessage = app.lang.get('LBL_NO_ACCESS', 'Reports'); } if (this.SERVER_ERROR_CODES.includes(error.status)) { errorMessage = app.lang.get('LBL_SERVER_ERROR', 'Reports'); } app.alert.show('report-data-error', { level: 'error', title: errorMessage, messages: app.lang.getModuleName('Reports') + ': ' + targetReportId, }); } this.context.set( 'permissionsRestrictedReport', error.status === this.RECORD_NOT_FOUND_ERROR_CODE ); }, /** * Setup and store report data * * @param {Object} data * @param {Function} callback */ _storeReportData: function(callback, filtersData) { if (this.disposed) { return; } if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this._failedLoadReportData({}); return; } const filterMeta = this.model.get('filter') || {}; let filtersMeta = filtersData.reportDef.filters_def; const lastStateKey = this.model.get('lastStateKey'); const customFilterMeta = this._getCustomFiltersMeta(filterMeta, lastStateKey); if (!_.isEmpty(customFilterMeta)) { filtersMeta = customFilterMeta; } const filtersDef = filtersMeta ? this._getValidFilters(filtersMeta.Filter_1) : {}; if (_.isEmpty(filtersDef)) { this._showEmptyFilters(true); this.context.trigger('report:data:filters:loaded', false, this.dataType); return; } this._reportData.set('filtersDef', filtersMeta); this._reportData.set('fullTableList', filtersData.reportDef.full_table_list); this._reportData.set('operators', filtersData.runtimeOperators); this._reportData.set('users', filtersData.users); this._runtimeFiltersDef = app.utils.deepCopy(this._reportData.get('filtersDef')); const validFilters = this._tryBuildFilters(); if (!validFilters) { this.context.trigger('report:data:filters:loaded', false, this.dataType); return; } this.context.trigger('report:data:filters:loaded', false, this.dataType); this._setTitle('LBL_REPORTS_FILTERS'); this.layout.trigger('panel:widget:finished:loading', false, false); if (this.context.get('previewMode')) { this.$('.report-filters-container :input').attr('disabled', true); } this.context.trigger('filters-loaded-successfully'); if (callback) { callback(); } }, /** * Show/Hide the filters widget * * @param {boolean} show */ _showFilters: function(show) { const emptyFiltersEl = this.$('[data-container="filters-container"]'); const applyButtonEl = this.$('[data-container="apply-button-container"]'); if (show) { emptyFiltersEl.show(); applyButtonEl.show(); } else { emptyFiltersEl.hide(); applyButtonEl.hide(); if (!this.context.get('previewMode')) { this._hideResetFilterButton(); } } }, /** * Hide Reset filter button when there are no active filters */ _hideResetFilterButton: function() { const toolbar = this.layout.getComponent('report-filters-toolbar'); if (!toolbar) { return; } const dropdownButtons = toolbar.getField('action_menu').dropdownFields; let resetBtnIdx = -1; _.each(dropdownButtons, function getResetButton(button, idx) { if (button.name === 'reset') { resetBtnIdx = idx; } }, this); dropdownButtons.splice(resetBtnIdx, 1); }, /** * Show/Hide the hidden filters widget * * @param {boolean} show */ _showHiddenFilters: function(show) { const emptyFiltersEl = this.$('[data-widget="report-hidden-filters"]'); if (show) { this.context.trigger('report:data:filters:loaded', false, this.dataType); this._showFilters(false); this._setTitle('LBL_REPORTS_FILTERS', false); emptyFiltersEl.removeClass('hidden'); } else { emptyFiltersEl.addClass('hidden'); } }, /** * Show/Hide the empty filters widget * * @param {boolean} show * @param {boolean} noAccess */ _showEmptyFilters: function(show, noAccess) { const elId = noAccess ? 'report-no-data' : 'report-no-filters'; const emptyFiltersEl = this.$(`[data-widget="${elId}"]`); this.context.trigger('report:data:filters:loaded', !show, this.dataType); this._showFilters(!show); this._setTitle('LBL_REPORTS_FILTERS', false); emptyFiltersEl.toggleClass('hidden', !show); }, /** * Show the apply filters button */ _hideAdditionalComponents: function() { this.$('[data-container="apply-button-container"]').removeClass('hidden'); }, /** * Set the report title * * @param {Mixed} title * @param {boolean} showNumberOfFilters */ _setTitle: function(title, showNumberOfFilters) { const panelToolbar = this.layout.getComponent('report-filters-toolbar'); if (_.isEmpty(panelToolbar)) { return; } const filtersLabel = title ? title : 'Filters'; let filtersTitle = app.lang.get(filtersLabel, 'Reports'); if (showNumberOfFilters) { filtersTitle = filtersTitle + ' (' + _.keys(this._runtimeFilters).length + ')'; } panelToolbar.$('.dashlet-title').text(filtersTitle); }, /** * Build filters widgets * * @return {boolean} */ _tryBuildFilters: function() { const filters = this._runtimeFiltersDef; const runtimeFilters = this._getRuntimeFilters(this._getValidFilters(filters), []); if (_.isEmpty(runtimeFilters)) { this._showHiddenFilters(true); } this._disposeFilters(); _.each(runtimeFilters, function buildFilter(runtimeFilter) { this._buildFilter(app.utils.deepCopy(runtimeFilter)); }, this); return !_.isEmpty(runtimeFilters); }, /** * Build filter widget element * * @param {Object} filterData */ _buildFilter: function(filterData) { const runtimeFilterId = filterData.runtimeFilterId; const runtimeFilterWidget = app.view.createView({ module: 'Reports', type: 'report-runtime-filter-widget', context: this.context, reportData: this._reportData, stayCollapsed: !!this.options.stayCollapsed, hideToolbar: !!this.options.hideToolbar, filterData, runtimeFilterId, }); runtimeFilterWidget.render(); this.$('[data-container="filters-container"]').append(runtimeFilterWidget.$el); this._runtimeFilters[runtimeFilterId] = runtimeFilterWidget; }, /** * Update filterDefition * * @param {Object} filters * @param {Object} runtimeFilterData */ _updateFilterDefinition: function(filters, runtimeFilterData) { let filterDef = runtimeFilterData.filterData; _.each(filters, function goThroughFilters(filter) { if (filter.operator) { this._updateFilterDefinition(filter, runtimeFilterData); } else if (filterDef.name === filter.name && filterDef.table_key === filter.table_key && runtimeFilterData.runtimeFilterId === filter.runtimeFilterId ) { _.each(filterDef, function updateValue(prop, key) { filter[key] = prop; }, this); } }, this); }, /** * Returns all the runtime filters * * @param {Object} filters * @param {Array} runtimeFilters * @return {Array} */ _getRuntimeFilters: function(filters, runtimeFilters) { _.each(filters, function goThroughFilters(filter) { if (_.isEmpty(filter.operator)) { if (filter.runtime === 1) { runtimeFilters.push(filter); filter.runtimeFilterId = app.utils.generateUUID(); } } else { const validFilters = this._getValidFilters(filter); runtimeFilters = this._getRuntimeFilters(validFilters, runtimeFilters); } }, this); return runtimeFilters; }, /** * Returns only filters, without operators * @param {Object} filter * @return {Array} */ _getValidFilters: function(filter) { return _.chain(filter) .values() .filter(function ignoreOperator(filterData) { return _.isObject(filterData); }) .value(); }, /** * Dispose runtime filters elements */ _disposeFilters: function() { _.each(this._runtimeFilters, function disposeWidget(widget) { widget.dispose(); }, this); this._runtimeFilters = {}; this.$('[data-container="filters-container"]').empty(); }, /** * @inheritdoc */ _dispose: function() { this._disposeFilters(); this._super('_dispose'); }, }) }, "matrix": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.MatrixView * @alias SUGAR.App.view.views.MatrixView */ ({ // Matrix View (base) plugins: ['ReportsPanel'], /** * Initialize helper data */ _initProperties: function() { this._matrixTypeMapping = { '2x2': { template: 'two-by-two', builder: '_buildTwoByTwoMatrix' }, '2x1': { template: 'two-by-one', builder: '_buildTwoByOneMatrix' }, '1x2': { template: 'one-by-two', builder: '_buildOneByTwoMatrix' }, }; this._matrixType = 'two-by-two'; this._matrixTable = []; this._hasData = false; this.RECORD_NOT_FOUND_ERROR_CODE = 404; this.SERVER_ERROR_CODES = [500, 502, 503, 504]; }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'runtime:filters:updated', _.bind(this._loadReportData, this, undefined)); }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.context.get('previewMode')) { this.context.trigger('report:data:table:loaded', false, 'table'); } }, /** * Setup preview widget view */ _setupPreviewReportPanel: function() { this._buildMatrix(this.context.get('previewData').tableData); this.context.trigger('report:data:table:loaded', false, 'table'); }, /** * Fetch the data to be rendered in list * */ _loadReportData: function() { const url = app.api.buildURL('Reports', 'retrieveSavedReportsRecords'); let reportModel = this.context.get('model') || this.get('model'); if (!reportModel.get('report_type') && this.layout) { reportModel = this.layout.model; } const reportId = reportModel.get('id') || reportModel.get('report_id'); const reportType = reportModel.get('report_type'); const intelligent = reportModel.get('intelligent'); let requestMeta = { record: reportId, use_saved_filters: true, intelligent, reportType, }; const listOptions = reportModel.get('list'); const lastStateKey = reportModel.get('lastStateKey'); let customMeta = this._getCustomReportMeta(listOptions, lastStateKey); requestMeta = _.extend(requestMeta, customMeta); app.api.call('create', url, requestMeta, { success: _.bind(this._buildMatrix, this), error: _.bind(this._failedLoadReportData, this), }); }, /** * Handle the report failed * * @param {Error} error */ _failedLoadReportData: function(error) { if (this.disposed) { return; } this._matrixTable = []; this._hasData = false; this.render(); this.context.trigger('report:data:table:loaded', false, 'table'); let reportModel = this.context.get('model'); if (!reportModel.get('report_type') && this.layout) { reportModel = this.layout.model; } let showErrorAlert = error && _.isString(error.message); // don't show no access alert for dashlet if (error && reportModel.get('filter') && _.has(error, 'status') && error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { showErrorAlert = false; } if (showErrorAlert) { app.alert.show('failed_to_load_report', { level: 'error', messages: error.message, autoClose: true, }); } // don't show alert for dashlets if (!reportModel.get('list')) { const message = app.utils.tryParseJSONObject(error.responseText); let errorMessage = message ? message.error_message : error.responseText; const targetReportId = reportModel.get('id') || reportModel.get('report_id'); if (_.isEmpty(errorMessage) || error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { errorMessage = app.lang.get('LBL_NO_ACCESS', 'Reports'); } if (this.SERVER_ERROR_CODES.includes(error.status)) { errorMessage = app.lang.get('LBL_SERVER_ERROR', 'Reports'); } app.alert.show('report-data-error', { level: 'error', title: errorMessage, messages: app.lang.getModuleName('Reports') + ': ' + targetReportId, }); } this.context.set( 'permissionsRestrictedReport', error.status === this.RECORD_NOT_FOUND_ERROR_CODE ); }, /** * Build Matrix Table * * @param {Object} reportData */ _buildMatrix: function(reportData) { if (_.isEmpty(reportData.layoutType)) { this.context.trigger('report:build:data:table', 'summary'); return; } if (this.disposed || _.isEmpty(reportData) || (!_.isEmpty(reportData) && _.isEmpty(reportData.data))) { this._matrixTable = []; this._hasData = false; this.render(); if (this.context) { this.context.trigger('report:data:table:loaded', false, 'table'); } return; } this._hasData = true; const matrixTypeData = this._matrixTypeMapping[reportData.layoutType]; this._matrixType = matrixTypeData.template; if (_.isFunction(this[matrixTypeData.builder])) { this[matrixTypeData.builder](reportData); } if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this._failedLoadReportData({}); return; } this.render(); this.context.trigger('report:data:table:loaded', false, 'table'); }, /** * Build the 1 by 2 matrix report table * * @param {Object} reportData */ _buildOneByTwoMatrix: function(reportData) { const headers = reportData.header; const columnsIdx = 1; let groupColumns = _.union( [_.first(_.first(headers))], headers[columnsIdx], ['Total'] ); const lastGroupColumns = _.union(_.last(reportData.header), ['Total']); const lastGroupColumnsNb = lastGroupColumns.length; const legendNb = reportData.legend.length; this._buildOneByTwoHeader(reportData, lastGroupColumns, lastGroupColumnsNb); _.each(reportData.data, function(group) { this._buildOneByTwoBody(groupColumns, legendNb, group, lastGroupColumnsNb, lastGroupColumns, true); }, this); this._buildOneByTwoBody( groupColumns, legendNb, reportData.grandTotalBottomFormatted, lastGroupColumnsNb, lastGroupColumns, false, true, true ); }, /** * Build Table header * * @param {Object} reportData * @param {Object} lastGroupColumns * @param {number} lastGroupColumnsNb */ _buildOneByTwoHeader: function(reportData, lastGroupColumns, lastGroupColumnsNb) { const secondGroupIdx = 1; const thirdGroupIdx = 2; const firstHeader = _.first(reportData.header); const secondHeader = reportData.header[secondGroupIdx]; const thirdHeader = reportData.header[thirdGroupIdx]; // add the first header this._matrixTable = [ [ { value: _.first(firstHeader), rowspan: 4, colspan: 1, bold: true, }, { value: firstHeader[secondGroupIdx], rowspan: 1, colspan: secondHeader.length * lastGroupColumnsNb, bold: true, }, { value: _.last(firstHeader), rowspan: 4, colspan: 1, bold: true, grandTotal: true, }, ], ]; this._matrixTable.push([]); // add the second header _.each(secondHeader, function(columnName) { this._matrixTable[this._matrixTable.length - 1].push({ value: columnName, colspan: lastGroupColumnsNb, rowspan: 1, bold: true, }); }, this); this._matrixTable.push([]); // add the third header for (let headerIdx = 0; headerIdx < secondHeader.length; headerIdx++) { const columnName = _.first(thirdHeader); this._matrixTable[this._matrixTable.length - 1].push({ value: columnName, colspan: lastGroupColumnsNb, rowspan: 1, bold: true, }); } this._matrixTable.push([]); // add the final header for (let headerIdx = 0; headerIdx < secondHeader.length; headerIdx++) { _.each(lastGroupColumns, function(columnName) { this._matrixTable[this._matrixTable.length - 1].push({ value: columnName, colspan: 1, rowspan: 1, bold: true, grandTotal: columnName === 'Total', }); }, this); } }, /** * Build Table body * * @param {Object} groupColumns * @param {number} legendCount * @param {Object} group * @param {number} lastGroupColumnsNb * @param {Object} lastGroupColumns * @param {boolean} skipTotal * @param {boolean} bold * @param {boolean} grandTotal */ _buildOneByTwoBody: function( groupColumns, legendCount, group, lastGroupColumnsNb, lastGroupColumns, skipTotal, bold, grandTotal) { const totalKey = 'Total'; let groupKeys = groupColumns; // go through all the data as many times as the legend's size for (let displayColumnIdx = 0; displayColumnIdx < legendCount; displayColumnIdx++) { _.each(groupKeys, function(groupKey) { const secondGroup = group[groupKey]; // check if this is the header cell if (_.isString(secondGroup)) { const cellValue = displayColumnIdx ? [] : [{ value: secondGroup, colspan: 1, rowspan: legendCount, bold: true, grandTotal, }]; this._matrixTable.push(cellValue); } else { // if not we have to go through all of the columns if (groupKey === totalKey && !skipTotal) { return; } this._buildOneByTwoGrid( secondGroup, lastGroupColumns, lastGroupColumnsNb, groupKey, skipTotal, displayColumnIdx, bold, grandTotal ); } }, this); if (!skipTotal) { groupByDataKeys = _.keys(group.Total); groupByDataValue = group.Total[groupByDataKeys[displayColumnIdx]]; this._matrixTable[this._matrixTable.length - 1].push({ value: groupByDataValue, colspan: 1, rowspan: 1, bold: true, grandTotal, }); } } }, /** * Build Table Grid * * @param {Object} secondGroup * @param {Object} lastGroupColumns * @param {number} lastGroupColumnsNb * @param {string} groupKey * @param {boolean} skipTotal * @param {boolean} bold * @param {boolean} grandTotal */ _buildOneByTwoGrid: function( secondGroup, lastGroupColumns, lastGroupColumnsNb, groupKey, skipTotal, displayColumnIdx, bold, grandTotal ) { const totalKey = 'Total'; if (_.isUndefined(secondGroup)) { secondGroup = {}; } for (let lastGroupIdx = 0; lastGroupIdx < lastGroupColumnsNb; lastGroupIdx++) { const lastGroupColumnName = lastGroupColumns[lastGroupIdx]; const lastGroup = secondGroup[lastGroupColumnName]; if (groupKey === totalKey && lastGroupColumnName !== totalKey && skipTotal) { continue; } let groupByDataKeys = []; let groupByDataValue = ''; if (!_.isUndefined(lastGroup)) { groupByDataKeys = _.keys(lastGroup); groupByDataValue = lastGroup[groupByDataKeys[displayColumnIdx]]; } this._matrixTable[this._matrixTable.length - 1].push({ value: groupByDataValue, colspan: 1, rowspan: 1, bold: bold ? bold : lastGroupColumnName === totalKey, grandTotal, }); } }, /** * Build the 2 by 1 matrix report table * * @param {Object} reportData */ _buildTwoByOneMatrix: function(reportData) { const headers = reportData.header; const columnsIdx = 1; let groupColumns = _.union( [_.first(_.first(headers))], headers[columnsIdx], ['Total'] ); const groupByColumnsNb = groupColumns.length; const lastGroupColumns = _.union(_.last(reportData.header), ['Total']); const lastGroupByColumnsNb = lastGroupColumns.length; const legendCount = reportData.legend.length; this._buildTwoByOneHeader(reportData); _.each(reportData.data, function(group, groupName) { this._buildTwoByOneBody( group, lastGroupColumns, lastGroupByColumnsNb, groupColumns, groupByColumnsNb, legendCount ); }, this); // build grand total let grandTotalKeys = _.keys(reportData.grandTotalBottomFormatted); grandTotalKeys.unshift(grandTotalKeys.pop()); for (let displayColumnIdx = 0; displayColumnIdx < legendCount; displayColumnIdx++) { this._buildTwoByOneGrandTotal(reportData, grandTotalKeys, displayColumnIdx, legendCount); } }, /** * Build Matrix Table header * * @param {Object} reportData */ _buildTwoByOneHeader: function(reportData) { const firstHeader = _.first(reportData.header); const lastHeader = _.last(reportData.header); const secondGroupIdx = 1; const thirdGroupIdx = 2; this._matrixTable = [ [ { value: _.first(firstHeader), rowspan: 2, colspan: 1, bold: true, }, { value: firstHeader[secondGroupIdx], rowspan: 2, colspan: 1, bold: true, }, { value: firstHeader[thirdGroupIdx], rowspan: 1, colspan: lastHeader.length, bold: true, }, { value: _.last(firstHeader), rowspan: 2, colspan: 1, bold: true, grandTotal: true, }, ], ]; this._matrixTable.push([]); _.each(lastHeader, function(columnName) { this._matrixTable[this._matrixTable.length - 1].push({ value: columnName, colspan: 1, rowspan: 1, bold: true, }); }, this); }, /** * Build Table Body * * @param {Object} data * @param {Object} lastGroupColumns * @param {number} lastGroupByColumnsNb * @param {number} groupByColumnsNb * @param {number} legendCount */ _buildTwoByOneBody: function( data, lastGroupColumns, lastGroupByColumnsNb, groupColumns, groupByColumnsNb, legendCount ) { let isSameRow = true; _.each(groupColumns, function(groupName) { const grandTotal = groupName === 'Total'; const group = data[groupName]; if (_.isString(group)) { const topRowSpan = (groupByColumnsNb - 1) * legendCount; this._matrixTable.push([{ value: group, colspan: 1, rowspan: topRowSpan, bold: true, }]); isSameRow = true; } else { if (isSameRow) { isSameRow = false; this._matrixTable[this._matrixTable.length - 1].push({ value: groupName, colspan: 1, rowspan: legendCount, bold: true, grandTotal, }); } else { this._matrixTable.push([{ value: groupName, colspan: 1, rowspan: legendCount, bold: true, grandTotal, }]); } this._buildTwoByOneGrid(group, lastGroupColumns, legendCount, lastGroupByColumnsNb, grandTotal); } }, this); }, /** * Build Table Grid * * @param {Object} group * @param {Object} lastGroupColumns * @param {number} legendCount * @param {number} lastGroupByColumnsNb * @param {boolean} bold */ _buildTwoByOneGrid: function(group, lastGroupColumns, legendCount, lastGroupByColumnsNb, bold) { if (_.isUndefined(group)) { group = {}; } for (let displayColumnIdx = 0; displayColumnIdx < legendCount; displayColumnIdx++) { if (displayColumnIdx) { this._matrixTable.push([]); } for (let lastGroupIdx = 0; lastGroupIdx < lastGroupByColumnsNb; lastGroupIdx++) { const lastGroupColumnName = lastGroupColumns[lastGroupIdx]; const lastGroupData = group[lastGroupColumnName]; let groupByDataKeys = []; let groupByDataValue = ''; if (!_.isUndefined(lastGroupData)) { groupByDataKeys = _.keys(lastGroupData); groupByDataValue = lastGroupData[groupByDataKeys[displayColumnIdx]]; } this._matrixTable[this._matrixTable.length - 1].push({ value: groupByDataValue, colspan: 1, rowspan: 1, bold: bold ? bold : lastGroupColumnName === 'Total', }); } } }, /** * Build Table Grand Total * * @param {Object} reportData * @param {Object} grandTotalKeys * @param {number} displayColumnIdx * @param {number} legendCount */ _buildTwoByOneGrandTotal: function(reportData, grandTotalKeys, displayColumnIdx, legendCount) { _.each(grandTotalKeys, function(columnName) { const group = reportData.grandTotalBottomFormatted[columnName]; if (_.isString(group)) { const cellValue = displayColumnIdx ? [] : [{ value: group, colspan: 2, rowspan: legendCount, bold: true, grandTotal: true, }]; this._matrixTable.push(cellValue); } else { let groupByDataKeys = []; let groupByDataValue = 0; if (!_.isUndefined(group)) { groupByDataKeys = _.keys(group); groupByDataValue = group[groupByDataKeys[displayColumnIdx]]; } if (_.isObject(groupByDataValue)) { const groupKeyValue = _.chain(groupByDataValue).keys().first().value(); groupByDataValue = groupByDataValue[groupKeyValue]; } this._matrixTable[this._matrixTable.length - 1].push({ value: groupByDataValue, colspan: 1, rowspan: 1, bold: true, grandTotal: true, }); } }, this); }, /** * Build a two group defs type of matrix report * * @param {Object} reportData */ _buildTwoByTwoMatrix: function(reportData) { const secondGroupIdx = 1; const thirdGroupIdx = 2; const firstGroupByRowsNb = 2; let completeHeader = app.utils.deepCopy(_.first(reportData.header)); completeHeader.push('Total'); const secondGroupColumns = reportData.header[secondGroupIdx]; const secondGroupByColumnsNb = secondGroupColumns.length; const grandTotalRowsNb = 2; const legendCount = reportData.legend.length; // build header this._matrixTable = [ [ { value: _.first(_.first(reportData.header)), rowspan: firstGroupByRowsNb, colspan: 1, bold: true, }, { value: _.first(reportData.header)[secondGroupIdx], rowspan: 1, colspan: secondGroupByColumnsNb, bold: true, }, { value: _.first(reportData.header)[thirdGroupIdx], rowspan: grandTotalRowsNb, colspan: 1, bold: true, grandTotal: true, }, ], _.map(secondGroupColumns, function build(value) { return { value: value, rowspan: 1, colspan: 1, bold: true, }; }) ]; // build body const headers = reportData.header; const columnsIdx = 1; let groupColumns = _.union( [_.first(_.first(headers))], headers[columnsIdx], ['Total'] ); if (_.has(reportData, 'groupColumns') && _.isArray(reportData.groupColumns)) { groupColumns = reportData.groupColumns; } _.each(reportData.data, _.bind(this._processMatrixDataGroup, this, groupColumns, legendCount)); }, /** * Process and add cells to matrix * @param {Object} columnsNames * @param {number} legendCount * @param {Object} groupByData */ _processMatrixDataGroup: function(columnsNames, legendCount, groupByData) { for (let displayColumnIdx = 0; displayColumnIdx < legendCount; displayColumnIdx++) { const grandTotal = groupByData[_.first(columnsNames)] === 'Grand Total'; _.each(columnsNames, function(columnName) { const bold = grandTotal ? grandTotal : columnName === 'Total'; this._addCellToMatrix(groupByData[columnName], legendCount, displayColumnIdx, bold, grandTotal); }, this); } }, /** * Adding a cell to the row data * * @param {Object} groupByData * @param {number} legendCount * @param {number} displayColumnIdx * @param {boolean} bold * @param {boolean} grandTotal */ _addCellToMatrix: function(groupByData, legendCount, displayColumnIdx, bold, grandTotal) { if (_.isString(groupByData)) { const cellValue = displayColumnIdx ? [] : [{ value: groupByData, colspan: 1, rowspan: legendCount, bold: true, grandTotal: grandTotal, }]; this._matrixTable.push(cellValue); } else { if (_.isUndefined(groupByData)) { this._matrixTable[this._matrixTable.length - 1].push({ value: '', colspan: 1, rowspan: 1, bold, grandTotal, }); } else { const groupByDataKeys = _.keys(groupByData); const groupByDataValue = groupByData[groupByDataKeys[displayColumnIdx]]; this._matrixTable[this._matrixTable.length - 1].push({ value: groupByDataValue, colspan: 1, rowspan: 1, bold, grandTotal, }); } } }, }) }, "report-filters-toolbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportFiltersToolbarView * @alias SUGAR.App.view.views.BaseReportsReportFiltersToolbarView * @extends View.Views.Base.Reports.ReportPanelToolbarView */ ({ // Report-filters-toolbar View (base) extendsFrom: 'ReportsReportPanelToolbarView', className: 'dashlet-header border-b border-[--border-base] dark:border-none flex flex-row items-center m-0.75', events: { 'click [data-panelaction="toggleAdvancedFilters"]': 'toggleAdvancedFilters', }, /** * @inheritdoc */ _registerEvents: function() { this._super('_registerEvents'); this.listenTo(this.context, 'button:reset:filters:click', this.resetToDefault, this); this.listenTo(this.context, 'button:copy:filters:click', this.copyFilters, this); }, /** * Render * * Update button label */ _render: function() { this._super('_render'); }, /** * Reset filters to default */ resetToDefault: function() { this.context.trigger('reset:to:default:filters'); }, /** * Copy filters def to clipboard */ copyFilters: function() { this.context.trigger('copy:filters:to:clipboard'); }, /** * Show advanced filters */ toggleAdvancedFilters: function() { this.context.trigger('toggle:advanced:filters'); }, }) }, "report-runtime-filter-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button configuration settings view * * @class View.Views.Base.AdministrationActionbuttonDisplaySettingsRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonDisplaySettingsRecordView * @extends View.View */ ({ // Report-runtime-filter-widget View (base) events: { 'change [data-fieldname="runtime-qualifier"]': 'qualifierChanged', 'change [data-fieldname="enum-single"]': 'enumSingleChanged', 'change [data-fieldname="text"]': 'textChanged', 'keyup [data-fieldname="text"]': 'textChanged', 'change [data-fieldname="text-between-start"]': 'textStartChanged', 'change [data-fieldname="text-between-end"]': 'textEndChanged', 'change [data-fieldname="time-datetime"]': 'timeDatetimeChanged', 'change [data-fieldname="time-datetime-start"]': 'timeDatetimeStartChanged', 'change [data-fieldname="time-datetime-end"]': 'timeDatetimeEndChanged', 'change [data-fieldname="select-single"]': 'selectSingleChanged', 'change [data-fieldname="enum-multiple"]': 'enumMultipleChanged', 'change [data-fieldname="select-multiple"]': 'selectMultipleChanged', 'click [data-panelaction="toggleCollapse"]': 'toggleCollapse', 'click .reports-runtime-widget-body': 'filterCollapse', 'hide': 'handleHideDatePicker', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { this._reportData = options.reportData; this._filterData = options.filterData; this._runtimeFilterId = options.runtimeFilterId; this._users = this._reportData ? this._reportData.get('users') : []; this._isEnabled = true; if (!this._users) { this._users = options.users ? options.users : []; } }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { this._stayCollapsed = this.options.stayCollapsed; this._hideToolbar = this.options.hideToolbar; this._targetModule = this._getTargetModule(); this._targetModuleLabel = this._getTargetModuleLabel(); this._targetField = this._getTargetField(); this._operators = this._getOperators(); // ugly naming so we match the bwc reports naming(easier to read) this._tempRelateController = false; this._inputType = false; this._inputData = false; this._inputValue = false; this._inputValue1 = false; this._inputValue2 = false; this._inputValue3 = false; this._rendered = false; this._searchTerm = ''; this._selectionType = { range: 'Range', caret: 'Caret', }; this._updateFilterInput(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (!this._targetField) { this.$el.empty(); app.alert.show('runtime-filter-no-permission', { level: 'warning', messages: app.lang.get('LBL_REPORT_FILTER_UNAVAILABLE', 'Reports'), }); return; } // we need to take extra care of relate fields as we need to bypass acls // all users should be able to see the runtime filters in their entirety if (this._inputType === 'relate') { this._createTempRelateController(); } this._rendered = true; this.$('.runtime-filter-select').select2(); this.$('.runtime-filter-summary-text').html(this.getSummaryText()); this._createDatePicker(); this.$('[data-type="time"]').timepicker(); const keydownTimer = 100; this.$('[data-fieldname="search-multiple"]').on( 'keydown', _.debounce( _.bind(this.searchMultipleOptions, this), keydownTimer ) ); this.$('[data-tooltip="filter-summary"]').tooltip({ delay: 1000, container: 'body', placement: 'bottom', title: _.bind(this._getTooltipText, this), html: true, trigger: 'hover', }); if (this._stayCollapsed) { this.toggleCollapse(); } if (this._isEnabled) { this.enableRuntimeFilter(); } else { this.disableRuntimeFilter(); } if (this._hideToolbar) { this.$('.dashlet-toolbar').empty(); } if (this._inputType === 'enum-multiple' || this._inputType === 'select-multiple') { this.lastItemClickedIdx = false; this.$('.report-multiselect-options .flex-row').on('click', _.debounce(_.bind(this.listItemClicked, this), 0) ); this.$('label input[name="select-all"]').on('click', _.debounce(_.bind(this.selectAllClicked, this), 0)); this.$('.report-multiselect-options input').on('change', _.debounce(_.bind(this.updateItemsCount, this), 0) ); this._setupSelectAll(); this._markSelectedItems(); this.updateItemsCount(); } }, /** * Setup select all checkbox */ _setupSelectAll: function() { const inputData = this._getInputData(); const nrOfItems = _.filter(inputData, function(item) { return item !== ''; }).length; if (this._filterData.input_name0.length === nrOfItems && nrOfItems !== 0) { this.$('input[name="select-all"]').prop('indeterminate', false); this.$('input[name="select-all"]').prop('checked', true); } else if (this._filterData.input_name0.length === 0) { this.$('input[name="select-all"]').prop('indeterminate', false); this.$('input[name="select-all"]').prop('checked', false); } else if (this._filterData.input_name0.length > 0) { this.$('input[name="select-all"]').prop('indeterminate', true); } }, /** * Get list of options used by the field * * @return {Mixed} */ _getInputData: function() { let inputData; if (this._targetField.options || this._targetField.enumOptions) { inputData = this._inputData; } else { inputData = this._users; } return inputData; }, /** * List item clicked * * @param {Event} evt */ listItemClicked: function(evt) { this.itemClickedIdx = $(evt.target).closest('.flex-row').index(); if (evt.target.tagName !== 'INPUT') { const clickEvent = $.Event('click'); if (evt.shiftKey) { clickEvent.shiftKey = true; } if (evt.isRecursive) { clickEvent.isRecursive = true; } $(evt.target).find('input').trigger(clickEvent); return; } this._itemClicked(evt); if (!evt.isRecursive) { this.lastItemClickedIdx = this.itemClickedIdx; } }, /** * Item clicked * * @param {Event} evt */ _itemClicked: function(evt) { this._markSelectedItems(); if (evt.isRecursive) { return; } if (evt.isSelectAllEvent) { this._setupSelectAll(); return; } if (evt.isSelectAllEvent) { this._automaticallyClickItems(0, this.$('.report-multiselect-options input').length); } else if (this.lastItemClickedIdx !== this.itemClickedIdx && evt.shiftKey) { this._automaticallyClickItems(this.lastItemClickedIdx, this.itemClickedIdx); } this._setupSelectAll(); }, /** * Automatically click items * * @param {int} lastItemClickedIdx * @param {int} itemClickedIdx */ _automaticallyClickItems: function(lastItemClickedIdx, itemClickedIdx) { let itemsToSelectStartIdx = lastItemClickedIdx; let itemsToSelectStopIdx = itemClickedIdx; if (lastItemClickedIdx > itemClickedIdx) { itemsToSelectStartIdx = itemClickedIdx; itemsToSelectStopIdx = lastItemClickedIdx; } let clickedCheckboxElement = this.$('.report-multiselect-options').find('input')[itemClickedIdx]; for (let itemIdx = itemsToSelectStartIdx; itemIdx <= itemsToSelectStopIdx; itemIdx++) { let checkboxElement = this.$('.report-multiselect-options').find('input')[itemIdx]; const clickEvent = $.Event('click'); clickEvent.isRecursive = true; if ($(clickedCheckboxElement).is(':checked') && !$(checkboxElement).is(':checked')) { $(checkboxElement).trigger(clickEvent); } else if (!$(clickedCheckboxElement).is(':checked') && $(checkboxElement).is(':checked')) { $(checkboxElement).trigger(clickEvent); } } }, /** * Select all handler * * @param {Event} evt */ selectAllClicked: function(evt) { const selectAllChecked = evt.currentTarget.checked; const inputData = this._getInputData(); if (selectAllChecked) { for (let itemIdx = 0; itemIdx < _.keys(inputData).length; itemIdx++) { let clickEvent = $.Event('click'); clickEvent.isSelectAllEvent = true; let checkboxElement = this.$('.report-multiselect-options').find('input')[itemIdx]; let itemKey = _.keys(inputData)[itemIdx]; let itemValue = _.values(inputData)[itemIdx]; if (!$(checkboxElement).is(':checked') && !(itemKey === '' && itemValue === '' && checkboxElement.value === '')) { $(checkboxElement).trigger(clickEvent); } } } else { for (let itemIdx = 0; itemIdx < _.keys(inputData).length; itemIdx++) { let clickEvent = $.Event('click'); clickEvent.isSelectAllEvent = true; let checkboxElement = this.$('.report-multiselect-options').find('input')[itemIdx]; if ($(checkboxElement).is(':checked')) { $(checkboxElement).trigger(clickEvent); } } } }, /** * Update items count */ updateItemsCount: function() { let newCount = this._filterData.input_name0.length; if (newCount === 0) { newCount = ''; } else { newCount = `(${newCount})`; } this.$('.items-selected-nr').html(newCount); }, /** * Marks selected items */ _markSelectedItems: function() { _.each(this.$('.report-multiselect-options').find('input'), function(input) { if (this._filterData.input_name0.indexOf(input.value) === -1) { $(input).closest('.items-center').removeClass('item-selected'); } else { $(input).closest('.items-center').addClass('item-selected'); } }, this); }, /** * Create a relate field controller */ _createTempRelateController: function() { this._disposeTempRelateController(); this._tempRelateController = app.view.createField({ def: this._targetField, view: this, viewName: 'edit', model: this._inputValue, }); // we want to bypass all acl rules // as on runtime filters the user should always be able to see the records this._tempRelateController._checkAccessToAction = function() { return true; }; this._tempRelateController.render(); this.$('[data-container="relate-input-container"]').append(this._tempRelateController.$el); }, /** * Create date picker widget */ _createDatePicker: function() { const userDateFormat = app.user.getPreference('datepref'); const options = { format: app.date.toDatepickerFormat(userDateFormat), weekStart: parseInt(app.user.getPreference('first_day_of_week'), 10), }; let datePickerElements = this.$('[data-type="date"]').datepicker(options); if (datePickerElements.length > 0) { _.each(datePickerElements, function(datePickerElement) { const calendarObject = $.datepicker._getInst(datePickerElement); calendarObject.picker.addClass('reportFilter'); }); } }, /** * Handle collapse */ toggleCollapse: function(e, forceCollapse) { const $el = this.$('.panel-toggle > i'); const collapsed = forceCollapse || $el.is('.sicon-chevron-up'); $el.toggleClass('sicon-chevron-down', collapsed); $el.toggleClass('sicon-chevron-up', !collapsed); this.$('[data-container="input-container"]').toggleClass('hide', collapsed); this.$('[data-container="operators-container"]').toggleClass('hide', collapsed); this.$el.toggleClass('collapsed-widget', collapsed); if (e) { e.stopPropagation(); } }, /** * Handle runtime filter collapse */ filterCollapse: function(e) { const currentTarget = $(e.target); let shouldCollapse = currentTarget.children().attr('data-tooltip') === 'filter-summary' || currentTarget.closest('[data-tooltip="filter-summary"]').length || currentTarget.closest('.collapsed-widget').length; let selection = window.getSelection(); if (shouldCollapse && selection.type !== this._selectionType.range) { this.toggleCollapse(e); } }, /** * Handle update of qualifier * * @param {UIEvent} e */ qualifierChanged: function(e) { this._filterData.qualifier_name = e.currentTarget.value; this._filterData.input_name0 = ''; this._filterData.input_name1 = ''; this._filterData.input_name2 = ''; this._filterData.input_name3 = ''; this._refreshWidget(); this.render(); }, /** * Date picker doesn't trigger a `change` event whenever the date value * changes we need to override this method and listen to the `hide` event. */ handleHideDatePicker: function() { if (this._filterData.qualifier_name === 'between_dates') { this._betweenDatesChanged(); } else if (this._filterData.qualifier_name === 'between_datetimes') { this._betweenDateTimesChanged(); } else if (this._targetField.type === 'datetimecombo') { this.dateDatetimeChanged(); } else { this.dateChanged(); } }, /** * Handle enum changed * * @param {UIEvent} e */ enumSingleChanged: function(e) { this._filterData.input_name0 = [e.currentTarget.value]; this._refreshWidget(); }, /** * Handle text changed * * @param {UIEvent} e */ textChanged: function(e) { this._filterData.input_name0 = e.currentTarget.value; this._refreshWidget(); }, /** * Handle date changed */ dateChanged: function() { const dateEl = this.$('[data-fieldname="date"]'); const date = dateEl.val(); if (this._isValidDatePickerFormat(date)) { this._filterData.input_name0 = this._toFilterDate(date, false); dateEl.toggleClass('error', false); this._refreshWidget(); } else { dateEl.toggleClass('error', true); this._showDatePrefMissmatchAlert(); } }, /** * Handle between dates changed */ _betweenDatesChanged: function() { this._updateBetweenDateTimes({ afterDateId: 'date-between-start', beforeDateId: 'date-between-end', afterDateKey: 'input_name0', beforeDateKey: 'input_name1', }, false); }, /** * Handle between dates and times changed */ _betweenDateTimesChanged: function() { this._updateBetweenDateTimes({ afterDateId: 'date-datetime-start', beforeDateId: 'date-datetime-end', afterDateKey: 'input_name0', beforeDateKey: 'input_name2', }, true); }, /** * Update both dates of a between operator * * @param {Object} datesMeta * @param {boolean} useTime */ _updateBetweenDateTimes: function(datesMeta, useTime) { const afterEl = this.$(`[data-fieldname="${datesMeta.afterDateId}"]`); const beforeEl = this.$(`[data-fieldname="${datesMeta.beforeDateId}"]`); let after = afterEl.val(); let before = beforeEl.val(); let canRefreshWidget = true; if (after && this._isValidDatePickerFormat(after)) { this._filterData[datesMeta.afterDateKey] = this._toFilterDate(after, useTime); afterEl.toggleClass('error', false); } else if (after) { afterEl.toggleClass('error', true); canRefreshWidget = false; this._showDatePrefMissmatchAlert(); } if (before && this._isValidDatePickerFormat(before)) { this._filterData[datesMeta.beforeDateKey] = this._toFilterDate(before, useTime, 'input_name3'); beforeEl.toggleClass('error', false); } else if (before) { beforeEl.toggleClass('error', true); canRefreshWidget = false; this._showDatePrefMissmatchAlert(); } if (canRefreshWidget) { this._refreshWidget(); } }, /** * Handle date datetime changed */ dateDatetimeChanged: function() { const dateEl = this.$('[data-fieldname="date-datetime"]'); const val = dateEl.val(); if (this._isValidDatePickerFormat(val)) { this._filterData.input_name0 = this._toFilterDate(val, true); dateEl.toggleClass('error', false); this._refreshWidget(); } else { dateEl.toggleClass('error', true); this._showDatePrefMissmatchAlert(); } }, /** * Show date pref missmatch alert */ _showDatePrefMissmatchAlert: function() { const dateFormatMapping = { 'Y-m-d': 'YYYY-MM-DD', 'm-d-Y': 'MM-DD-YYYY', 'd-m-Y': 'DD-MM-YYYY', 'Y/m/d': 'YYYY/MM/DD', 'm/d/Y': 'MM/DD/YYYY', 'd/m/Y': 'DD/MM/YYYY', 'Y.m.d': 'YYYY.MM.DD', 'd.m.Y': 'DD.MM.YYYY', 'm.d.Y': 'MM.DD.YYYY', }; let userFormat = app.user.getPreference('datepref'); const displayFormat = dateFormatMapping[userFormat] || userFormat; app.alert.show('date-format-error', { level: 'warning', messages: app.lang.get('LBL_RUNTIME_FILTER_DATE_PREF_MISSMATCH') + displayFormat, }); this.context.trigger('runtime:filter:broken'); }, /** * Handle time datetime changed * * @param {UIEvent} e */ timeDatetimeChanged: function(e) { this._changeFilterTime('input_name0', 'input_name1', e.currentTarget.value); }, /** * Handle time datetime start changed * * @param {UIEvent} e */ timeDatetimeStartChanged: function(e) { this._changeFilterTime('input_name0', 'input_name1', e.currentTarget.value); }, /** * Handle time datetime end changed * * @param {UIEvent} e */ timeDatetimeEndChanged: function(e) { this._changeFilterTime('input_name2', 'input_name3', e.currentTarget.value); }, /** * Update the time fragment of a datetime filter * * @param {string} dateKey * @param {string} timeKey * @param {string} newValue */ _changeFilterTime: function(dateKey, timeKey, newValue) { const timeFragmentIdx = 1; const datetimeFragments = this._filterData[dateKey].split(' '); const timeFragment = datetimeFragments[timeFragmentIdx]; const time = app.date(newValue, ['h:mma']).format('HH:mm:ss'); this._filterData[dateKey] = this._filterData[dateKey].replace(timeFragment, time); this._filterData[timeKey] = newValue; this._refreshWidget(); }, /** * Handle text start changed * * @param {UIEvent} e */ textStartChanged: function(e) { this._filterData.input_name0 = e.currentTarget.value; this._refreshWidget(); }, /** * Handle text end changed * * @param {UIEvent} e */ textEndChanged: function(e) { this._filterData.input_name1 = e.currentTarget.value; this._refreshWidget(); }, /** * Handle select single changed * * @param {UIEvent} e */ selectSingleChanged: function(e) { this._filterData.input_name0 = [e.currentTarget.value]; this._refreshWidget(); }, /** * Handle enum multiple changed * * @param {UIEvent} e */ enumMultipleChanged: function(e) { const value = e.currentTarget.value; const insert = e.currentTarget.checked; if (!_.isArray(this._filterData.input_name0)) { this._filterData.input_name0 = []; } if (insert) { this._filterData.input_name0.push(value); } else { this._filterData.input_name0 = _.without(this._filterData.input_name0, value); if (_.isArray(this._filterData.input_name0) && this._filterData.input_name0.length === 0) { this._filterData.input_name0 = ''; } } this._refreshWidget(); }, /** * Handle select multiple changed * * @param {UIEvent} e */ selectMultipleChanged: function(e) { const value = e.currentTarget.value; const insert = e.currentTarget.checked; if (!_.isArray(this._filterData.input_name0)) { this._filterData.input_name0 = []; } if (insert) { this._filterData.input_name0.push(value); } else { this._filterData.input_name0 = _.without(this._filterData.input_name0, value); if (_.isArray(this._filterData.input_name0) && this._filterData.input_name0.length === 0) { this._filterData.input_name0 = ''; } } this._refreshWidget(); }, /** * Build tooltip description text * * @return {string} */ _getTooltipText: function() { const tables = this._reportData.get('fullTableList'); const targetTable = tables[this._filterData.table_key]; const tableHierarchy = targetTable.name ? targetTable.name : app.lang.getModuleName(targetTable.value, { plural: true, }); const fieldLabel = app.lang.get(this._targetField.vname, this._targetModule); const summaryText = this.getSummaryText(); // tooltip text has to be html so we can meet the UX mocks const title = '<div class="runtime-filter-summary-tooltip">' + tableHierarchy.replace(/\s\s+/g, ' ') + ' ><b> ' + fieldLabel + '</b><br>' + summaryText + '</div>'; return title; }, /** * Refresh UI */ _refreshWidget: function() { this._initProperties(); this._stayCollapsed = false; this.$('.runtime-filter-summary-text').html(this.getSummaryText()); this.notifyRuntimeFilterChange(); }, /** * Notify every change the filter has supported */ notifyRuntimeFilterChange: function() { this.context.trigger('runtime:filter:changed', { filterData: this._filterData, runtimeFilterId: this._runtimeFilterId, }); }, /** * Checks if the values are valid * * @return {boolean} */ isValid: function() { const data = this._filterData; const fieldTypesRequiringDates = [ 'date', 'date-between', 'datetime-between', 'after', 'before', 'on', 'datetimecombo' ]; if (_.contains(fieldTypesRequiringDates, this._inputType)) { let requiredValuesAreGiven; if (this._inputType === 'datetime-between') { requiredValuesAreGiven = data.input_name0 && data.input_name1 && data.input_name2 && data.input_name3; } else if (this._inputType === 'date-between' || this._inputType === 'datetimecombo') { requiredValuesAreGiven = data.input_name0 && data.input_name1; } else { requiredValuesAreGiven = !_.isEmpty(data.input_name0); } if (!requiredValuesAreGiven) { return false; } if (!_.isEmpty(data.input_name0) && !this._isValidDatePickerFormat(this._inputValue)) { return false; } if (!_.isEmpty(data.input_name1) && (this._inputType === 'date-between' && !this._isValidDatePickerFormat(this._inputValue1)) || (this._inputType === 'datetime-between' && !this._isValidTimePickerFormat(this._inputValue1))) { return false; } if (!_.isEmpty(data.input_name2) && this._inputType !== 'datetimecombo' && !this._isValidDatePickerFormat(this._inputValue2)) { return false; } if (!_.isEmpty(data.input_name3) && (this._inputType === 'date-between' && !this._isValidDatePickerFormat(this._inputValue3)) || (this._inputType === 'datetime-between' && !this._isValidTimePickerFormat(this._inputValue3))) { return false; } } if (!this._inputType || this._inputType === 'empty') { return true; } return (data.input_name0 || data.input_name1 || data.input_name2 || data.input_name3); }, /** * Is valid date picker format * * @param {string} date * * @return {boolean} */ _isValidDatePickerFormat: function(date) { const dateFormatMapping = { 'Y-m-d': 'YYYY-MM-DD', 'm-d-Y': 'MM-DD-YYYY', 'd-m-Y': 'DD-MM-YYYY', 'Y/m/d': 'YYYY/MM/DD', 'm/d/Y': 'MM/DD/YYYY', 'd/m/Y': 'DD/MM/YYYY', 'Y.m.d': 'YYYY.MM.DD', 'd.m.Y': 'DD.MM.YYYY', 'm.d.Y': 'MM.DD.YYYY', }; const formattedDate = _.first(date.split(' ')); const userFormat = app.user.getPreference('datepref'); const acceptedFormat = dateFormatMapping[userFormat] || userFormat; return moment(formattedDate, acceptedFormat, true).isValid(); }, /** * Is valid time picker format * * @param {string} time * @return {boolean} */ _isValidTimePickerFormat: function(time) { const timeRegex = /^(?:(?:0?\d|1[0-2]):[0-5]\d)$/; const meridianLength = 2; const validTime = time.substring(0, time.length - meridianLength); return timeRegex.test(validTime); }, /** * Disable the widget */ disableRuntimeFilter: function() { this._isEnabled = false; this.$el.css({ opacity: 0.5, }); this.$('.reports-runtime-widget-body').css({ 'pointer-events': 'none', }); this.$('.reports-runtime-widget').attr('rel', 'tooltip'); this.$('[data-tooltip="runtime-filter-widget-container"]').tooltip({ delay: 100, container: 'body', placement: 'bottom', title: 'This filter is already being used in a dashboard filter.', trigger: 'hover', }); this.toggleCollapse(false, true); }, /** * Enable the widget */ enableRuntimeFilter: function() { this._isEnabled = true; this.$el.css({ opacity: 1, }); this.$('.reports-runtime-widget-body').css({ 'pointer-events': '', }); this.$('.reports-runtime-widget').removeAttr('rel'); }, /** * Handle search param changed * * @param {UIEvent} e */ searchMultipleOptions: function(e) { this._searchTerm = e.currentTarget.value; this._inputValue = {}; this._inputValue1 = {}; const data = (this._targetField.options || this._targetField.enumOptions) ? this._inputData : this._users; _.each(this._filterData.input_name0, function getOptions(option) { const insensitiveSearchTerm = this._searchTerm.toLocaleLowerCase(); const insensitiveOption = option.toLocaleLowerCase(); if (!_.isEmpty(option) && (insensitiveOption.includes(insensitiveSearchTerm) || (!_.isEmpty(data[option]) && data[option].toLocaleLowerCase().includes(insensitiveSearchTerm)))) { this._inputValue[option] = data[option]; } }, this); _.each(data, function getOptions(option, key) { const insensitiveSearchTerm = this._searchTerm.toLocaleLowerCase(); const insensitiveOption = option.toLocaleLowerCase(); if (!_.has(this._inputValue, key) && (insensitiveOption.includes(insensitiveSearchTerm) || (!_.isEmpty(data[option]) && data[option].toLocaleLowerCase().includes(insensitiveSearchTerm)))) { this._inputValue1[key] = option; } }, this); // we don't want the filter widget to collapse when we search for terms // so we keep the state, set it to false, and put the state back after render const stayCollapsed = this._stayCollapsed; this._stayCollapsed = false; this.render(); this._stayCollapsed = stayCollapsed; const input = this.$('[data-fieldname="search-multiple"]'); input.focus().val('').val(this._searchTerm); }, /** * Handle relate field changed */ relateChanged: function(model) { this._filterData.input_name0 = model.get(this._targetField.id_name); this._filterData.input_name1 = model.get(this._filterData.name); this._refreshWidget(); }, /** * Returns the filter's target module * * @return {Mixed} */ _getTargetModule: function() { const tableKey = this._filterData ? this._filterData.table_key : false; const tables = this._reportData ? this._reportData.get('fullTableList') : false; const targetModule = tables ? tables[tableKey].module : false; return targetModule; }, /** * Returns the filter's target module label * * @return {Mixed} */ _getTargetModuleLabel: function() { const tableKey = this._filterData ? this._filterData.table_key : false; const tables = this._reportData ? this._reportData.get('fullTableList') : false; const targetModule = tables ? app.lang.getModuleName(tables[tableKey].label, { plural: true, }) : false; return targetModule; }, /** * Returns the filter's target field * * @return {Mixed} */ _getTargetField: function() { if (!this._targetModule) { return false; } if (this._targetField && this._targetField.type === 'enum' && this._targetField.function) { return this._targetField; } const targetField = app.utils.deepCopy( app.metadata.getField({ module: this._targetModule, name: this._filterData.name, }) ); if (!targetField) { return false; } const reportId = this.model.get('id'); if (targetField.type === 'enum' && targetField.function && reportId) { const url = app.api.buildURL('Reports/' + reportId + '/retrieveEnumFieldOptions'); app.api.call('create', url, { targetField: targetField.name, targetModule: this._targetModule, }, { success: _.bind(this._updateTargetField, this), }); } return targetField; }, /** * Update the target field's options enum * * @param {Array} options */ _updateTargetField: function(options) { if (this.disposed) { return; } options = _.extend({'': ''}, options); const qualifier = this._filterData ? this._filterData.qualifier_name : ''; this._targetField.enumOptions = options; if (_.contains(['empty', 'not_empty'], qualifier)) { return; } this._updateEnumFilterData(qualifier); if (this._rendered) { this.render(); } }, /** * Returns all the operators available for this filter type * * @return {Mixed} */ _getOperators: function() { let allOperators = this._reportData ? this._reportData.get('operators') : false; if (!allOperators) { allOperators = this.options.operators ? this.options.operators : false; } return allOperators ? allOperators[this._targetField.type] : false; }, /** * Returns filter summary text * * @return {string} */ getSummaryText: function() { let filterValue = this._filterData.input_name0; let filterValue2 = this._filterData.input_name1; const qualifierName = this._filterData.qualifier_name; if (_.isUndefined(filterValue) || filterValue === 'undefined') { filterValue = ''; } if (_.isUndefined(filterValue2) || filterValue2 === 'undefined') { filterValue2 = ''; } let prefixLabel = _.filter(this._operators, function getOperator(label, type) { return type === this._filterData.qualifier_name; }, this); let prefixEl = document.createElement('i'); prefixEl.innerText = app.lang.get(_.first(prefixLabel), 'Reports'); let prefix = prefixEl.outerHTML; if (qualifierName === filterValue) { return prefix; } if (_.isArray(filterValue)) { if (!this._targetField.options && !this._targetField.enumOptions) { const translatedValues = []; _.each(filterValue, function translateLabels(value) { translatedValues.push(this._users[value] || value); }, this); filterValue = translatedValues; } else if (_.isString(this._targetField.options)) { const options = app.lang.getAppListStrings(this._targetField.options); filterValue = _.map(filterValue, function(value) { return options[value]; }, this); } else if (this._targetField.enumOptions) { let filterValueLabels = []; _.each(filterValue, function getLabels(filterValueKey) { const filterValueLabel = this._targetField.enumOptions[filterValueKey]; if (filterValueLabel) { filterValueLabels.push(filterValueLabel); } }, this); filterValue = filterValueLabels; } return prefix + ' ' + _.escape(_.sortBy(filterValue).join(', ')); } const fieldTypesRequiringDates = [ 'date', 'date-between', 'datetime-between', 'after', 'before', 'on', 'datetimecombo' ]; if (_.contains(fieldTypesRequiringDates, this._inputType)) { filterValue = this._toDisplayDate(filterValue); } if (_.contains(['text-between', 'date-between'], this._inputType)) { return prefix + ' ' + _.escape(filterValue + ' ' + app.lang.get('LBL_AND') + ' ' + this._inputValue1); } if (_.contains(['datetimecombo'], this._inputType)) { return prefix + ' ' + _.escape(this._inputValue + ' ' + this._inputValue1); } if (_.contains(['datetime-between'], this._inputType)) { return prefix + ' ' + _.escape(this._inputValue + ' ' + this._inputValue1 + ' ' + app.lang.get('LBL_AND') + ' ' + this._inputValue2 + ' ' + this._inputValue3); } return prefix + ' ' + _.escape((this._inputType === 'relate' ? filterValue2 : filterValue)); }, /** * Setup proper input depending on qulifier type */ _updateFilterInput: function() { const qualifierName = this._filterData ? this._filterData.qualifier_name : ''; const fieldType = this._targetField ? this._targetField.type : ''; this._inputValue = false; this._inputValue1 = false; this._inputValue2 = false; this._inputValue3 = false; // it is ugly, unfortunately we had to keep the logic of the bwc reports when deciding what input type to render if (qualifierName === 'anything') { this._inputType = false; } else if (qualifierName === 'between') { this._inputType = 'text-between'; this._inputValue = this._filterData.input_name0; this._inputValue1 = this._filterData.input_name1; } else if (qualifierName === 'between_dates') { this._updateBetweenDatesFilterData(); } else if (qualifierName === 'between_datetimes') { this._updateBetweenDatetimesFilterData(); } else if (qualifierName.indexOf('_n_days') != -1) { this._inputType = 'text'; this._inputValue = this._filterData.input_name0; } else if (_.contains(['empty', 'not_empty'], qualifierName)) { this._inputType = false; } else if (_.contains(['date', 'datetime', 'service-enddate'], fieldType)) { this._updateDatetimeFilterData(qualifierName); } else if (fieldType === 'datetimecombo') { this._updateDatetimecomboFilterData(qualifierName); } else if (_.contains(['id', 'name', 'fullname', 'relate'], fieldType)) { this._updateNameFilterData(qualifierName); } else if (_.contains(['username', 'assigned_user_name'], fieldType)) { this._updateUsernameFilterData(qualifierName); } else if (_.contains( ['enum', 'multienum', 'parent_type', 'radioenum', 'timeperiod', 'currency_id'], fieldType) ) { this._updateEnumFilterData(qualifierName); } else if (fieldType === 'bool') { this._inputType = 'enum-single'; this._inputData = { yes: 'yes', no: 'no' }; let _inputValue = _.first(this._filterData.input_name0); if (!_inputValue) { _inputValue = 'yes'; this._filterData.input_name0 = [_inputValue]; } this._inputValue = _inputValue; } else { this._inputType = 'text'; this._inputValue = this._filterData ? this._filterData.input_name0 : ''; } }, /** * Update date filter data */ _updateBetweenDatesFilterData: function() { this._inputType = 'date-between'; this._inputValue = this._toDisplayDate(this._filterData.input_name0); this._inputValue1 = this._toDisplayDate(this._filterData.input_name1); }, /** * Update datetime filter data */ _updateBetweenDatetimesFilterData: function() { this._inputType = 'datetime-between'; this._inputValue = this._toDisplayDate(this._filterData.input_name0); this._inputValue1 = this._filterData.input_name1; this._inputValue2 = this._toDisplayDate(this._filterData.input_name2); this._inputValue3 = this._filterData.input_name3; }, /** * Update filter data * * @param {string} qualifierName */ _updateDatetimeFilterData: function(qualifierName) { this._setDatetimeFilterData(qualifierName, 'date', false); }, /** * Update filter data * * @param {string} qualifierName */ _updateDatetimecomboFilterData: function(qualifierName) { this._setDatetimeFilterData(qualifierName, 'datetimecombo', true); }, /** * Update input values of a date/datetime type * * @param {string} qualifierName * @param {string} dateType * @param {boolean} useTime */ _setDatetimeFilterData: function(qualifierName, dateType, useTime) { if (qualifierName.indexOf('tp_') === 0) { this._inputType = 'empty'; } else { this._inputType = dateType; this._inputValue = this._toDisplayDate(this._filterData.input_name0); if (useTime) { this._inputValue1 = this._filterData.input_name1; } } }, /** * Transform user date into filter date * * @param {string} userDate * @param {boolean} useTime * @param {string} timeKey * * @return {string} */ _toFilterDate: function(userDate, useTime, timeKey) { timeKey = timeKey || 'input_name1'; const userDatePref = app.user.getPreference('datepref').toUpperCase(); const unformattedDateTime = `${userDate} ${this._filterData[timeKey]}`; const tempFormat = `${userDatePref} h:mma`; const date = moment(unformattedDateTime, tempFormat); return useTime ? date.format('YYYY-MM-DD HH:mm:ss') : date.format('YYYY-MM-DD'); }, /** * Transform filter date into user date * * @param {string} filterDate * * @return {string} */ _toDisplayDate(filterDate) { let dateFragments = filterDate.match(/([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})/); const yearIdx = 1; const monthIdx = 2; const dayIdx = 3; if (!dateFragments) { return filterDate; } let displayDate = app.user.getPreference('datepref'); displayDate = displayDate.replace('Y', dateFragments[yearIdx]); displayDate = displayDate.replace('m', dateFragments[monthIdx]); displayDate = displayDate.replace('d', dateFragments[dayIdx]); return displayDate; }, /** * Update filter data * * @param {string} qualifierName */ _updateNameFilterData: function(qualifierName) { if (_.contains(['is', 'is_not'], qualifierName)) { this._inputType = 'relate'; this._targetField.type = 'relate'; this._targetField.id_name = 'id_relate_reports'; this._targetField.module = this._targetField.ext2 ? this._targetField.ext2 : this._targetModule; this._inputValue = app.data.createBean(this._targetField.module); this._inputValue.set(this._targetField.id_name, this._filterData.input_name0); this._inputValue.set(this._targetField.name, this._filterData.input_name1); this.listenTo(this._inputValue, 'change:' + this._targetField.name, this.relateChanged, this); } else { this._inputType = 'text'; this._inputValue = this._filterData.input_name0; } }, /** * Update filter data * * @param {string} qualifierName */ _updateUsernameFilterData: function(qualifierName) { if (_.contains(['one_of', 'not_one_of'], qualifierName)) { this._inputType = 'select-multiple'; this._inputValue = {}; this._inputValue1 = {}; _.each(this._filterData.input_name0, function getOptions(option) { this._inputValue[option] = this._users[option]; }, this); _.each(this._users, function getOptions(option, key) { if (!_.has(this._inputValue, key)) { this._inputValue1[key] = option; } }, this); } else if (_.contains(['is', 'is_not', 'reports_to'], qualifierName) && _.isEmptyValue(this._filterData.input_name0)) { this._inputType = 'select-single'; this._inputValue = _.first(Object.keys(this._users)); this._filterData.input_name0 = [this._inputValue]; } else { this._inputType = 'select-single'; this._inputValue = _.first(this._filterData.input_name0); } }, /** * Update filter data * * @param {string} qualifierName */ _updateEnumFilterData: function(qualifierName) { const optionsList = this._targetField.options; this._inputData = app.lang.getAppListStrings(optionsList); if (!_.isEmpty(this._targetField.enumOptions)) { this._inputData = this._targetField.enumOptions; } if (!_.isEmpty(this._inputData)) { delete this._inputData['']; } if (qualifierName === 'anything') { this.inputData = []; return; } if (_.contains(['one_of', 'not_one_of'], qualifierName)) { this._inputType = 'enum-multiple'; this._inputValue = {}; this._inputValue1 = {}; _.each(this._filterData.input_name0, function getOptions(option) { this._inputValue[option] = this._inputData[option]; }, this); _.each(this._inputData, function getOptions(option, key) { if (!_.has(this._inputValue, key) && option) { this._inputValue1[key] = option; } }, this); } else { this._inputType = 'enum-single'; const choices = this._filterData.input_name0; this._inputValue = _.first(choices); if (!this._inputValue) { const firstEnumChoice = _.chain(this._inputData).keys().first().value(); this._inputValue = firstEnumChoice; this._filterData.input_name0 = [this._inputValue]; } } }, /** * Dispose the relate field controller */ _disposeTempRelateController: function() { if (this._tempRelateController) { this._tempRelateController.dispose(); this._tempRelateController = false; } }, /** * @inheritdoc */ _dispose: function() { this._disposeTempRelateController(); this._super('_dispose'); }, }) }, "report-table": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.ReportTableView * @alias SUGAR.App.view.views.ReportsReportTableView * @extends View.Views.Base.View */ ({ // Report-table View (base) /** * Map used in rendering data tables */ _dataTableMap: { 'tabular': 'rows-columns', 'detailed_summary': 'summation-details', 'summary': 'summation', 'Matrix': 'matrix', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Init Properties */ _initProperties: function() { this._dataTable = null; this._viewTypeMappingTable = { 'tabular': 'rows-columns', 'summation': 'summation', 'detailed_summary': 'summation-details', }; this._customCssClasses = this.model.get('customCssClasses'); this._initModelProperties(); }, /** * Init data from model */ _initModelProperties: function(forceRender) { this.reportType = 'tabular'; if (this.context.get('previewMode')) { this.reportType = this.context.get('previewData').reportType; } else if (this.model && this.model.get('report_type')) { this.reportType = this.model.get('report_type'); } }, /** * Register events */ _registerEvents: function() { if (_.has(this, 'layout') && _.has(this.layout, 'layout')) { this.listenTo(this.layout.layout, 'panel:collapse', this._collapseTable, this); } this.listenTo(this.context, 'report:build:data:table', this.rebuildDataTableList, this); }, /** * @inheritdoc */ render: function() { this._super('render'); this.renderList(); }, /** * Instantiate the right table to render */ renderList: function() { const dataTableType = this._dataTableMap[this.reportType]; this._disposeList(); let layoutMeta = { type: dataTableType, context: this.context, module: 'Reports', useCustomReportDef: this.options.useCustomReportDef, }; if (_.has(this, 'layout') && this.layout) { layoutMeta.layout = this.layout; } if (_.has(this, 'layout') && _.has(this.layout, 'layout')) { layoutMeta.panelWrapper = this.layout.layout; } if (!this.context.get('model').get('report_type')) { layoutMeta.model = this.model; } this._dataTable = app.view.createLayout(layoutMeta); this._dataTable.initComponents(); this.$('.dataTablePlaceholder').append(this._dataTable.$el); this._dataTable.render(); this.$('.dataTablePlaceholder').toggleClass('!overflow-y-auto', this.reportType === 'Matrix'); }, rebuildDataTableList: function(reportType) { this.model.set('report_type', reportType); this.reportType = this.model.get('report_type'); this.renderList(); }, /* * Collapse/Maximize the table widget * * @param {boolean} collapse */ _collapseTable: function(collapse) { if (collapse) { this.$el.hide(); } else { this.$el.show(); } }, /** * Dispose subcomponent */ _disposeList: function() { if (this._dataTable) { this._dataTable.dispose(); this._dataTable = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeList(); this._super('_dispose'); }, }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.RecordlistView * @alias SUGAR.App.view.views.BaseReportsRecordlistView * @extends View.Views.Base.RecordListView */ ({ // Recordlist View (base) extendsFrom: 'RecordListView', /** * @inheritdoc */ initialize: function(options) { this.contextEvents = _.extend({}, this.contextEvents, { 'list:editreport:fire': 'editReport', 'list:schedulereport:fire': 'scheduleReport', 'list:viewschedules:fire': 'viewSchedules', 'list:copy:fire': 'copyTemplate', }); this._super('initialize', [options]); this._initProperties(); }, /** * Property initialization */ _initProperties: function() { this._fieldsToFetch = ['is_template']; }, /** * Go to the Reports Wizard Edit page * * @param {Data.Bean} model Selected row's model. * @param {RowActionField} field */ editReport: function(model, field) { var route = app.bwc.buildRoute('Reports', null, 'ReportCriteriaResults', { id: model.id, page: 'report', mode: 'edit', fromListView: true, }); app.router.navigate(route, {trigger: true}); }, /** * Open schedule report drawer * @param model * @param field */ scheduleReport: function(model, field) { var newModel = app.data.createBean('ReportSchedules'); newModel.set({ report_id: model.get('id'), report_name: model.get('name') }); app.drawer.open({ layout: 'create', context: { create: true, module: 'ReportSchedules', model: newModel } }); }, /** * View report schedules * @param model * @param field */ viewSchedules: function(model, field) { var filterOptions = new app.utils.FilterOptions().config({ initial_filter_label: model.get('name'), initial_filter: 'by_report', filter_populate: { 'report_id': [model.get('id')] } }); app.controller.loadView({ module: 'ReportSchedules', layout: 'records', filterOptions: filterOptions.format() }); }, /** * Event handler for open copy modal. */ copyTemplate: function(model) { const modal = app.view.createView({ name: 'report-copy-modal', type: 'report-copy-modal', model: model }); $('body').append(modal.$el); modal.openModal(); }, }) }, "drillthrough-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.DrillthroughHeaderpaneView * @alias SUGAR.App.view.views.BaseReportsDrillthroughHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // Drillthrough-headerpane View (base) extendsFrom: 'HeaderpaneView', /** * @inheritdoc */ _renderHtml: function() { this._super('_renderHtml'); this.layout.once('drillthrough:closedrawer:fire', _.bind(function() { this.$el.off(); app.drawer.close(); }, this)); }, /** * @inheritdoc */ _formatTitle: function(title) { var chartModule = this.context.get('chartModule'); return app.lang.get('LBL_MODULE_NAME', chartModule); } }) }, "report-panel-footer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportWidgetFooterView * @alias SUGAR.App.view.views.BaseReportsReportWidgetFooterView * @extends View.Views.Base.View */ ({ // Report-panel-footer View (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * @inheritdoc */ _initProperties: function() { this.showFooter = true; const visibilityForDashlet = this._getFooterVisbilityForDashlet(); if (!_.isUndefined(visibilityForDashlet)) { this.showFooter = visibilityForDashlet; } }, /** * For dashlet the footer can be hidden, we have to figure out * @return {Mixed} - if we are not on dashlet we will get undefined either true/false */ _getFooterVisbilityForDashlet: function() { if (!this.layout) { return; } const summationDetailsComponent = this.layout.getComponent('summation-details'); const summationComponent = this.layout.getComponent('summation'); //this footer is used in both simple summation also summation with details //always there will be only one component available if (!summationDetailsComponent && !summationComponent) { return; } let component = summationDetailsComponent ? summationDetailsComponent : summationComponent; const summationDetailsLayout = component.layout; if (!summationDetailsLayout || !summationDetailsLayout.model) { return; } const dashletListOptions = summationDetailsLayout.model.get('list'); if (!dashletListOptions) { return; } if (!_.has(dashletListOptions, 'showCount')) { return; } return dashletListOptions.showCount; }, /** * Register events */ _registerEvents: function() { if (_.has(this, 'layout') && _.has(this.layout, 'layout')) { this.listenTo(this.layout.layout, 'panel:collapse', this.toggleFooter, this); this.listenTo(this.layout.layout, 'panel:minimize', this.toggleFooter, this); this.listenTo(this.context, 'report:set-footer-visibility', this.toggleFooter, this); } }, /** * Hide/Show footer bar * * @param {boolean} collapsed */ toggleFooter: function(collapsed) { this.$el.toggleClass('hide', collapsed); }, }) }, "report-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportChartView * @alias SUGAR.App.view.views.BaseReportsReportChartView * @extends View.Views.Base.View */ ({ // Report-chart View (base) plugins: ['ReportsPanel'], /** * Before init properties */ _beforeInit: function() { this._reportData = app.data.createBean(); this._settings = app.data.createBean(); this._chartField = null; this._chartDef = { type: 'chart', customLegend: true, }; this.RECORD_NOT_FOUND_ERROR_CODE = 404; this.SERVER_ERROR_CODES = [500, 502, 503, 504]; }, /** * Register events */ _registerEvents: function() { this.listenTo(this, 'chart:clicked', this._drilldown, this); this.listenTo(this.context, 'runtime:filters:updated', this.runtimeFiltersUpdated, this); this.listenTo(this.context, 'split-screens-resized', this.updateChartUI, this); this.listenTo(this.context, 'dashlet:mode:loaded', this.updateChartUI, this); if (_.has(this, 'layout') && this.layout && _.has(this.layout, 'layout') && this.layout.layout) { this.listenTo(this.layout.layout, 'panel:collapse', this._collapseChart, this); this.listenTo(this.layout.layout, 'grid-panel:size:changed', this._updateChartSize, this); } }, /** * Whenever orientation/size/visibility of the chart container changes, we have to rerender the chart */ updateChartUI: function() { if (!this._chartField) { return; } const isFunnelOrDonut = (type) => ['funnelF', 'donutF'].includes(type); const hasDefaultChart = isFunnelOrDonut(this.model.get('chart_type')); const hasCustomChart = this.model.get('chart') && isFunnelOrDonut(this.model.get('chart').chartType); // we only need to recreate the chart element if it's donut or funnel if (hasDefaultChart || hasCustomChart) { this._chartField.generateD3Chart(); } }, /** * Refetch chart data with the new runtime filters */ runtimeFiltersUpdated: function() { this._loadReportData(); }, /** * @inheritdoc */ _renderField: function(field) { this._super('_renderField', [field]); if (!this._chartField && field.def.type === 'chart') { this._chartField = field; } }, /** * Get the report and chart data */ _loadReportData: function() { const useCustomReportDef = this.options.useCustomReportDef; if (!_.isEmpty(this.context)) { this.context.set('reportHasChart', false); } if (useCustomReportDef) { this._loadCustomReportData(); return; } this._loadDefaultReportData(); }, /** * Get the default report and chart data */ _loadDefaultReportData: function() { const reportId = this.model.get('id'); const url = app.api.buildURL('Reports/' + reportId + '/chart?use_saved_filters=true'); app.api.call('read', url, null, { success: _.bind(this._storeReportData, this), error: _.bind(this._handleError, this), }); }, /** * Get the custom report and chart data */ _loadCustomReportData: function() { const reportId = this.model.get('id'); const url = app.api.buildURL('Reports/' + reportId + '/chart'); app.api.call('create', url, this._getChartReportMeta(), { success: _.bind(this._storeReportData, this), error: _.bind(this._handleError, this), }); }, /** * Build and apply the complete meta for a report chart * * @param {Object} reportDef * * @return {Object} */ _applyCustomChartReportMeta: function(reportDef) { const mappingTable = { filtersDef: 'filters_def', summaryColumns: 'summary_columns', displayColumns: 'display_columns', groupDefs: 'group_defs', fullTableList: 'full_table_list', multipleOrderBy: 'multipleOrderBy', orderBy: 'order_by', summaryOrderBy: 'summary_order_by', intelligent: 'intelligent', }; const chartReportMeta = this._getChartReportMeta(); _.each(chartReportMeta, function mapProperties(propValue, propKey) { const reportDefKey = mappingTable[propKey]; if (reportDefKey) { reportDef[reportDefKey] = propValue; } }, this); reportDef.useSavedFilters = false; return reportDef; }, /** * Build and return the complete meta for a report chart * * @return {Object} */ _getChartReportMeta: function() { const reportId = this.model.get('id'); const chartMeta = app.utils.deepCopy(this.model.get('chart')) || {}; const intelligenceMeta = this.model.get('intelligent') || {}; const meta = { record: reportId, reportType: this.model.get('report_type'), }; const lastStateKey = this.model.get('lastStateKey'); let hasSecondGroupBy = false; meta.filtersDef = this._getCustomFiltersMeta(chartMeta, lastStateKey); if (_.has(chartMeta, 'summaryColumns') && !_.isEmpty(chartMeta.summaryColumns)) { meta.summaryColumns = chartMeta.summaryColumns; } if (_.has(chartMeta, 'displayColumns') && !_.isEmpty(chartMeta.displayColumns)) { meta.displayColumns = chartMeta.displayColumns; } if (_.has(chartMeta, 'groupDefs') && !_.isEmpty(chartMeta.groupDefs)) { meta.groupDefs = chartMeta.groupDefs; hasSecondGroupBy = meta.groupDefs.length > 1; } if (_.has(chartMeta, 'fullTableList') && !_.isEmpty(chartMeta.fullTableList)) { meta.fullTableList = chartMeta.fullTableList; } const orderByKey = 'orderBy'; const summaryOrderByKey = 'summaryOrderBy'; const sortDirKey = 'sort_dir'; if (_.has(chartMeta, 'primaryOrderBy') && !_.isEmpty(chartMeta.primaryOrderBy)) { let order = _.first(chartMeta.primaryOrderBy); order[sortDirKey] = order[sortDirKey] === 'asc' ? 'a' : 'd'; meta[orderByKey] = [order]; } if (_.has(chartMeta, 'secondaryOrderBy') && !_.isEmpty(chartMeta.secondaryOrderBy) && chartMeta.isBarChart && hasSecondGroupBy ) { let order = _.first(chartMeta.secondaryOrderBy); order[sortDirKey] = order[sortDirKey] === 'asc' ? 'a' : 'd'; meta[orderByKey] = _.union(meta[orderByKey], [order]); meta.multipleOrderBy = true; } if (_.has(meta, orderByKey)) { meta[summaryOrderByKey] = meta[orderByKey]; } if (!_.isEmpty(intelligenceMeta)) { meta.intelligent = intelligenceMeta; } if (!_.isUndefined(chartMeta.chartType)) { meta.chartType = chartMeta.chartType; } return meta; }, /** * Setup preview widget view */ _setupPreviewReportPanel: function() { _.defer( _.bind(this._storeReportData, this, this.context.get('previewData').chartData) ); }, /** * Setup chart properties and store report data * * @param {Object} data */ _storeReportData: function(data) { if (this.disposed) { return; } if (data.error) { this._handleError(data); return; } if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this._handleError({}); } data = this._sanitizeData(data); if (_.isEmpty(data)) { this._showEmptyChart(true); if (!_.isEmpty(this.context)) { this.context.set('reportHasChart', false); } if (_.has(this, 'layout') && this.layout && typeof(this.layout.trigger) === 'function') { this.layout.trigger('panel:widget:finished:loading', true, false); } return; } const validChart = this._setChartParams(data); if (!validChart) { this._showEmptyChart(true); if (!_.isEmpty(this.context)) { this.context.set('reportHasChart', false); } if (_.has(this, 'layout') && this.layout && typeof(this.layout.trigger) === 'function') { this.layout.trigger('panel:widget:finished:loading', true, false); } return; } this._reportData.set('rawReportData', data.reportData); this._reportData.set('rawChartData', data.chartData); this.context.set('reportHasChart', true); this.context.trigger('report:data:chart:loaded', false, 'chart'); this._setFooter(); if (_.has(this, 'layout') && this.layout && typeof(this.layout.trigger) === 'function') { this.layout.trigger('panel:widget:finished:loading', false, false); } }, /** * Remove broken data * * @param {Object} data * @return {Object} */ _sanitizeData: function(data) { if (_.isEmpty(data) || _.isEmpty(data.chartData.values)) { return data; } const props = data.chartData.properties; const title = _.first(props).title.split(' '); if (data.chartData.values.length > 0 && _.last(title) === _.last(data.chartData.values).label) { data.chartData.values.pop(); const labelIdx = data.chartData.label.indexOf(''); data.chartData.label.splice(labelIdx, 1); _.each(data.chartData.values, function cleanValues(barData) { barData.links.splice(labelIdx, 1); barData.valuelabels.splice(labelIdx, 1); barData.values.splice(labelIdx, 1); }); } return data; }, /** * Collapse/Maximize the chart widget * * @param {boolean} collapse */ _collapseChart: function(collapse) { if (collapse) { this.$el.hide(); } else { this.$el.show(); } }, /** * Update chart size */ _updateChartSize: function() { this.trigger('chart-container:size:changed'); }, /** * Show/Hide the chart widget * * @param {boolean} show */ _showChart: function(show) { const emptyChartEl = this.$('[data-container="chart-container"]'); if (show) { emptyChartEl.show(); } else { emptyChartEl.hide(); } }, /** * Show/Hide the empty chart widget * * @param {boolean} show * @param {boolean} noAccess */ _showEmptyChart: function(show, noAccess) { if (this.disposed) { return; } const elId = noAccess ? 'report-no-data' : 'report-no-chart'; const emptyChartEl = this.$(`[data-widget="${elId}"]`); this.context.trigger('report:data:chart:loaded', !show, 'chart'); this._showChart(!show); this._showFooter(!show); emptyChartEl.toggleClass('hidden', !show); if (this._chartField) { this._chartField.disposeLegend(); } }, /** * Show/Hide the footer bar * * @param {boolean} show */ _showFooter: function(show) { const footerEl = this.$('[data-widget="report-footer"]'); if (show) { footerEl.show(); } else { footerEl.hide(); } }, /** * Set the report footer */ _setFooter: function() { if (!_.has(this, 'layout') || !this.layout || typeof(this.layout.getComponent) !== 'function' || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { return; } const footerBar = this.layout.getComponent('report-panel-footer'); const chartData = this._reportData.get('rawChartData').values; if (_.isEmpty(footerBar)) { return; } if (_.isEmpty(chartData)) { footerBar.$('.dashlet-title').text(''); this.context.trigger('report:set-footer-visibility', true); return; } const title = this._reportData.get('rawChartParams').report_title; footerBar.$('.dashlet-title').text(title); }, /** * Setup chart properties * * @param {Object} data * * @return {boolean} */ _setChartParams: function(data) { const reportData = data.reportData; const chartData = data.chartData; const chartProperties = _.first(chartData.properties); const chartConfig = this._getChartConfig(chartProperties.type); const chartType = chartProperties.type; const groupDefsKey = 'group_defs'; const groupDefs = reportData[groupDefsKey]; if (chartType === 'none' || _.isEmpty(groupDefs)) { return false; } const properties = { report_title: chartProperties.title, show_legend: chartProperties.legend === 'on' ? true : false, print_chart_legend: chartProperties.legend === 'on' ? true : false, print_chart_title: chartProperties.title ? true : false, module: chartProperties.base_module, allow_drillthru: chartProperties.allow_drillthru, saveChartAsImage: true, imageExportType: reportData.pdfChartImageExt, saved_report_id: this.model.get('id'), }; const config = { label: reportData.label, chart_type: chartType, stacked: chartConfig.barType === 'stacked' || chartConfig.barType === 'basic' ? true : false, vertical: chartConfig.orientation === 'vertical' ? true : false, x_axis_label: this._getXaxisLabel(reportData.group_defs, chartProperties, chartType), y_axis_label: this._getYaxisLabel(reportData), }; const defaultSettings = this._getChartDefaultSettings(); const customSettings = this._getCustomChartParams(); const settings = _.extend(properties, config, defaultSettings, customSettings); this.setupConsistentChartColors(data, settings); this._reportData.set('rawChartParams', settings); this._settings.set(settings); return true; }, /** * Setup consistent chart colors * * @param {Object} data * @param {Object} settings */ setupConsistentChartColors: function(data, settings) { const chartLabels = this.getChartLabels(data.chartData); const chartColors = this.getConsistentChartColors(data); const chartType = _.first(data.chartData.properties).type; if (!_.isEmpty(chartColors) && chartColors.length === chartLabels.length) { settings.colorOverrideList = chartColors; } delete settings.legendItemColor; // if we have 2 group bys but only 1 label, we need to use the second group by as the label // The exception is that the line chart may have only one label, but the legend contains two items, // so we don't need to set the legend color if (data.chartData.label.length === 1 && data.reportData.group_defs && chartType !== 'line chart') { this.setupSingularLabel(data.reportData, data.chartData, settings); } }, /** * Setup singular label * * @param {Object} reportData * @param {Object} chartData * @param {Object} settings * * @return {boolean} */ setupSingularLabel: function(reportData, chartData, settings) { const groupMeta = this.getGroupByMeta('last', reportData.group_defs, reportData); if (!groupMeta || !groupMeta.options || groupMeta.type !== 'enum') { return false; } const options = app.lang.getAppListStrings(groupMeta.options); const optionsStyle = app.metadata.getDropdownStyle(groupMeta.options); if (_.isEmpty(optionsStyle) || _.isEmpty(options)) { return false; } const targetLabel = _.first(chartData.label); const optionKey = this.getOptionKeyByLabel(options, targetLabel); if (!optionKey) { return false; } const optionStyle = optionsStyle[optionKey]; if (!optionStyle || !optionStyle.backgroundColor) { return false; } settings.legendItemColor = optionStyle.backgroundColor; }, /** * Get custom chart settings * * @return {Object} */ _getCustomChartParams: function() { const useCustomReportDef = this.options.useCustomReportDef; const chartConfig = this.model.get('chart'); let customSettings = {}; if (!useCustomReportDef || !chartConfig) { return customSettings; } const showLegendIndex = 'show_legend'; const showControlsIndex = 'show_controls'; const showTitleIndex = 'show_title'; const showXlabelIndex = 'show_x_label'; const showYlabelIndex = 'show_y_label'; const xAxisLabel = 'x_axis_label'; const yAxisLabel = 'y_axis_label'; if (!_.isUndefined(chartConfig.config)) { customSettings.config = chartConfig.config; } if (!_.isUndefined(chartConfig.showTitle)) { customSettings[showTitleIndex] = chartConfig.showTitle; } if (!_.isUndefined(chartConfig.showXLabel)) { customSettings[showXlabelIndex] = chartConfig.showXLabel; } if (!_.isUndefined(chartConfig.showYLabel)) { customSettings[showYlabelIndex] = chartConfig.showYLabel; } if (!_.isUndefined(chartConfig.showValues)) { customSettings.showValues = chartConfig.showValues; } if (!_.isUndefined(chartConfig.xAxisLabel)) { customSettings[xAxisLabel] = chartConfig.xAxisLabel; } if (!_.isUndefined(chartConfig.yAxisLabel)) { customSettings[yAxisLabel] = chartConfig.yAxisLabel; } if (!_.isUndefined(chartConfig.colorData)) { customSettings.colorData = chartConfig.colorData; } if (!_.isUndefined(chartConfig.reduceXTicks)) { customSettings.config = chartConfig.reduceXTicks; } if (!_.isUndefined(chartConfig.showControls)) { customSettings[showControlsIndex] = chartConfig.showControls; } if (!_.isUndefined(chartConfig.showLegend)) { customSettings[showLegendIndex] = chartConfig.showLegend; } return customSettings; }, /** * Open drilldown drawer * * @param {Object} drawerContext */ _openDrawer: function(drawerContext) { const currentModule = app.drawer.context.get('module'); app.drawer.context.set('module', drawerContext.chartModule); app.drawer.open({ layout: 'drillthrough-drawer', context: drawerContext }, _.bind(function resetDrawerModule() { if (currentModule) { // reset the drawer module app.drawer.context.set('module', currentModule); } }, this, currentModule)); }, /** * Open a focus drawer * * @param {Object} drawerContext * @param {string} tabTitle */ _openSideDrwawer: function(context, tabTitle) { if (app.sideDrawer) { const drawerIsOpen = app.sideDrawer.isOpen(); const drawerContext = app.sideDrawer.currentContextDef; if (drawerIsOpen && _.isEqual(context, drawerContext)) { return; } const sideDrawerClick = !!this.$el && (this.$el.closest('#side-drawer').length > 0); if (!_.has(context, 'dataTitle')) { const baseModuleKey = 'base_module'; const hasReportData = _.has(context, 'reportData') && _.has(context.reportData, 'base_module'); const hasLabel = _.has(context, 'reportData') && _.has(context.reportData, 'label'); const reportModule = hasReportData ? context.reportData[baseModuleKey] : ''; const reportLabel = hasLabel ? context.reportData.label : ''; context.dataTitle = app.sideDrawer.getDataTitle( reportModule, 'LBL_FOCUS_DRAWER_DASHBOARD', reportLabel ); } const sideDrawerMeta = { dashboardName: tabTitle, layout: 'report-side-drawer', css_class: 'flex flex-column', context }; app.sideDrawer.open(sideDrawerMeta, null, sideDrawerClick); } }, /** * Setup drilldown list view * * @param event * @param activeElements * @param {Function} chart * @param {BaseChart} wrapper * @param {Object} reportDef */ _drilldown: function(event, activeElements, chart, wrapper, reportDef) { const useCustomReportDef = this.options.useCustomReportDef; if (useCustomReportDef) { reportDef = this._applyCustomChartReportMeta(reportDef); } const chartConfig = this.model.get('chart'); const chartExtraParams = {}; const showLegendKey = 'show_legend'; if (chartConfig) { chartExtraParams[showLegendKey] = chartConfig.showLegend; } else { chartExtraParams[showLegendKey] = this._reportData.get('rawChartParams')[showLegendKey]; } if (this.context.get('previewMode')) { app.alert.show('report-preview-limitation', { level: 'warning', messages: app.lang.get('LBL_REPORTS_PREVIEW_LIMITATION'), autoClose: true }); return; } let params = Object.assign({}, wrapper.params, chartExtraParams); // funnel chart uses chartjs v2 which has a different signature if (wrapper.chartType === 'funnel') { if (_.isEmpty(activeElements)) { return; } const internalChart = _.first(activeElements)._chart; const elementClicked = _.first(internalChart.getElementAtEvent(event)); params.seriesIndex = elementClicked._datasetIndex; params.seriesLabel = internalChart.data.datasets[params.seriesIndex].label; params.groupIndex = elementClicked._index; params.groupLabel = internalChart.data.labels[params.groupIndex]; } else { const element = chart.getElementsAtEventForMode(event, 'nearest', {intersect: true}, false)[0]; if (_.isEmpty(element)) { return; } if (params.chart_type === 'line chart') { params.groupIndex = element.datasetIndex; params.groupLabel = chart.data.datasets[params.groupIndex].label; params.seriesIndex = element.index; params.seriesLabel = chart.data.labels[params.seriesIndex]; } else { params.seriesIndex = element.datasetIndex; params.seriesLabel = chart.data.datasets[params.seriesIndex].label; params.groupIndex = element.index; params.groupLabel = chart.data.labels[params.groupIndex]; } } params.saved_report_id = this.model.id ? this.model.id : this.model.get('id'); const uniqueStateId = this.layout.model.get('dashlet_id'); this._handleFilter(params, null, reportDef, wrapper.rawData, uniqueStateId); }, /** * Handle either navigating to target module or update list view filter. * * @param {Object} params chart display parameters * @param {Object} state chart display and data state * @param {Object} reportData report data as returned from API * @param {Object} chartData chart data with properties and data array */ _handleFilter: function(params, state, reportData, chartData, uniqueStateId) { app.alert.show('listfromreport_loading', {level: 'process', title: app.lang.get('LBL_LOADING')}); const module = params.baseModule; const reportId = this.model.get('id'); const enums = SUGAR.charts.getEnums(reportData); const groupDefs = SUGAR.charts.getGrouping(reportData); const drawerContext = { chartData: chartData, chartModule: module, chartState: state, uniqueStateId: uniqueStateId, dashModel: null, dashConfig: params, enumsToFetch: enums, filterOptions: { auto_apply: false }, groupDefs: groupDefs, layout: 'report-side-drawer', module: 'Reports', reportData: reportData, reportId: reportId, skipFetch: true, useCustomReportDef: this.options.useCustomReportDef, useSavedFilters: (_.isUndefined(reportData.useSavedFilters) || reportData.useSavedFilters) ? true : reportData.useSavedFilters, }; this._openSideDrwawer(drawerContext, reportData.label); }, /** * Update the record list in drill through drawer. * * @param {Object} params chart display parameters * @param {Object} state chart display and data state */ _updateList: function(params, state) { const drawer = this.closestComponent('drawer').getComponent('drillthrough-drawer'); drawer.context.set('dashConfig', params); drawer.context.set('chartState', state); drawer.updateList(); if (this._chartField && this._chartField.chart) { this._chartField.chart.updateParams(params); } }, /** * Returns the x-axis label based on report data * * @param {Object} groups * @param {Object} properties * @param {string} chartType * * @return {string} */ _getXaxisLabel: function(groups, properties, chartType) { if (_.isEmpty(groups)) { return ''; } return chartType === 'line chart' ? properties.seriesName || _.last([].concat(groups)).label : properties.groupName || _.first([].concat(groups)).label; }, /** * Returns the y-axis label based on report data * * @param {Object} data * * @return {string} */ _getYaxisLabel: function(data) { let label = ''; let chartFunction = ''; if (_.has(data, 'numericalChartColumn')) { const dataSeries = app.utils.reports.getDataSeries(data.numericalChartColumn); if (dataSeries && _.has(dataSeries, 'groupFunction')) { chartFunction = dataSeries.groupFunction; } } if (data && data.summary_columns && _.isArray(data.summary_columns)) { for (let i = 0; i < data.summary_columns.length; i++) { let column = data.summary_columns[i]; if (_.has(column, 'group_function') && !_.isUndefined(column.group_function) && chartFunction === column.group_function) { label = column.label; break; } } } return label; }, /** * Builds the chart config based on the type of chart * * @param {string} chartType * * @return {Mixed} */ _getChartConfig: function(chartType) { if (_.contains(['pie chart', 'donut chart', 'treemap chart', 'gauge chart'], chartType)) { return { chartType: chartType, }; } if (_.contains(['line chart'], chartType)) { return { lineType: 'grouped', chartType: 'line chart', }; } if (_.contains(['funnel chart 3D'], chartType)) { return { chartType: 'funnel chart', }; } if (_.contains(['stacked group by chart'], chartType)) { return { orientation: 'vertical', barType: 'stacked', chartType: 'group by chart', }; } if (_.contains(['group by chart'], chartType)) { return { orientation: 'vertical', barType: 'grouped', chartType: 'group by chart', }; } if (_.contains(['bar chart'], chartType)) { return { orientation: 'vertical', barType: 'basic', chartType: 'group by chart', }; } if (_.contains(['horizontal group by chart'], chartType)) { return { orientation: 'horizontal', barType: 'stacked', chartType: 'horizontal group by chart', }; } if (_.contains(['horizontal bar chart', 'horizontal'], chartType)) { return { orientation: 'horizontal', barType: 'grouped', chartType: 'horizontal group by chart', }; } if (_.contains(['horizontal grouped bar chart'], chartType)) { return { orientation: 'horizontal', barType: 'grouped', chartType: 'horizontal group by chart' }; } if (_.contains(['vertical grouped bar chart'], chartType)) { return { orientation: 'vertical', barType: 'grouped', chartType: 'group by chart', }; } return { orientation: 'vertical', barType: 'stacked', chartType: 'bar chart', }; }, /** * Handle the report failed * * @param {Error} error */ _handleError: function(error) { // don't show alert for dashlets if (!this.options.useCustomReportDef) { const message = app.utils.tryParseJSONObject(error.responseText); let errorMessage = message ? message.error_message : error.responseText; let reportModel = this.context.get('model'); if (!reportModel.get('report_type') && this.layout) { reportModel = this.layout.model; } const targetReportId = reportModel.get('id') || reportModel.get('report_id'); if (_.isEmpty(errorMessage) || error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { errorMessage = app.lang.get('LBL_NO_ACCESS', 'Reports'); } if (this.SERVER_ERROR_CODES.includes(error.status)) { errorMessage = app.lang.get('LBL_SERVER_ERROR', 'Reports'); } app.alert.show('report-data-error', { level: 'error', title: errorMessage, messages: app.lang.getModuleName('Reports') + ': ' + targetReportId, }); } this._showEmptyChart(true, true); if (!_.isEmpty(this.context)) { this.context.set('reportHasChart', false); } if (_.has(this, 'layout') && this.layout && typeof(this.layout.trigger) === 'function') { this.layout.trigger('panel:widget:finished:loading', true, false); } if (!_.isEmpty(this.context)) { this.context.set( 'permissionsRestrictedReport', error.status === this.RECORD_NOT_FOUND_ERROR_CODE ); } }, /** * Provides the base chart settings * * @return {Object} */ _getChartDefaultSettings: function() { return { direction: app.lang.direction, colorData: 'class', allowScroll: true, config: true, hideEmptyGroups: true, reduceXTicks: true, rotateTicks: true, show_controls: false, show_title: false, show_x_label: true, show_y_label: true, staggerTicks: false, wrapTicks: false, showValues: 'middle', auto_refresh: 0, }; }, /** * Get the option key based on label * * @param {Object} dom * @param {string} optionLabel * * @return {string} */ getOptionKeyByLabel: function(dom, optionLabel) { let optionKey = false; _.each(dom, (label, key) => { if (label === optionLabel) { optionKey = key; } }); return optionKey; }, /** * Get consistent chart colors * * @param {Object} data * * @return {Object} */ getConsistentChartColors: function(data) { const defaultColor = '#e2d4fd'; const reportData = data.reportData; const chartData = data.chartData; const fieldMeta = this.getChartGroupColumn(reportData, chartData); const chartType = _.first(chartData.properties).type; if (!fieldMeta || !fieldMeta.options || fieldMeta.type !== 'enum') { return []; } const options = app.lang.getAppListStrings(fieldMeta.options); const optionsStyle = app.metadata.getDropdownStyle(fieldMeta.options); if (_.isEmpty(options) || _.isEmpty(optionsStyle)) { return []; } if (chartType === 'treemap chart') { chartData.values = chartData.values.sort( (a, b) => _.first(b.values) - _.first(a.values) ); } const chartLabels = this.getChartLabels(chartData); const consistentColors = []; _.each(chartLabels, (optionLabel) => { const targetLabel = _.isArray(optionLabel) ? _.first(optionLabel) : optionLabel; const optionKey = this.getOptionKeyByLabel(options, targetLabel); if (optionKey === false) { consistentColors.push(defaultColor); return; } const optionStyle = optionsStyle[optionKey]; if (!optionStyle) { consistentColors.push(defaultColor); return; } consistentColors.push(optionStyle.backgroundColor || defaultColor); }); if (chartType === 'funnel chart 3D') { return consistentColors.reverse(); } return consistentColors; }, /** * Get chart labels * * @param {Object} chartData * * @return {Array} */ getChartLabels: function(chartData) { const parsedLabelsCharts = ['line chart', 'treemap chart']; const chartType = _.first(chartData.properties).type; let chartLabels = chartData.label; if (_.includes(parsedLabelsCharts, chartType) || chartData.label.length === 1) { chartLabels = _.pluck(chartData.values, 'label'); } return chartLabels; }, /** * Get chart group column * * @param {Object} reportData * @param {Object} chartData * * @return {Object} */ getChartGroupColumn: function(reportData, chartData) { const reportDataGroupDefs = reportData.group_defs; if (reportDataGroupDefs.length === 0) { return false; } const alwaysFirstGrp = [ 'pie chart', 'funnel chart 3D', 'line chart', 'donut chart', 'treemap chart', ]; const chartType = _.first(chartData.properties).type; const singularLabel = chartData.label.length === 1; const targetGroupByFn = (_.includes(alwaysFirstGrp, chartType) || singularLabel) ? 'first' : 'last'; return this.getGroupByMeta(targetGroupByFn, reportDataGroupDefs, reportData); }, /** * Get group by meta * * @param {string} targetGroupByFn * @param {Object} reportDataGroupDefs * @param {Object} reportData * * @return {Object} */ getGroupByMeta: function(targetGroupByFn, reportDataGroupDefs, reportData) { let targetIdx = 0; if (targetGroupByFn === 'last') { targetIdx = Math.min(1, reportDataGroupDefs.length - 1); } const chartGroupBy = reportDataGroupDefs[targetIdx]; const tableKey = chartGroupBy.table_key; const fieldName = chartGroupBy.name; const tableList = reportData.full_table_list; const module = tableList[tableKey] ? (tableList[tableKey].module || '') : ''; if (!module || !fieldName) { return false; } const fieldMeta = app.metadata.getField({ module: module, name: fieldName }); return fieldMeta; }, /** * Dispose chart element */ _disposeChart: function() { if (this._chartField) { this._chartField.dispose(); this._chartField = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeChart(); this._super('_dispose'); }, }) }, "report-preview-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.ReportPreviewHeaderView * @alias SUGAR.App.view.views.ReportsReportPreviewHeaderView */ ({ // Report-preview-header View (base) events: { 'click [data-action="close-report-preview"]': 'closeDrawer', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Init Properties */ _initProperties: function() { const previewData = this.layout ? this.layout.options.def.previewData : {}; this._reportName = previewData.reportName ? previewData.reportName : app.lang.get('LBL_REPORT_DEFAULT_NAME'); this._showQuery = previewData.showQuery; this._queries = previewData.tableData ? previewData.tableData.queries : []; }, /** * Close Drawer */ closeDrawer: function() { app.drawer.close(); }, }) }, "report-side-drawer-list-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportSideDrawerListHeaderpaneView * @alias SUGAR.App.view.views.ReportsReportSideDrawerListHeaderpaneView * @extends View.View */ ({ // Report-side-drawer-list-headerpane View (base) extendsFrom: 'HeaderpaneView', /** * @inheritdoc */ _formatTitle: function(title) { const chartModule = this.context.get('chartModule'); return app.lang.get('LBL_MODULE_NAME', chartModule); }, }) }, "summation-details": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.SummationDetailsView * @alias SUGAR.App.view.views.ReportsSummationDetailsView * @extends View.Views.Base.View */ ({ // Summation-details View (base) extendsFrom: 'ReportsRowsColumnsView', pagination: false, LIST_ACTION: { 'APPEND': 'APPEND', 'PREPEND': 'PREPEND', }, events: { 'click .sicon-arrow-left-double': 'toggleGroup', 'click .sortable': 'sortGroup', 'click [data-action=export-csv]': 'exportToCsv', 'click [data-action=show-simplified]': 'showSimplified', }, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.filter(this.plugins, function(pluginName) { return pluginName !== 'ResizableColumns'; }); if (!_.contains(this.plugins, 'ReportExport')) { this.plugins.push('ReportExport'); } this._super('initialize', [options]); this.template = app.template.getView('summation-details', this.module); this.rightColumns = []; this.isFirstColumnFreezed = false; }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.loading) { this._adjustLoadingWidgetSize('table'); } if (this.reportComplexity === this.complexities.high || (this.reportComplexity === this.complexities.medium && this.loading)) { this.setHeaderVisibility(false); this.setFooterVisibility(false); return; } if (this.reportComplexity === this.complexities.medium) { this.context.trigger('toggle-orientation-buttons', false); } else { this.context.trigger('toggle-orientation-buttons', true); } if (!this.loading) { this._placeGroups(); } }, /** * Toggle group * * @param {Event} evt */ toggleGroup: function(evt) { const isUp = _.contains(evt.target.classList, 'up'); if (this.reportComplexity === this.complexities.medium) { this.toggleSimplifiedGroup(evt); } else { let closestEl = 'table'; let elementsToModify = [ '.subgroup', 'tbody', ]; const setElementsVisibility = function setVisibility(show) { _.each(elementsToModify, function each(element) { const targetEl = this.$(evt.target).closest(closestEl).find(element); show ? targetEl.show() : targetEl.hide(); }); }; setElementsVisibility(!isUp); } this.$(evt.target).toggleClass('up', !isUp).toggleClass('down', isUp); }, /** * Toggle group * * @param {Event} evt */ toggleSimplifiedGroup: function(evt) { const isUp = _.contains(evt.target.classList, 'up'); const bodyEl = this.$(evt.target).closest('tbody'); const groupBody = bodyEl.find('[data-table="group-body"]'); if (groupBody.length === 0) { return; } isUp ? groupBody.hide() : groupBody.show(); }, /** * Rerender the header * * @param {Array} data */ renderTableMeta: function(data) { if (data) { this.context.set('data', data); } this._setHeaderFields(); this._initializeOrderBy(data); this.render(); }, /** * Initialize the orderBy object * * TODO: - try and get order_by from this.data.order_by */ _initializeOrderBy: function() { //we get the last state if (this.useCustomReportDef) { this._initializeCustomOrderBy(true); return; } this.orderBy = app.user.lastState.get(this.orderByLastStateKey); const dataOrderBy = _.first(this.data.orderBy); if (dataOrderBy) { this.orderBy = dataOrderBy; this.orderBy.field = this.orderBy.rname = dataOrderBy.name; this.orderBy.direction = dataOrderBy.sort_dir === 'a' ? 'asc' : 'desc'; app.user.lastState.set(this.orderByLastStateKey, this.orderBy); } }, /** * Render table meta * * @param {Array} data */ _rebuildData: function(data) { if (this.disposed) { return; } this.context.set('rebuildData', true); if (_.has(data, 'reportType') && data.reportType === 'summary') { this.context.trigger('report:build:data:table', 'summary'); return; } if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this._toggleEmptyPanel(true); return; } this.data = data; this._initializeOrderBy(data); this.renderTableMeta(data); const emptyPanel = this._isEmptyPanel(data); this._toggleEmptyPanel(emptyPanel); if (!this.loading) { this.context.trigger('report:data:table:loaded', false, 'table'); } }, /** * Build collection * * @param {Array} data */ buildCollection: function(data) { this.data = data; this.reportComplexity = this._getReportComplexity(data.recordsNo, _.size(data.header)); if (_.has(this, 'layout') && this.layout) { this.exportAccess = app.acl.hasAccess('export', this.layout.module) && app.utils.reports.hasAccessToAllReport(this.layout.model, 'export'); } if (this.reportComplexity === this.complexities.medium) { this.context.trigger('report:data:table:loaded', false, 'table'); this.render(); return; } if (this.reportComplexity === this.complexities.high) { this.context.trigger('report:data:table:loaded', false, 'table'); } this.startBuildCollection(data); }, /** * Start to build the data collection * * @param {Array} data */ startBuildCollection: function(data) { let groups = []; for (const index in data.groups) { let group = data.groups[index]; let groupBy = _.flatten(this._buildGroupHeader(group)); let subgroups = this._buildSubgroups(group); let colspan; for (let subgroup of subgroups) { colspan = _.first(subgroup.records).length; } groups.push({ subgroups, groupBy, colspan }); } data.groups = groups; this.data = data; if (this.reportComplexity === this.complexities.medium) { this.setHeaderVisibility(true); this.setFooterVisibility(true); this.loading = false; this.render(); } else if (this.reportComplexity === this.complexities.low) { this.loading = false; this.render(); } const visibleEmptyPanel = this._isEmptyPanel(data) || !this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model); this._toggleEmptyPanel(visibleEmptyPanel); }, /** * * @param {Array} group */ _buildGroupHeader: function(group) { let keys = Object.keys(group); if (keys.length === 1) { group = group[keys[0]]; } let groupBy = []; if (!_.isUndefined(group.key) && !_.isUndefined(group.id)) { let groupValue = this._getGroupHeaderValue(group); const countLbl = app.lang.get('LBL_COUNT', 'Reports'); if (_.isEmpty(groupValue)) { groupValue = app.lang.get('LBL_NONE_STRING', 'Reports'); } groupBy.push(`${group.key} = ${groupValue}, ${countLbl} = ${group.count}`); } if (group.dataStructure) { groupBy.push(this._buildGroupHeader(group.dataStructure)); } return groupBy; }, /** * Get group header value * * @param {Object} group * @return {string} */ _getGroupHeaderValue: function(group) { let groupValue = group.id; if (_.isEmpty(group.fieldMeta)) { return groupValue; } if (!_.isEmpty(group.fieldMeta.id) && !_.isEmpty(group.fieldMeta.module)) { groupValue = `<a href='#${group.fieldMeta.module}/${group.fieldMeta.id}'>${group.fieldMeta.value}</a>`; return groupValue; } if (group.fieldMeta.type) { let format; switch (group.fieldMeta.type) { case 'date': if (!group.fieldMeta.showPlainText) { format = app.user.getPreference('datepref'); format = app.date.convertFormat(format); const value = group.fieldMeta.value; if (value === '') { groupValue = app.lang.get('LBL_NONE_STRING', 'Reports'); } else { const dateValue = app.date(group.fieldMeta.value); groupValue = dateValue.format(format); } } break; case 'datetime': case 'datetimecombo': if (!group.fieldMeta.showPlainText) { format = app.user.getPreference('datepref') + ' ' + app.user.getPreference('timepref'); format = app.date.convertFormat(format); const value = group.fieldMeta.value; if (value === '') { groupValue = app.lang.get('LBL_NONE_STRING', 'Reports'); } else { const dateTimeValue = app.date(group.fieldMeta.value); groupValue = dateTimeValue.format(format); } } break; case 'enum': if (_.isString(group.fieldMeta.module)) { const moduleMeta = app.metadata.getModule(group.fieldMeta.module); const fieldDef = moduleMeta.fields[group.fieldMeta.name]; if (_.isString(fieldDef.options)) { const options = app.lang.getAppListStrings(fieldDef.options); groupValue = options[groupValue]; } else if (_.isString(fieldDef.function) && !_.isUndefined(this.data.functionOptions) && !_.isUndefined(this.data.functionOptions[fieldDef.function]) && _.isString(group.fieldMeta.value)) { groupValue = this.data.functionOptions[fieldDef.function][group.fieldMeta.value]; } } break; } } return groupValue; }, /** * Build subgroups * * @param {Array} group */ _buildSubgroups: function(group) { let keys = Object.keys(group); if (keys.length === 1) { group = group[keys[0]]; } if (_.has(group, 'dataStructure')) { group = this._buildSubgroups(group.dataStructure); } let subgroups = []; if (_.has(group, 'records')) { subgroups.push(group); return subgroups; } for (let index in group) { let subgroup = group[index]; let header = _.flatten(this._buildGroupHeader(subgroup)); if (_.has(subgroup, 'header')) { header = [subgroup.header] } if(_.has(subgroup, 'dataStructure')) { subgroup = this._buildSubgroups(subgroup); if (_.isArray(subgroup)) { for (let item of subgroup) { if (_.has(item, 'header')) { const _header = this._buildSubgroupHeader(item); header.push(_header); } header = this._flattenHeader(header); const records = item.records; subgroups.push({header, records}); header = []; } } } header = this._flattenHeader(header); if (_.has(subgroup, 'records')) { const records = subgroup.records; subgroups.push({header, records}); } } return subgroups; }, /** * Flattend an array * * @param {Array} header */ _flattenHeader: function(header) { return _.chain(header) .flatten() .unique() .value(); }, /** * Return a header for a subgroup * * @param {Array|Object} subgroup */ _buildSubgroupHeader: function(subgroup) { if (_.isArray(subgroup.header) && subgroup.header.length === 1 && _.isEmpty(_.first(subgroup.header)) && _.has(subgroup, 'records')) { const countLbl = app.lang.get('LBL_COUNT', 'Reports'); const groupSize = subgroup.records.length; const newHeader = [`${countLbl}: ${groupSize}`]; return newHeader; } else { return subgroup.header; } }, /** * Add groups in dom */ _placeGroups: function() { if (this.context.get('rebuildData') === false) { return; } if (!this.data) { return; } let groupPartial = 'group'; if (this.reportComplexity === this.complexities.medium) { groupPartial = 'group-simplified'; } let placeholder = document.createDocumentFragment(); for (let group of this.data.groups) { let data = Handlebars.helpers.partial(groupPartial, this, group, {hash: {}}); placeholder.appendChild(this.createElementFromHTML(data.string)); } _.defer(_.bind(function _append() { let list = this.$('.flex-list-view-content')[0]; list.appendChild(placeholder); placeholder = null; this.context.trigger('report:data:table:loaded', false, 'table'); this.context.trigger('report:panel-toolbar-visibility', true); }, this)); if (this.reportComplexity === this.complexities.medium) { this.data = null; } this.context.set('rebuildData', false); }, /** * Create Html element * * @param {string} htmlString * @return HTMLElement */ createElementFromHTML: function(htmlString) { var div = document.createElement('div'); div.innerHTML = htmlString.trim(); // Change this to div.childNodes to support multiple top-level nodes. return div.firstChild; }, /** * Sort a group * * @param {Event} evt */ sortGroup: function(evt) { this.loading = true; this.context.trigger('report:data:table:loaded', this.loading, 'table'); this.context.trigger('report:panel-toolbar-visibility', false); const eventTarget = this.$(evt.currentTarget); let orderBy = eventTarget.data('orderby'); const fieldName = eventTarget.data('fieldname'); const fieldMeta = _.filter(this._fields.visible, function(item) { return item.name === fieldName; })[0]; this.context.set('rebuildData', true); // if no alternate orderby, use the field name if (!orderBy) { orderBy = fieldName; } if (_.isUndefined(this.orderBy)) { this.orderBy = { field: '', direction: 'desc' } } let tableKey = evt.currentTarget.dataset.tablekey; this.orderBy.table_key = tableKey.substr(0, tableKey.lastIndexOf(':')); this.orderBy.type = fieldMeta.type; this.orderBy.sort_on = fieldMeta.sort_on; this.orderBy.sort_on2 = fieldMeta.sort_on2; this.orderBy.rname = fieldMeta.rname; // if same field just flip if (orderBy === this.orderBy.field) { this.orderBy.direction = this.orderBy.direction === 'desc' ? 'asc' : 'desc'; } else { this.orderBy.field = orderBy; this.orderBy.direction = 'desc'; } //we get the last state if (this.useCustomReportDef && !_.isUndefined(this.customOrderBy)) { this.customOrderBy.table_key = tableKey.substr(0, tableKey.lastIndexOf(':')); this.customOrderBy.type = fieldMeta.type; this.customOrderBy.sort_on = fieldMeta.sort_on; this.customOrderBy.sort_on2 = fieldMeta.sort_on2; this.customOrderBy.rname = fieldMeta.rname; this._setCustomOrderBy(); } else if (this.orderByLastStateKey) { app.user.lastState.set(this.orderByLastStateKey, this.orderBy); } this._loadReportData(); }, /** * Set header visibility * * @param {boolean} show */ setHeaderVisibility: function(show) { if (_.has(this, 'layout') && _.has(this.layout, 'layout')) { const header = this.layout.layout.getComponent('report-panel-toolbar'); if (!header) { return; } show ? header.show() : header.hide(); } }, /** * Set footer visibility * * @param {boolean} show */ setFooterVisibility: function(show) { const footer = this.layout.getComponent('report-panel-footer'); if (!footer) { return; } show ? footer.show() : footer.hide(); }, /** * Is empty panel * * @param {Object} data * @return {boolean} */ _isEmptyPanel: function(data) { return data.groups.length === 0; }, }) }, "report-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportHeaderView * @alias SUGAR.App.view.views.BaseReportsReportHeaderView * @extends View.View */ ({ // Report-header View (base) events: { 'click .report-header-padding [data-bs-toggle="dropdown"]': 'toggleActionButton', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Init properties */ _initProperties: function() { this._currentUrl = Backbone.history.getFragment(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'button:refresh:click', this.refreshClicked, this); this.listenTo(this.context, 'button:edit:click', this.editClicked, this); this.listenTo(this.context, 'button:copy:click', this.copyClicked, this); this.listenTo(this.context, 'button:schedule:click', this.scheduleListClicked, this); this.listenTo(this.context, 'button:export:click', this.exportClicked, this); this.listenTo(this.context, 'button:delete:click', this.deleteClicked, this); this.listenTo(this.context, 'button:details:click', this.detailsClicked, this); this.listenTo(this.model, 'sync', this.toggleRigtsideButton, this); //event register for preventing actions // when user escapes the page without confirming deleting app.routing.before('route', this.beforeRouteDelete, this); $(window).on('beforeunload.delete' + this.cid, _.bind(this.warnDeleteOnRefresh, this)); }, /** * Hide/Show action buttons of the dropdown menu */ toggleActionButton: function() { if (this.disposed) { return; } const restrictionActions = ['export']; const dropdownMenu = this.$('.report-header-padding [data-menu="dropdown"]'); _.each(restrictionActions, function(typeOfAction) { if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model, typeOfAction)) { typeOfAction = typeOfAction.replace(/^\w/, c => c.toUpperCase()); const actionButton = dropdownMenu.find(`span:contains("${typeOfAction}")`); if (actionButton.length > 0) { actionButton.parent().hide(); } } }, this); }, /** * Hide/Show rightside buttons */ toggleRigtsideButton: function() { if (this.disposed) { return; } if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this.$('.report-header-padding').hide(); } }, /** * Trigger the refresh of the report. */ refreshClicked: function() { this.context.trigger('report:panel-toolbar-visibility', false); this.context.trigger('report:refresh'); }, /** * Go to the Reports Wizard Edit page * * @param {Data.Bean} model Selected row's model. * @param {RowActionField} field */ editClicked: function(model, field) { if (!model || !_.has(model, 'id')) { return; } if (this.context.get('permissionsRestrictedReport')) { app.alert.show('data-table-error', { level: 'error', title: app.lang.get('LBL_NO_ACCESS', 'Reports'), messages: app.lang.getModuleName('Reports') + ': ' + model.id, }); return; } const route = app.bwc.buildRoute('Reports', null, 'ReportCriteriaResults', { id: model.id, page: 'report', mode: 'edit', sidecarEdit: 'edit' }); app.router.navigate(route, {trigger: true}); }, /** * Go to the Reports Wizard Edit page * * @param {Data.Bean} model Selected row's model. * @param {RowActionField} field */ scheduleListClicked: function(model, field) { if (!model || !_.has(model, 'id')) { return; } const filterOptions = new app.utils.FilterOptions().config({ initial_filter_label: model.get('name'), initial_filter: 'by_report', filter_populate: { 'report_id': [model.get('id')] } }); app.controller.loadView({ module: 'ReportSchedules', layout: 'records', filterOptions: filterOptions.format() }); }, /** * Event handler for open copy modal. */ copyClicked: function() { const modal = app.view.createView({ name: 'report-copy-modal', type: 'report-copy-modal' }); $('body').append(modal.$el); modal.openModal(); }, /** * Event handler for export click event. */ exportClicked: function() { const reportExport = app.view.createView({ name: 'report-export-modal', type: 'report-export-modal', context: this.context }); $('body').append(reportExport.$el); reportExport.openModal(); }, /** * Delete current record * * @param {Data.Bean} model Selected row's model. */ deleteClicked: function(model) { if (!model || !_.has(model, 'id')) { return; } this.warnDelete(model); }, /** * Event handler for openening details module. */ detailsClicked: function() { const modal = app.view.createView({ name: 'report-detail-modal', type: 'report-detail-modal' }); $('body').append(modal.$el); modal.openModal(); }, /** * Render * * Update button label */ _render: function() { this._super('_render'); }, /** * Pre-event handler before current router is changed. * * @return {boolean} `true` to continue routing, `false` otherwise. */ beforeRouteDelete: function() { if (this._modelToDelete) { this.warnDelete(this._modelToDelete); return false; } return true; }, /** * Popup dialog message to confirm delete action * * @param {Data.Bean} model Selected row's model. */ warnDelete: function(model) { let self = this; this._modelToDelete = model; self._targetUrl = Backbone.history.getFragment(); //Replace the url hash back to the current staying page if (self._targetUrl !== self._currentUrl) { app.router.navigate(self._currentUrl, {trigger: false, replace: true}); } app.alert.show('delete_confirmation', { level: 'confirmation', messages: self.getDeleteMessages().confirmation, onConfirm: _.bind(self.deleteModel, self), onCancel: function() { self._modelToDelete = false; } }); }, /** * Delete the model once the user confirms the action */ deleteModel: function() { let self = this; self.model.destroy({ //Show alerts for this request showAlerts: { 'process': true, 'success': { messages: self.getDeleteMessages().success } }, success: function() { const redirect = self._targetUrl !== self._currentUrl; self.context.trigger('record:deleted', self._modelToDelete); self._modelToDelete = false; if (redirect) { self.unbindBeforeRouteDelete(); //Replace the url hash back to the current staying page app.router.navigate(self._targetUrl, {trigger: true}); return; } app.router.navigate(self.module, {trigger: true}); } }); }, /** * Formats the messages to display in the alerts when deleting a record. * * @return {Object} The list of messages. * @return {string} return.confirmation Confirmation message. * @return {string} return.success Success message. * * @return {string} */ getDeleteMessages: function() { let messages = {}; const model = this.model; const name = Handlebars.Utils.escapeExpression(this._getNameForMessage(model)).trim(); const context = app.lang.getModuleName(model.module).toLowerCase() + ' "' + name + '"'; messages.confirmation = app.utils.formatString( app.lang.get('NTC_DELETE_CONFIRMATION_FORMATTED', this.module), [context] ); messages.success = app.utils.formatString(app.lang.get('NTC_DELETE_SUCCESS'), [context]); return messages; }, /** * Retrieves the name of a record * * @param {Data.Bean} model The model concerned. * * @return {string} name of the record. */ _getNameForMessage: function(model) { return app.utils.getRecordName(model); }, /** * Popup browser dialog message to confirm delete action * * @return {string} The message to be displayed in the browser dialog. */ warnDeleteOnRefresh: function() { if (this._modelToDelete) { return this.getDeleteMessages().confirmation; } }, /** * Detach the event handlers for warning delete */ unbindBeforeRouteDelete: function() { app.routing.offBefore('route', this.beforeRouteDelete, this); $(window).off('beforeunload.delete' + this.cid); }, /** * @inheritdoc */ _dispose: function() { this._super('_dispose'); this.unbindBeforeRouteDelete(); }, }) }, "filter-rows": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.FilterRowsView * @alias SUGAR.App.view.views.BaseReportsFilterRowsView * @extends View.Views.Base.FilterRowsView */ ({ // Filter-rows View (base) extendsFrom: 'FilterRowsView', /** * @inheritdoc */ loadFilterFields: function(module) { this._super('loadFilterFields', [module]); // last_run_date is a related datetime fields and shouldn't rely on its id_name if (this.fieldList && this.fieldList.last_run_date) { delete this.fieldList.last_run_date.id_name; } } }) }, "report-copy-modal": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ReportsReportCopyModalView * @alias SUGAR.App.view.views.BaseReportsReportCopylModalView * @extends View.View */ ({ // Report-copy-modal View (base) /** * @inheritdoc */ events: { 'click [data-type]': 'copyAs', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Init properties */ _initProperties: function() { const forceRender = false; this._isReady = false; this._currentRecordType = null; this._initModelProperties(forceRender); }, /** * Register events */ _registerEvents: function() { const forceRender = true; this.listenTo( this.model, 'sync', this._initModelProperties, this, forceRender ); }, /** * Init data from model * * @param {boolean} forceRender */ _initModelProperties: function(forceRender) { if (this.model && this.model.get('report_type')) { this._isReady = true; this._currentRecordType = this.model.get('report_type'); } if (forceRender === true) { this.render(); } }, /** * Copy report as * * @param {jQuery} element */ copyAs: function(element) { const reportType = element.currentTarget.dataset.type; const route = app.bwc.buildRoute('Reports', null, 'ReportCriteriaResults', { id: this.model.get('id'), page: 'report', mode: 'copyAs', sidecarDuplicate: 'duplicate', newReportType: reportType, }); this.closeModal(); app.router.navigate(route, {trigger: true}); }, /** * Open Copy Modal */ openModal: function() { this.render(); let modalEl = this.$('[data-content=report-copy-modal]'); modalEl.modal({ backdrop: 'static' }); modalEl.modal('show'); modalEl.on('hidden.bs.modal', _.bind(function handleModalClose() { this.$('[data-content=report-copy-modal]').remove(); }, this)); }, /** * Close the modal and destroy it */ closeModal: function() { this.dispose(); }, /** * @inheritdoc */ _dispose: function() { this.$('[data-content=report-copy-modal]').remove(); $('.modal-backdrop').remove(); this._super('_dispose'); }, }) }, "report-panel-toolbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportPanelToolbarView * @alias SUGAR.App.view.views.BaseReportsReportPanelToolbarView * @extends View.View */ ({ // Report-panel-toolbar View (base) events: { 'click .toggleGrooups': 'toggleGroups', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._isDetail = !this.context.get('previewMode'); this._initProperties(); this._registerEvents(); }, /** * Initialize helper data */ _initProperties: function() { this.collectionCount = null; if (_.isFunction(this.layout.getTitle)) { this.meta.label = this.layout.getTitle(); } }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'report:panel-toolbar-visibility', this.setVisibility); this.listenTo(this.context, 'report:set-header-visibility', this.setHeaderVisibility); }, /** * Handle the children visiblity * * @param {bool} isVisible */ setVisibility: function(isVisible) { const child = this.$el.children(); if (child.length !== 1) { return; } isVisible ? child.removeClass('hidden') : child.addClass('hidden'); }, /** * Used in summary tables to collapse/expand groups * * @param {Event} evt */ toggleGroups: function(evt) { const shouldCollapse = _.contains(evt.target.classList, 'groups-collapse'); const collapseLabel = app.lang.get('LBL_COLLAPSE_ALL', 'Reports'); const expandLabel = app.lang.get('LBL_EXPAND_ALL', 'Reports'); if (shouldCollapse) { this.$(evt.target).removeClass('groups-collapse'); this.$(evt.currentTarget).text(expandLabel); this._showTables(false); } else { this.$(evt.target).addClass('groups-collapse'); this.$(evt.currentTarget).text(collapseLabel); this._showTables(true); } }, /** * Show/Hide the tables in a summation with details report * * @param {bool} shouldShow */ _showTables: function(shouldShow) { const show = true; let reportComplexity = 0; let reportComplexities = []; if (this.context && this.context.get) { reportComplexities = this.context.get('reportComplexities'); reportComplexity = this.context.get('reportComplexity'); } let closestEl = 'table'; let elementsToModify = [ '.subgroup', 'tbody', ]; if (reportComplexity === reportComplexities.medium) { return this._toggleSimplifiedGroup(shouldShow); } let tables = this.layout.getComponent('report-table').$el.find(closestEl); const setElementsVisibility = function setVisibility(table, show) { _.each(elementsToModify, function each(element) { const targetEl = $(table).find(element); show ? targetEl.show() : targetEl.hide(); }); }; for (const table of tables) { if (shouldShow) { setElementsVisibility(table, show); $(table).find('.sicon-arrow-left-double').switchClass('down', 'up'); } else { setElementsVisibility(table, !show); $(table).find('.sicon-arrow-left-double').switchClass('up', 'down'); } } }, /** * Show/Hide the tables in a summation with details report for simplified group * * @param {bool} shouldShow */ _toggleSimplifiedGroup: function(shouldShow) { const reportTable = this.layout.getComponent('report-table'); const groupBody = reportTable.$el.find('[data-table="group-body"]'); shouldShow ? groupBody.show() : groupBody.hide(); const icons = reportTable.$el.find('.sicon-arrow-left-double'); shouldShow ? icons.switchClass('down', 'up') : icons.switchClass('up', 'down'); }, /** * Hide/Show header bar * * @param {boolean} show */ setHeaderVisibility: function(show) { // do not hide filters toolbar if (this.name === 'report-filters-toolbar') { return; } this.$el.toggleClass('hidden', show); }, }) }, "report-side-drawer-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportSideDrawerHeaderpaneView * @alias SUGAR.App.view.views.ReportsReportSideDrawerHeaderpaneView * @extends View.View */ ({ // Report-side-drawer-headerpane View (base) extendsFrom: 'HeaderpaneView', /** * @inheritdoc */ events: { 'click [data-action="refresh-widget"]': 'refreshWidget', }, /** * @inheritdoc */ _formatTitle: function(title) { const chartModule = this.context.get('chartModule'); return app.lang.get('LBL_MODULE_NAME', chartModule); }, /** * Refresh list and chart */ refreshWidget: function() { this.context.trigger('report:side:drawer:list:refresh'); this.context.trigger('saved:report:chart:refresh'); }, }) }, "rows-columns": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.RowsColumnsView * @alias SUGAR.App.view.views.ReportsRowsColumnsView * @extends View.Views.Base.RecordlistView */ ({ // Rows-columns View (base) extendsFrom: 'RecordlistView', /** * Used in report complexity calculations */ complexities: { 'low': 0, 'medium': 1, 'high': 2, }, pagination: true, loading: true, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.filter(this.plugins, function(pluginName) { if (!_.isUndefined(options.layout) && options.layout.options.useCustomReportDef) { return (pluginName !== 'ReorderableColumns' && pluginName !== 'ResizableColumns'); } return (pluginName !== 'ReorderableColumns'); }); if (!_.contains(this.plugins, 'ReportsPanel')) { this.plugins.push('ReportsPanel'); } this._super('initialize', [options]); this.context.set('reportComplexities', this.complexities); /** * Init data is being used on the parent controller */ this._setUserLastState(); }, /** * Initialize helper data */ _initProperties: function() { this.model = app.data.createBean(); // reset current model this.collection = app.data.createBeanCollection(); // reset collection this.context.set('mass_collection', app.data.createBeanCollection()); //reset mass collection this.useCustomReportDef = false; this.orderByKeys = ['orderBy']; if (this.layout && this.layout.options) { this.useCustomReportDef = this.layout.options.useCustomReportDef; } if (this.useCustomReportDef) { this._initCustomProperties(); } if (!this.limit) { this.limit = 50; } this.RECORD_NOT_FOUND_ERROR_CODE = 404; this.SERVER_ERROR_CODES = [500, 502, 503, 504]; this.context.set('rebuildData', true); this._isDetail = !this.context.get('previewMode'); this.leftColumns = []; this.rightColumns = [{isColumnDropdown: true}]; if (_.isUndefined(this._fields)) { this._fields = {}; } this._fields.all = []; this.isReportComplex = false; }, /** * Init custom properties */ _initCustomProperties: function() { const pageListOptions = this.layout.model.get('list'); this.reportType = this.layout.model.get('report_type'); this.lastStateKey = this.layout.model.get('lastStateKey'); if (_.has(pageListOptions, 'rowsPerPage')) { this.limit = pageListOptions.rowsPerPage; } if (pageListOptions) { this.showFooterDetails = pageListOptions.showCount; } if (_.has(pageListOptions, 'orderBy') && pageListOptions.orderBy) { //this is coming from the user state config this.customConfiguredOrderBy = pageListOptions.orderBy; } const dataLoaded = false; this._initializeCustomOrderBy(dataLoaded); const userLastState = this.layout.model.get('userLastState'); if (userLastState && _.has(userLastState, 'defaultView')) { this.lastSelectedView = userLastState.defaultView; } else { this.lastSelectedView = this.layout.model.get('defaultSelectView'); } }, /** * Register events */ _registerEvents: function() { const noOptions = undefined; this.listenTo(this.context, 'rows-columns:load:collection', this._loadReportData, this); this.listenTo(this.context, 'runtime:filters:updated', _.bind(this._loadReportData, this, noOptions)); }, /** * @inheritdoc */ _render: function() { if (!this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model)) { this._fields.visible = []; this._failedLoadReportData({}); this._super('_render'); return; } this._processHeader(); if (_.isUndefined(this.data)) { this._fields.visible = []; } else { this._fields.visible = this.data.header; } const ctxRebuildData = this.context.get('rebuildData'); if (ctxRebuildData === true && !_.isUndefined(this.data)) { this.buildCollection(this.data); } this._super('_render'); if (!_.isUndefined(this.data) && this.pagination) { const pagination = this.layout.getComponent('report-table-pagination'); pagination.collection = this.collection; pagination.totalCount = pagination.collection.total; pagination.pagesCount = this.data.totalPages; } this._setFooter(); }, /** * Initialize the orderBy object * * TODO: - try and get order_by from this.data.order_by */ _initializeOrderBy: function() { //we get the last state if (this.useCustomReportDef) { this._initializeCustomOrderBy(true); return; } //we get the last state for report this.orderBy = app.user.lastState.get(this.orderByLastStateKey); const dataOrderBy = _.first(this.data.orderBy); if (_.isUndefined(this.orderBy) && dataOrderBy) { this.orderBy = { field: dataOrderBy.name, direction: dataOrderBy.sort_dir === 'a' ? 'asc' : 'desc', table_key: '', column_function: '', group_function: '', }; } }, /** * Initialize the orderBy object * * @param {boolean} dataLoaded check if the data is loaded from server */ _initializeCustomOrderBy: function(dataLoaded) { const lastState = app.user.lastState.get(this.lastStateKey); const hasLastStateForOrderBy = _.has(lastState, 'orderBy') && lastState.orderBy; const lastOrderBy = hasLastStateForOrderBy ? lastState.orderBy : false; let orderBy; if (lastOrderBy) { orderBy = this._createOrderByMeta(lastOrderBy, dataLoaded); } else if (this.customConfiguredOrderBy && this.customConfiguredOrderBy.length === 1) { orderBy = this._createOrderByMeta(this.customConfiguredOrderBy, dataLoaded); } if (orderBy) { this.customOrderBy = orderBy; this.orderBy = orderBy; } }, /** * Create orderBy object * * @param {Array} ordersMeta * @param {boolean} dataLoaded check if the data is loaded from server * * @return {Object} */ _createOrderByMeta: function(ordersMeta, dataLoaded) { const tableKey = 'table_key'; const sortDirKey = 'sort_dir'; const columnFunctionKey = 'column_function'; const groupFunctionKey = 'group_function'; let orderByMeta = _.first(ordersMeta); let orderBy = { column_function: '', group_function: '', table_key: '', }; const hasName = (_.has(orderByMeta, 'name') && orderByMeta.name) || (_.has(orderByMeta, 'rname') && orderByMeta.rname); const hasField = _.has(orderByMeta, 'field') && orderByMeta.field; if (dataLoaded && (!hasName) && (!hasField)) { orderBy.field = _.first(this.data.header).name; } else if (dataLoaded && hasName) { orderBy.field = orderByMeta.field || orderByMeta.name; } else { orderBy.field = !hasName ? orderByMeta.field : orderByMeta.name || orderByMeta.rname; } if (_.has(orderByMeta, sortDirKey)) { const curentSortDir = orderByMeta.sort_dir; if (curentSortDir !== 'asc' && curentSortDir !== 'desc') { orderBy.direction = curentSortDir === 'a' ? 'asc' : 'desc'; } else { orderBy.direction = curentSortDir; } } else if (_.has(orderByMeta, 'direction')) { orderBy.direction = orderByMeta.direction; } else { orderBy.direction = 'asc'; } if (_.has(orderByMeta, tableKey)) { orderBy[tableKey] = orderByMeta[tableKey]; } if (_.has(orderByMeta, columnFunctionKey)) { orderBy[columnFunctionKey] = orderByMeta[columnFunctionKey]; } if (_.has(orderByMeta, groupFunctionKey)) { orderBy[groupFunctionKey] = orderByMeta[groupFunctionKey]; } if (_.has(orderByMeta, 'rname')) { orderBy.rname = orderByMeta.rname; } return orderBy; }, /** * Process table header */ _processHeader: function() { if (_.isUndefined(this.data)) { return; } for (let index in this.data.header) { let headerItem = this.data.header[index]; const headerName = headerItem.name; if (['relate', 'name', 'fullname', 'username'].includes(headerItem.type)) { headerItem.link = !this.context.get('previewMode'); } if (['datetime'].includes(headerItem.type)) { headerItem.type = 'datetimecombo'; } headerItem.module = headerItem.ext2 || headerItem.module; headerItem.rname = !_.isUndefined(headerItem.rname) ? headerItem.rname : _.clone(headerItem.name); const headerCol = _.filter(this.data.header, function(item) { return item.name === headerName; }); if (headerCol.length > 1) { const orderBy = _.first(this.data.orderBy); let columnKey = headerItem.column_key; if (!_.isUndefined(orderBy) && !_.isUndefined(columnKey) && orderBy.name === headerName && orderBy.table_key === columnKey.substr(0, columnKey.lastIndexOf(':'))) { orderBy.field += index; } headerItem.name += index; this.data.header[index] = headerItem; } } }, /** * Build collection * * @param {Array} data */ buildCollection: function(data) { const records = data.records; const header = data.header || this._fields.visible; this._initCollection(); /** * Iterate records and try to create models for each value * * later these models will be evaluated separately for each table cell */ for (let index in records) { const record = records[index]; let model = app.data.createBean(); for (let recordIndex in record) { const field = record[recordIndex]; const fieldMeta = header[recordIndex]; if (_.has(fieldMeta, 'group_function') && _.has(fieldMeta, 'type') && _.has(field, 'type') && (fieldMeta.type !== field.type)) { fieldMeta.type = field.type; fieldMeta.field_type = field.type; } if (['date', 'datetime', 'datetimecombo'].includes(field.type) && !_.isEmpty(field.value) && _.has(fieldMeta ,'qualifier')) { fieldMeta.type = 'text'; } let modelField = this._buildModel(field, fieldMeta); model.set(fieldMeta.name, modelField); } this.collection.models.push(model); } this.collection.length = this.collection.models.length; }, /** * Based on the number of records we have to figured out how we will display the report * * @param {number} recordsNo * @param {number} fieldsNo * @return {boolean} */ _getReportComplexity: function(recordsNo, fieldsNo) { const reportsComplexityKey = 'reports_complexity_display'; const reportsComplexity = app.config[reportsComplexityKey]; if (_.isUndefined(reportsComplexity) || _.isEmpty(reportsComplexity)) { return this.complexities.low; } let complexity = 0; if (_.isNumber(recordsNo) && _.isNumber(fieldsNo)) { complexity = recordsNo * fieldsNo; } if (complexity < reportsComplexity.simplified) { this.context.set('reportComplexity', this.complexities.low); return this.complexities.low; } if (complexity > reportsComplexity.export) { this.context.set('reportComplexity', this.complexities.high); return this.complexities.high; } this.context.set('reportComplexity', this.complexities.medium); return this.complexities.medium; }, /** * Set some init data on the collection */ _initCollection: function() { this.collection.setOption('limit', this.limit); this.collection.models = []; this.collection.dataFetched = true; this.collection.offset = 0; this.collection.next_offset = this.data.nextOffset || 0; this.collection.total = this.data.totalCount; }, /** * Create a model * * @param {object} field * @param {object} fieldMeta * @returns BeanModel */ _buildModel: function(field, fieldMeta) { let modelField = app.data.createBean(); modelField.fields = [fieldMeta]; /** * set the values of each cell model */ modelField.set(fieldMeta.name, field.value); modelField.set(field.name, field.value); modelField.set(field.rname, field.value); if (fieldMeta.type !== 'id') { modelField.set('id', field.id); } modelField.set('_module', field.module); //make the focus drawer work modelField.set(field.id_name, field.id); modelField.module = fieldMeta.module || field.module; if (fieldMeta.type === 'image') { modelField.value = field.value; modelField.id = field.parentRecordId; } return modelField; }, /** * @inheritdoc */ _loadTemplate: function() { this.tplName = 'recordlist'; this.template = app.template.getView(this.tplName); }, /** * Set data pagination * * @param {Array} data */ setData: function(data) { this.data = data; }, /** * Set user last state */ _setUserLastState: function() { const moduleReportId = this.module + ':' + this.context.get('modelId'); this._allListViewsFieldListKey = app.user.lastState.buildKey('field-list', 'list-views', moduleReportId); this._thisListViewFieldSizesKey = app.user.lastState.buildKey('width-fields', 'record-list', moduleReportId); this.orderByLastStateKey = app.user.lastState.buildKey('order-by', 'record-list', moduleReportId); }, /** * Alter meta in order to be renderable by this controller */ _setHeaderFields: function() { this.data = this.context.get('data'); this._fields.visible = this.data.header; // disable focus drawer buttons _.each(this._fields.visible, function disableFocusDrawer(item) { item.disableFocusDrawerRecordSwitching = true; }); const panelHeaderIndex = this._getPanelIndexByName('panel_header'); this.meta.panels[panelHeaderIndex].fields = this._fields.visible; }, /** * Get the panel by name * * @param {string} name * * @return {number} */ _getPanelIndexByName: function(name) { return _.findIndex(this.meta.panels, function goThroughPanels(panelDef) { return panelDef.name === name; }); }, /** * @inheritdoc */ setOrderBy: function(event) { if (this.context.get('previewMode')) { app.alert.show('report-preview-limitation', { level: 'warning', messages: app.lang.get('LBL_REPORTS_PREVIEW_LIMITATION'), autoClose: true }); return; } const currentEvt = event.currentTarget.dataset; if (this.useCustomReportDef && !_.isUndefined(this.customOrderBy)) { this.customOrderBy.table_key = currentEvt.tablekey; this.customOrderBy.rname = currentEvt.realname; this.customOrderBy.column_function = currentEvt.columnfct; this.customOrderBy.group_function = currentEvt.groupfct; } else if (!_.isUndefined(this.orderBy)) { this.orderBy.table_key = currentEvt.tablekey; this.orderBy.rname = currentEvt.realname; this.orderBy.column_function = currentEvt.columnfct; this.orderBy.group_function = currentEvt.groupfct; } this.context.set('rebuildData', true); if (_.isUndefined(this.orderBy)) { this.orderBy = { table_key: currentEvt && _.has(currentEvt, 'tablekey') ? currentEvt.tablekey : '', column_function: currentEvt && _.has(currentEvt, 'columnfct') ? currentEvt.columnfct : '', group_function: currentEvt && _.has(currentEvt, 'groupfct') ? currentEvt.groupfct : '', }; if (currentEvt && _.has(currentEvt, 'realname') && currentEvt.realname) { this.orderBy.rname = currentEvt.realname; } else { this.orderBy.field = ''; } } this._super('setOrderBy', [event]); }, /** * @inheritdoc */ _setOrderBy: function(options) { if (this.useCustomReportDef) { this._setCustomOrderBy(); } else if (this.orderByLastStateKey) { app.user.lastState.set(this.orderByLastStateKey, this.orderBy); } // refetch the collection this.context.resetLoadFlag({recursive: false}); this.context.set('skipFetch', false); this._loadReportData(); }, /** * Set custom orderBy */ _setCustomOrderBy: function() { let lastState = app.user.lastState.get(this.lastStateKey); if (lastState) { lastState.orderBy = [this.customOrderBy]; } else { lastState = { defaultView: this.lastSelectedView, orderBy: [this.customOrderBy], }; } app.user.lastState.set(this.lastStateKey, lastState); }, /** * Setup preview widget view */ _setupPreviewReportPanel: function() { this._rebuildData(this.context.get('previewData').tableData); this.context.trigger('report:data:table:build:count', this.collection); this.context.trigger('report:data:table:loaded', false, 'table'); }, /** * Fetch the data to be rendered in list * * @param {Object} options */ _loadReportData: function(options) { this.loading = true; this.viewingSimplified = false; this.context.trigger('report:data:table:loaded', this.loading, 'table'); var self = this; if (_.isUndefined(options)) { /** * If this is being called from an event, * then we will have no options. * * This means we need to make sure those are being set */ var reportModel = self.context.get('model'); if (_.isFunction(self.getSortOptions)) { var options = self.getSortOptions(this.collection); } if (_.isUndefined(options)) { return; } this.collection.resetPagination(); // set the success callback options.success = _.bind(this._rebuildData, self); options.error = _.bind(this._failedLoadReportData, self); } else if (options) { var self = options.functionContext; var reportModel = this.context.get('model') || this.get('model'); } if (!reportModel.get('report_type') && self.layout) { reportModel = self.layout.model; } const reportId = reportModel.get('id') || reportModel.get('report_id'); const reportType = reportModel.get('report_type'); const offset = options.limit * options.page - options.limit; const lastStateSort = app.user.lastState.get(self.orderByLastStateKey); const sort = lastStateSort ? lastStateSort : null; const intelligent = reportModel.get('intelligent'); const url = app.api.buildURL('Reports', 'retrieveSavedReportsRecords'); let orderBy = sort ? [{ name: sort.rname || sort.field, table_key: sort.table_key, sort_dir: sort.direction === 'asc' ? 'a' : 'd', column_function: sort.column_function ? sort.column_function : '', group_function: sort.group_function ? sort.group_function : '', }] : null; if (this.useCustomReportDef) { let customOrderBy = this.customOrderBy; orderBy = customOrderBy ? [{ name: customOrderBy.rname || customOrderBy.field, table_key: customOrderBy.table_key, sort_dir: customOrderBy.direction === 'asc' ? 'a' : 'd', column_function: customOrderBy.column_function ? customOrderBy.column_function : '', group_function: customOrderBy.group_function ? customOrderBy.group_function : '', }] : null; } let requestMeta = { maxNum: this.limit, record: reportId, use_saved_filters: true, intelligent, reportType, offset, }; _.each(this.orderByKeys, function each(item) { requestMeta[item] = orderBy; }); const listOptions = reportModel.get('list'); const lastStateKey = reportModel.get('lastStateKey'); const customReportMeta = this._getCustomReportMeta(listOptions, lastStateKey); requestMeta = _.extend(requestMeta, customReportMeta); app.api.call('create', url, requestMeta, { success: _.bind(options.success, self), error: _.bind(options.error, self), }); }, /** * Rebuild data * * @param {array} data * @returns */ _rebuildData: function(data) { if (this.disposed) { return; } this.context.set('data', data); const ctxModel = this.context.get('model'); ctxModel.dataFetched = true; this.context.trigger('report:data:table:loaded', false, 'table'); this._setHeaderFields(); this._initializeOrderBy(); this.layout.render(); this.layout.trigger('list:sort:fire'); const visibleEmptyPanel = this._isEmptyPanel(data) || !this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model); this._toggleEmptyPanel(visibleEmptyPanel); this.context.trigger('report:data:table:build:count', this.collection); this.layout.$el.removeClass('notLoaded'); }, /** * Is empty panel * * @param {Object} data * @return {boolean} */ _isEmptyPanel: function(data) { return data.records.length === 0; }, /** * Handle the report failed * * @param {Error} error */ _failedLoadReportData: function(error) { if (this.disposed) { return; } this._toggleEmptyPanel(true); this.showFooterDetails = false; this._prepareFooterForCustomView(); this.context.trigger('report:data:table:build:count', this.collection); this.context.trigger('report:data:table:loaded', false, 'table'); this.layout.$el.removeClass('notLoaded'); const siblings = this.$el.siblings(); siblings.find('.report-panel-footer').addClass('hidden'); if (siblings.hasClass('flex-table-pagination')) { siblings.hide(); } let reportModel = this.context.get('model'); if (!reportModel.get('report_type') && this.layout) { reportModel = this.layout.model; } let showErrorAlert = error && _.isString(error.message); // don't show no access alert for dashlet if (error && reportModel.get('filter') && _.has(error, 'status') && error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { showErrorAlert = false; } if (showErrorAlert) { app.alert.show('failed_to_load_report', { level: 'error', messages: error.message, autoClose: true, }); } // don't show alert for dashlets if (!reportModel.get('list')) { const message = app.utils.tryParseJSONObject(error.responseText); let errorMessage = message ? message.error_message : error.responseText; const targetReportId = reportModel.get('id') || reportModel.get('report_id'); if (_.isEmpty(errorMessage) || error.status === this.RECORD_NOT_FOUND_ERROR_CODE) { errorMessage = app.lang.get('LBL_NO_ACCESS', 'Reports'); } if (this.SERVER_ERROR_CODES.includes(error.status)) { errorMessage = app.lang.get('LBL_SERVER_ERROR', 'Reports'); } app.alert.show('report-data-error', { level: 'error', title: errorMessage, messages: app.lang.getModuleName('Reports') + ': ' + targetReportId, }); } this.context.set( 'permissionsRestrictedReport', error.status === this.RECORD_NOT_FOUND_ERROR_CODE ); }, /** * Set the data table footer */ _setFooter: function() { this._prepareFooterForCustomView(); const footerBar = this.layout.getComponent('report-panel-footer'); const grandTotalData = this.data ? this.data.grandTotal : []; if (_.isEmpty(footerBar)) { return; } footerBar.$('.title-container > ul').empty(); _.each(grandTotalData, function goThroughTotals(totalData) { let title = totalData.vname; let value = totalData.value; let moduleName = totalData.module; let isTranslatedTitle = totalData.isvNameTranslated; if (_.isEmpty(value) || value === '0') { return; } if (!isTranslatedTitle) { title = app.lang.get(title, moduleName); } _.defer(function addGrandTotal() { footerBar.$('.title-container > ul').append('<li>' + title + ': ' + value + '</li>'); }); }, this); }, /** * We have to resize the footer */ _prepareFooterForCustomView: function() { if (!this.reportType) { return; } const tablePlaceholder = this.$el.closest('.dataTablePlaceholder'); if (tablePlaceholder.length < 1) { return; } //we have to replace _ with - to match the css class name const reportType = this.reportType.replaceAll(/_/g, '-'); if (this.showFooterDetails === true) { tablePlaceholder.removeClass(`${reportType}-dashle-no-count`); tablePlaceholder.addClass(`${reportType}-dashlet-count`); } else if (this.showFooterDetails === false) { tablePlaceholder.removeClass(`${reportType}-dashlet-count`); tablePlaceholder.addClass(`${reportType}-dashlet-no-count`); } }, /** * Toggle empty panel * * @param {boolean} show */ _toggleEmptyPanel: function(show) { const emptyPanelEl = this.$('.no-data-available'); if (show) { emptyPanelEl.removeClass('hidden'); this.$('.dataTable').addClass('hidden'); this.$('.simplified-table').addClass('hidden'); this.$('.simplified-message').addClass('hidden'); this.$el.addClass('noBorderBottom'); } else { emptyPanelEl.addClass('hidden'); this.$('.dataTable').removeClass('hidden'); this.$('.simplified-message').removeClass('hidden'); this.$('.simplified-table').removeClass('hidden'); this.$el.removeClass('noBorderBottom'); } this.context.trigger('report:set-header-visibility', show); this.context.trigger('report:set-footer-visibility', show); }, /** * Show the simplified version of the table * * @param {Event} e */ showSimplified: function(e) { const dataset = e.target.dataset; const shouldRerender = dataset.rerender; this.viewingSimplified = true; this.startBuildCollection(this.data, shouldRerender); this.$('.simplified-message').addClass('hidden'); }, /** * Handling errors * * @param {Error} error */ _handleError: function(error) { app.alert.show('rows-columns-error', { level: 'error', title: error.responseText, }); }, }) }, "summation": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.SummationView * @alias SUGAR.App.view.views.ReportsSummationView * @extends View.Views.Base.ReportsRowsColumnsView */ ({ // Summation View (base) extendsFrom: 'ReportsRowsColumnsView', pagination: false, /** * @inheritdoc */ initialize: function(options) { if (!_.contains(this.plugins, 'ReportExport')) { this.plugins.push('ReportExport'); } this.events = _.extend({}, this.events, { 'click [data-action=export-csv]': 'exportToCsv', 'click [data-action=show-simplified]': 'showSimplified', }); this._super('initialize', [options]); this.template = app.template.getView('summation', this.module); }, /** * @inheritdoc */ _initProperties: function() { this._super('_initProperties'); this.orderByKeys.push('summaryOrderBy'); }, /** * Build collection * * @param {Array} data */ buildCollection: function(data) { this.data = data; const records = data.records; const header = data.header || this._fields.visible; const shouldRerender = true; this.reportComplexity = this._getReportComplexity(_.size(records), _.size(header)); if (_.has(this, 'layout') && this.layout) { this.exportAccess = app.acl.hasAccess('export', this.layout.module) && app.utils.reports.hasAccessToAllReport(this.layout.model, 'export'); } if (this.reportComplexity === this.complexities.medium && !this.viewingSimplified) { this.context.trigger('report:data:table:loaded', false, 'table'); return; } this.startBuildCollection(data, !shouldRerender); }, /** * Start to build the data collection * * @param {Array} data * @param {boolean} shouldRerender */ startBuildCollection: function(data, shouldRerender) { this._initCollection(); this.collection.models = data.records; this.collection.length = data.records.length; this.loading = false; this.context.trigger('report:data:table:loaded', this.loading, 'table'); if (shouldRerender) { this.render(); } const visibleEmptyPanel = this._isEmptyPanel(data) || !this.layout || !app.utils.reports.hasAccessToAllReport(this.layout.model); this._toggleEmptyPanel(visibleEmptyPanel); }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.loading) { this._adjustLoadingWidgetSize('table'); } if (this.reportComplexity === this.complexities.medium) { this.context.trigger('toggle-orientation-buttons', false); } else { this.context.trigger('toggle-orientation-buttons', true); } }, }) }, "report-advanced-filters": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportAdvancedFiltersView * @alias SUGAR.App.view.views.BaseReportsReportAdvancedFiltersView * @extends View.Views.Base.View */ ({ // Report-advanced-filters View (base) className: 'advanced-filters-container w-full h-full m-4', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Init properties */ _initProperties: function() { this._flattenedFilters = {}; this._filtersData = {}; const elementId = app.utils.generateUUID(); const tooltipId = app.utils.generateUUID(); const reportData = this.context.get('reportData'); const filtersData = reportData.get('filtersDef'); if (_.isEmpty(filtersData)) { return; } this._hasFlattenFilters = true; this._flattenFilters(elementId, tooltipId, filtersData.Filter_1, this._flattenedFilters, 0, 5); this._flattenedFilters = _.sortBy(this._flattenedFilters, 'row'); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._addMargins(); this._createLines(); this._recalculateContainerHeight(); this.$('[data-widget="non-runtime-filter"]').tooltip({ title: app.lang.get('LBL_ORIGINAL_DESIGN_FILTER'), }); this._hideDrawerTabs(); }, /** * Add margins to the elements */ _addMargins: function() { const tailwindPxPerUnit = 4; const marginType = app.lang.direction === 'rtl' ? 'margin-right' : 'margin-left'; _.each(this._flattenedFilters, function addMargins(widget) { const elWidget = this.$('#' + widget.elementId); elWidget.css(marginType, `${widget.column * tailwindPxPerUnit}px`); }, this); }, /** * Create lines between linked elements */ _createLines: function() { _.each(this._flattenedFilters, function createLine(widget) { let posY = false; let posX = false; _.each(widget.children, function buildLine(childId) { const color = widget.value === 'LBL_OR' ? '#ffd132' : '#00e0e0'; const elWidget = this.$('#' + widget.elementId); const data = this._generateLineHtml(elWidget, this.$('#' + childId), 2, color, posY); this.$('[data-container="advanced-filters-widget-container"]').append(data.html); posX = data.posX; posY = data.posY; }, this); if (widget.type === 'operator' && widget.children.length > 1) { const pillHtml = this._generatePillHtml(widget, posX); this.$('[data-container="advanced-filters-widget-container"]').append(pillHtml); } if (widget.type === 'condition') { this.$(`#${widget.tooltipId}`).tooltip({ delay: 1000, container: 'body', placement: 'bottom', title: widget.tooltipDescription, html: true, trigger: 'hover', }); } }, this); }, /** * Generate html code for the operator pill * * @param {Object} widget * @param {number} posX * * @return {string} */ _generatePillHtml: function(widget, posX) { const pillWidth = 40; const firstChildId = _.first(widget.children); const lastChildId = _.last(widget.children); const firstChildTop = this.$('#' + firstChildId).position().top; const lastChildTop = this.$('#' + lastChildId).position().top; const parentTop = this.$('#' + widget.elementId).position().top; posX = posX - pillWidth / 2; const posY = (lastChildTop - parentTop) / 2 + firstChildTop; let pillEl = document.createElement('div'); pillEl.style[app.lang.direction === 'rtl' ? 'right' : 'left'] = `${posX}px`; pillEl.style.top = `${posY}px`; pillEl.innerText = app.lang.get(widget.value); pillEl.className = widget.operator === 'OR' ? 'advanced-operator-pill-or' : 'advanced-operator-pill-and'; return pillEl.outerHTML; }, /** * Recalculate container height base on panel height */ _recalculateContainerHeight: function() { const offset = 50; const containerHeight = this.$('[data-container="advanced-container"]').height(); this.$('[data-container="advanced-filters-widget-container"]').height(containerHeight - offset); }, /** * Recursively parse filters and store them into a one dimensional object * * @param {string} elementId * @param {string} tooltipId * @param {Object} filterDefs * @param {Object} flattenedFilters * @param {number} row * @param {number} column * * @return {number} */ _flattenFilters: function(elementId, tooltipId, filterDefs, flattenedFilters, row, column) { const marginTopOffset = 5; const marginLeftOffset = 15; if (filterDefs.operator) { const isOROperator = filterDefs.operator === 'OR'; flattenedFilters[elementId] = { type: 'operator', operator: filterDefs.operator, value: isOROperator ? 'LBL_OR' : 'LBL_AND_UPPERCASE', description: isOROperator ? 'LBL_ADVANCED_OR_DESC' : 'LBL_ADVANCED_AND_DESC', startClass: isOROperator ? 'advanced-or-start' : 'advanced-and-start', endClass: isOROperator ? 'advanced-or-end' : 'advanced-and-end', children: [], elementId, tooltipId, row, column, }; row = row + marginTopOffset; column = column + marginLeftOffset; _.each(filterDefs, function flatten(subFilterDefs, key) { if (key === 'operator') { return; } const subElementId = app.utils.generateUUID(); const subElementTooltipId = app.utils.generateUUID(); row = this._flattenFilters( subElementId, subElementTooltipId, subFilterDefs, flattenedFilters, row, column ); flattenedFilters[elementId].children.push(subElementId); }, this); } else { const filterDescription = this._getRuntimeFilterDescription(filterDefs); flattenedFilters[elementId] = { type: 'condition', value: filterDescription.summaryText, tooltipDescription: filterDescription.tooltipDescription, runtime: filterDefs.runtime === 1, elementId, tooltipId, row, column, }; row = row + marginTopOffset; } return row; }, /** * Creates a summary text from a filter def * * @param {Object} filterData * * @return {string} */ _getRuntimeFilterDescription: function(filterData) { const runtimeFilterId = app.utils.generateUUID(); const runtimeFilterWidget = app.view.createView({ type: 'report-runtime-filter-widget', context: this.context, reportData: this.context.get('reportData'), filterData, runtimeFilterId, }); if (!runtimeFilterWidget._targetField) { runtimeFilterWidget.dispose(); return ''; } const completeDescription = runtimeFilterWidget._getTooltipText(); runtimeFilterDescription = { summaryText: completeDescription.replaceAll('<br>', ' '), tooltipDescription: completeDescription, }; runtimeFilterWidget.dispose(); return runtimeFilterDescription; }, /** * Create the html of a line between two elements * * @param {jQuery} parent * @param {jQuery} child * @param {number} lineWidth * @param {string} color * @param {number} calculatedPosY * * @return {string} */ _generateLineHtml: function(parent, child, lineWidth, color, calculatedPosY) { const isRTL = app.lang.direction === 'rtl'; const marginClass = isRTL ? 'margin-right' : 'margin-left'; const posX = parseFloat(parent.css(marginClass)) + parseFloat(parent.css('width')) - lineWidth / 2; const posY = calculatedPosY ? calculatedPosY : child.position().top; const width = parseFloat(child.css(marginClass)) - posX - lineWidth / 2; const height = child.position().top - parent.position().top - parent.outerHeight() / 2 - lineWidth / 2; let lineEl = document.createElement('div'); lineEl.style.position = 'absolute'; lineEl.style[isRTL ? 'borderRight' : 'borderLeft'] = `solid ${lineWidth}px ${color}`; lineEl.style.borderBottom = `solid ${lineWidth}px ${color}`; lineEl.style[isRTL ? 'right' : 'left'] = `${posX}px`; lineEl.style.top = `${posY}px`; lineEl.style.width = `${width}px`; lineEl.style.height = `${height}px`; return { html: lineEl.outerHTML, posX, posY, }; }, /** * Hide drawer tabs */ _hideDrawerTabs: function() { let sideDrawer = this.closestComponent('side-drawer'); sideDrawer.$('.drawer-tabs').hide(); sideDrawer.$('button[data-action="drawerClose"]').click(function() { sideDrawer.$('.drawer-tabs').show(); }); }, }) }, "report-detail-modal": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ReportsReportDetailModalView * @alias SUGAR.App.view.views.BaseReportsReportDetailModalView * @extends View.View */ ({ // Report-detail-modal View (base) /** * @inheritdoc */ events: { 'click .close': 'closeModal', 'click [class="modal-backdrop in"]': 'closeModal', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Init properties */ _initProperties: function() { this._createReportDetails(false); }, /** * Register events */ _registerEvents: function() { const forceRender = true; this.listenTo( this.context, 'report:savedReportsMeta:sync:complete', this._createReportDetails, this, forceRender ); this.listenTo( this.model, 'sync', this._createReportDetails, this, forceRender ); }, /** * Open Detail Modal */ openModal: function() { this.render(); let modalEl = this.$('[data-content=report-details-modal]'); modalEl.modal({ backdrop: 'static' }); modalEl.modal('show'); modalEl.on('hidden.bs.modal', _.bind(function handleModalClose() { this.$('[data-content=report-details-modal]').remove(); }, this)); }, /** * Prepare Report Details data to be displayed * * @param {boolean} forceRender */ _createReportDetails: function(forceRender) { if (!this.model || !this.context || !this.model.dataFetched) { return; } const encodedReportContent = this.model.get('content'); if (!encodedReportContent) { return; } const reportContent = JSON.parse(encodedReportContent); const reportNameKey = 'report_name'; const reportTypeKey = 'report_type'; const reportType = this.model.get(reportTypeKey); const type = app.lang.getAppListStrings('dom_report_types')[reportType]; const assignedUser = this.model.get('assigned_user_name'); const reportSchedules = this._createReportSchedules(); const reportName = reportContent[reportNameKey]; const modules = this._getReportModules(reportContent); const displayColumns = this._getReportDisplayColumns(reportContent); const groupBy = this._getReportGroupBy(reportContent); const teams = this._getReportTeams(); const summaryColumns = this._getReportSummaryColumns(reportContent); this.reportDetails = { reportName, modules, displayColumns, groupBy, reportSchedules, teams, assignedUser, summaryColumns, type, }; if (forceRender === true) { this.render(); } }, /** * Create a report schedules list * * @return {Array} */ _createReportSchedules: function() { let reportSchedules = []; const savedReportsMeta = this.context.get('savedReportsMeta'); const nextRunKey = 'next_run'; if (_.isEmpty(savedReportsMeta) || !_.has(savedReportsMeta, 'scheduler') || _.size(savedReportsMeta.scheduler) < 1) { return reportSchedules; } reportSchedules = app.utils.deepCopy(savedReportsMeta.scheduler); _.each(reportSchedules, function map(item) { item[nextRunKey] = app.date(item[nextRunKey]).formatUser(false); }, this); return reportSchedules; }, /** * Create a full modules list * * @param {Object} reportContent * * @return {Array} */ _getReportModules: function(reportContent) { let fullTableList = []; const appModuleListString = app.lang.getAppListStrings('moduleList'); const fullTableListKey = 'full_table_list'; if (_.size(reportContent[fullTableListKey]) < 1) { return fullTableList; } fullTableList = _.map(reportContent[fullTableListKey], function each(item) { if (!_.has(item, 'name') || !item.name) { if (_.has(item, 'module') && item.module && !_.has(fullTableList, item.module)) { const moduleKey = item.module; let moduleName = moduleKey; if (_.has(appModuleListString, moduleName)) { moduleName = appModuleListString[moduleName]; } return moduleName; } } else { if (!_.has(fullTableList, item.name)) { const moduleName = item.name; return moduleName; } } }, this); return fullTableList; }, /** * Create the display columns data to be displayed * * @param {Object} reportContent * * @return {string} */ _getReportDisplayColumns: function(reportContent) { const displayColumnsKey = 'display_columns'; const displayColumns = _.chain(reportContent[displayColumnsKey]) .map(function each(item) { return item.label; }) .join(', ') .value(); return displayColumns; }, /** * Create the group by data to be displayed * * @param {Object} reportContent * * @return {Array} */ _getReportGroupBy: function(reportContent) { let fullTableList = []; const orderByKey = 'group_defs'; if (_.size(reportContent[orderByKey]) < 1) { return fullTableList; } fullTableList = _.map(reportContent[orderByKey], function each(item) { const groupName = item.name; if (_.has(item, 'label')) { return item.label; } return groupName; }, this); return fullTableList; }, /** * Create the teams data to be displayed * * @return {Array} */ _getReportTeams: function() { const teamNames = this.model.get('team_name'); const teamsList = _.chain(teamNames) .sortBy(function sort(team) { return team.primary !== true; }) .map(this._createTeamName, this) .value(); return teamsList; }, /** * Create team name to be displayed * * @param {Object} team * * @return {string} */ _createTeamName: function(team) { let teamName = ''; const firstNameKey = 'name'; const secondNameKey = 'name_2'; const firstName = team[firstNameKey]; const secondName = team[secondNameKey]; const primaryLabel = app.lang.get('LBL_COLLECTION_PRIMARY'); if (team[secondNameKey]) { teamName = `${firstName} ${secondName}`; } else { teamName = `${firstName}`; } if (team.primary) { teamName += ` (${primaryLabel})`; } return teamName; }, /** * Create the group by data to be displayed * * @param {Object} reportContent * * @return {Array} */ _getReportSummaryColumns: function(reportContent) { let fullTableList = []; const orderByKey = 'summary_columns'; if (_.size(reportContent[orderByKey]) < 1) { return fullTableList; } fullTableList = _.map(reportContent[orderByKey], function each(item) { const groupName = item.name; if (_.has(item, 'label')) { return item.label; } return groupName; }, this); return fullTableList; }, /** * Close the modal and destroy it */ closeModal: function() { this.dispose(); }, /** * @inheritdoc */ _dispose: function() { this.$('[data-content=report-details-modal]').remove(); $('.modal-backdrop').remove(); this._super('_dispose'); }, }) }, "report-table-pagination": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Reports.ListPaginationView * @alias SUGAR.App.view.views.ReportsListPaginationView * @extends View.Views.Base.BaseListPaginationView */ ({ // Report-table-pagination View (base) extendsFrom: 'ListPaginationView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialize helper data */ _initProperties: function() { if (this.layout && this.layout.options) { this._panelWrapper = this.layout.options.panelWrapper; } this.context.set('isUsingListPagination', true); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'report:data:table:build:count', this.buildCollectionCount); this.listenTo(this._panelWrapper, 'panel:collapse', this.togglePaginationWidget, this); this.listenTo(this._panelWrapper, 'panel:minimize', this.togglePaginationWidget, this); }, /** * @inheritdoc */ getPageCount: function() { if (this.context.get('previewMode')) { app.alert.show('report-preview-limitation', { level: 'warning', messages: app.lang.get('LBL_REPORTS_PREVIEW_LIMITATION'), autoClose: true }); return; } this._super('getPageCount'); }, /** * @inheritdoc */ getPage: function(page) { if (this.context.get('previewMode')) { app.alert.show('report-preview-limitation', { level: 'warning', messages: app.lang.get('LBL_REPORTS_PREVIEW_LIMITATION'), autoClose: true }); return; } if (_.isString(page)) { page = parseInt(page); } let options = { reset: true, page: page, limit: this.collection.getOption('limit'), strictOffset: true }; this.page = page; options.success = _.bind(this.successPagination, this); options.error = _.bind(this.errorPagination, this); if (this.restoreFromCache()) { options.success(false, false); } else { options.functionContext = this.layout.getComponent('rows-columns'); this.context.trigger('rows-columns:load:collection', options); } }, /** * Success pagination * * @param {Object} data * @param {boolean} shouldCache */ successPagination: function(data, shouldCache = true) { this.layout.trigger('list:paginate:success'); this.context.trigger('report:data:table:loaded', false, 'table'); // Tell the side drawer that there are new records to look at if (app.sideDrawer) { app.sideDrawer.trigger('sidedrawer:collection:change', this.collection); } if (!_.isEmpty(data)) { const rowsAndColumnsComp = this.layout.getComponent('rows-columns'); this.context.set('rebuildData', true); rowsAndColumnsComp.setData(data); rowsAndColumnsComp.render(); } this.collection.page = this.page; this.collection.length = this.collection.models.length; // update count label this.context.trigger('list:paginate'); this.render(); if (shouldCache) { this.setCache(); } this.context.trigger('report:data:table:build:count', this.collection); }, /** * Error pagination handling * * @param {Object} error */ errorPagination: function(error) { app.alert.show('list-pagination-error', { level: 'error', title: error.responseText, }); }, /** * @inheritdoc */ restoreFromCache: function() { if (!this.cachedCollection[this.page]) { return false; } const cache = this.cachedCollection[this.page]; this.collection.next_offset = cache.next_offset; this.collection.page = cache.page; this.collection.models = cache.models; this.page = cache.page; this.context.set('rebuildData', false); const rowsAndColumnsComp = this.layout.getComponent('rows-columns'); rowsAndColumnsComp.collection = this.collection; rowsAndColumnsComp.render(); return true; }, /** * Hide/show pagination * * @param {boolean} hide */ togglePaginationWidget: function(hide) { if (hide) { this.$el.hide(); } else { this.$el.show(); } }, /** * Build collection count field * * @param {Object} collection */ buildCollectionCount: function(collection) { if (this.disposed || _.isUndefined(collection)) { return; } this._disposeCollectionCount(); this.collectionCount = app.view.createField({ def: { type: 'collection-count', name: 'CollectionCount', }, view: this, viewName: 'detail', model: this.model, collection }); this.collectionCount.cachedCount = collection.total; this.collectionCount.updateCount(); let countText = this.collectionCount.$el.find('.count').html(); const showingLabel = app.lang.get('LBL_SHOWING', 'Reports'); countText = `${showingLabel} ${countText}`; this.collectionCount.$el.find('.count').html(countText); this.collectionCount.$el.find('.count').removeClass('count'); this.$('[data-container="collection-count-widget-container"]').append(this.collectionCount.$el); }, /** * Dispose subcomponent */ _disposeCollectionCount: function() { if (this.collectionCount) { this.collectionCount.dispose(); this.collectionCount = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeCollectionCount(); this._super('_dispose'); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "report-panel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.ReportPanelLayout * @alias SUGAR.App.view.layouts.BaseReportsReportPanelLayout * @extends View.Views.Base.Layout */ ({ // Report-panel Layout (base) className: 'flex w-full multi-line-list-view report-panel', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._initContextFields(); this._registerEvents(); }, /** * Init Properties */ _initProperties: function() { this._endpoint = 'panel'; this._splitScreen = null; this._filterPanel = null; }, /** * Initialize model persistent options */ _initContextFields: function() { let fields = this.context.get('fields'); const requiredFields = [ 'chart_type', 'content', 'report_type', 'teams', 'team_name', 'assigned_user_name', 'is_template', ]; fields = _.union(fields, requiredFields); this.context.set('fields', fields); }, /** * Set this._panels when component is initialized. */ _initPanels: function() { if (!this.model.get('id')) { const config = {}; this._buildLayout(config); return; } let url = app.api.buildURL('Reports/' + this._endpoint, this.model.get('id')); app.api.call('read', url, {}, { success: _.bind(function(data) { if (this.disposed) { return; } if (_.isEmpty(data)) { data = {}; } else { data = JSON.parse(data); } this._buildLayout(data); }, this), error: function(error) { app.alert.show('error_while_retrieve', { level: 'error', messages: ['ERR_HTTP_500_TEXT_LINE2'] }); app.logger.error(error); } }); }, /** * Set the visibility of the chart * * @param {string} reportType * @param {string} chartType * @param {Object} config * * @return {Object} */ _manageChartVisibility: function(reportType, chartType, config) { if (reportType === 'tabular' || chartType === 'none') { config.hidden = 'firstScreen'; } return config; }, /** * Manage the visibility of the components on preview mode * * @param {Object} config * @return {Object} */ _managePreviewComponentsVisibility: function(config) { const previewData = this.context.get('previewData'); const reportType = previewData.reportType; const chartType = previewData.chartType; const reportConfig = this._manageChartVisibility(reportType, chartType, config); return reportConfig; }, /** * Manage the visibility of the components * * @param {Object} config * @return {Object} */ _manageComponentsVisibility: function(config) { const reportType = this.model.get('report_type'); const chartType = this.model.get('chart_type'); const reportConfig = this._manageChartVisibility(reportType, chartType, config); return reportConfig; }, /** * Setup layout components depending on preview mode * * @param {Object} config */ _buildLayout: function(config) { let reportConfig = {}; if (this.context.get('previewMode')) { reportConfig = this._managePreviewComponentsVisibility(config); } else { reportConfig = this._manageComponentsVisibility(config); } this._setupComponents(reportConfig); }, /** * Register panel related events */ _registerEvents: function() { this.listenToOnce(this.model, 'sync', this._initPanels); this.listenTo(this.context, 'split-screens-resized', this.handleSave); this.listenTo(this.context, 'change:reportComplexity', this.reportComplexityChanged); }, /** * Disable the resizer when the report is in simplified view * * @param {Context} context * @param {Integer} reportComplexity */ reportComplexityChanged: function(context, reportComplexity) { if (!_.isUndefined(this.context.get('reportComplexities')) && reportComplexity === this.context.get('reportComplexities').medium) { this._splitScreen.toggleResizer(false); } else { this._splitScreen.toggleResizer(true); } }, /** * Setup panels splitLayoutConfig * * @param {Object} splitLayoutConfig */ _setupComponents: function(splitLayoutConfig) { this._createSplitLayout(splitLayoutConfig); this._createFiltersLayout(); this.context.trigger('report-layout-config-retrieved', splitLayoutConfig); }, /** * Create split layout * * @param {Object} splitLayoutConfig */ _createSplitLayout: function(splitLayoutConfig) { if (this._splitScreen) { this._splitScreen.dispose(); this._splitScreen = null; } this._splitScreen = app.view.createLayout({ name: 'resizable-split-screens', layout: this, context: this.context, meta: { name: 'resizable-split-screens', isLoading: false, components: [ { layout: 'report-chart', }, { layout: 'report-table', }, ], secondScreenStyle: { overflow: 'hidden', }, handleDisabled: this.context.get('previewMode'), }, }); this._splitScreen.initComponents(); this._splitScreen.render(); this.$el.html(this._splitScreen.$el); this.context.trigger('split-screens-config-change', splitLayoutConfig, true); }, /** * Create filters layout */ _createFiltersLayout: function() { if (this._filterPanel) { this._filterPanel.dispose(); this._filterPanel = null; } this._filterPanel = app.view.createLayout({ name: 'report-filters', layout: this, context: this.context, }); this._filterPanel.initComponents(); this._filterPanel.render(); this.$el.append(this._filterPanel.$el); }, /** * Saves current model metadata * * @param {Object} resizeConfig */ handleSave: function(resizeConfig) { if (!app.acl.hasAccessToModel('read', this.model)) { this.model.unset('updated'); return; } let url = app.api.buildURL('Reports/panel', this.model.get('id')); if (_.isUndefined(resizeConfig)) { return; } const data = { layoutConfig: resizeConfig, }; app.api.call('update', url, data, { success: _.bind(function(data) { if (!this.disposed) { this.model.unset('updated'); } }, this), error: function(error) { app.alert.show('error_while_save', { level: 'error', title: app.lang.get('ERR_INTERNAL_ERR_MSG'), messages: ['ERR_HTTP_500_TEXT_LINE1', 'ERR_HTTP_500_TEXT_LINE2'] }); app.logger.error(error); } }); }, /** * @inheritdoc */ _dispose: function() { if (this._splitScreen instanceof app.view.Layout) { this._splitScreen.dispose(); this._splitScreen = null; } if (this._filterPanel instanceof app.view.Layout) { this._filterPanel.dispose(); this._filterPanel = null; } this._super('_dispose'); }, }) }, "report-filters": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportFiltersLayout * @alias SUGAR.App.view.views.BaseReportsReportFiltersLayout * @extends View.Views.Base.Layout */ ({ // Report-filters Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialize controller's */ _initProperties: function() { this._layoutConfigRetrieved = false; this._filtersRetrieved = false; this._hadUserInteraction = false; this._isVisible = false; }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'toggle:advanced:filters', this.toggleAdvancedFilters); this.listenTo(this.context, 'split-screens-visibility-change', this.setVisibilityState); this.listenTo(this.context, 'report-layout-config-retrieved', this.layoutConfigRetrieved); this.listenTo(this.context, 'report:data:filters:loaded', this.runtimeFiltersLoaded); }, /** * Handle Visibility State */ runtimeFiltersLoaded: function() { const runtimeFilters = this.getComponent('report-filters').getRawRuntimeFilters(); const numberOfFilters = _.keys(runtimeFilters).length; this.context.trigger('filters-container-content-loaded', { filtersActive: this._isVisible, numberOfFilters, }); if (this._hadUserInteraction) { return; } const initialConfigurationOfTheFilterPanel = _.isUndefined(this._isVisible); if (initialConfigurationOfTheFilterPanel) { const hasFilters = numberOfFilters > 0; this._isVisible = hasFilters; this.$el.toggleClass('!hidden', !this._isVisible); } }, /** * Handle Visibility State * * @param {Object} config */ layoutConfigRetrieved: function(config) { if (this._hadUserInteraction) { return; } this._isVisible = config.filtersActive; this.$el.toggleClass('!hidden', !this._isVisible); this.context.trigger('filters-container-content-loaded', { filtersActive: this._isVisible, }); }, /** * Handle Visibility State * * @param {Object} config */ setVisibilityState: function(config) { this._hadUserInteraction = true; this._isVisible = config.filtersActive; this.$el.toggleClass('!hidden', !this._isVisible); }, /** * Show Advanced Filters View */ toggleAdvancedFilters: function() { this.context.disableRecordSwitching = true; this.context.hideRecordSwitching = true; this.context.set({ runtimeFilters: this.getComponent('report-filters').getRuntimeFilters(), reportData: this.getComponent('report-filters').getReportData(), }); app.sideDrawer.open({ layout: 'report-advanced-filters', context: this.context, }); this.updateCloseButtonLabel(); }, /** * Update close button label */ updateCloseButtonLabel: function() { const closeTitle = app.lang.get('LBL_ADVANCED_CLOSE', 'Reports'); app.sideDrawer.$('button[data-action="drawerClose"]').attr({ 'data-original-title': closeTitle, 'title': closeTitle }); } }) }, "drillthrough-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.DrillthroughListLayout * @alias SUGAR.App.view.layouts.BaseReportsDrillthroughListLayout * @extends View.Views.Base.ListLayout */ ({ // Drillthrough-list Layout (base) extendsFrom: 'ListLayout', /** * @inheritdoc */ initialize: function(options) { // from this level down we should use target module instead of Reports // model needs to be a target module bean so some list level operations can work // for different module // for example, the function _filterMeta relies on the correct model var chartModule = options.context.get('chartModule'); options.context.set('model', app.data.createBean(chartModule)); options.context.set('collection', app.data.createBeanCollection(chartModule)); options.module = chartModule; this._super('initialize', [options]); }, }) }, "drillthrough-pane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Reports.DrillthroughPaneLayout * @alias SUGAR.App.view.layouts.ReportsDrillthroughPaneLayout * @extends View.Layout */ ({ // Drillthrough-pane Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // configuration from clicked dashlet var config = this.context.get('dashConfig'); var metadata = { component: 'saved-reports-chart', name: 'saved-reports-chart', type: 'saved-reports-chart', label: config.label || app.lang.get('LBL_DASHLET_SAVED_REPORTS_CHART', 'Reports'), description: 'LBL_DASHLET_SAVED_REPORTS_CHART_DESC', // module: this.context.get('module'), // this breaks Dashlet plugin at context.parent module: null, config: [], preview: [] }; var field = { type: 'chart', name: 'chart', label: 'LBL_CHART', view: 'detail', module: metadata.module, customLegend: true, }; var component = { name: metadata.component, type: metadata.type, preview: true, context: this.context, module: metadata.module, custom_toolbar: 'no', chart: field }; component.view = _.extend({module: metadata.module}, metadata.preview, component); this.initComponents([{ layout: { type: 'dashlet-grid-wrapper', css_class: 'dashlet-drill h-full mx-2', config: false, preview: true, label: metadata.label, module: metadata.module, context: this.context, components: [ component ] } }], this.context); this.on('click:refresh_list_chart', this.refreshListChart, this); }, /** * @inheritdoc */ render: function() { var config = this.context.get('dashConfig'); // Set the title of the side pane // label coming out of BWC html enoded, decode it first this.model.setDefault('title', $('<div/>').html(config.label).text()); this._super('render'); var dashlet = this.getComponent('dashlet-grid-wrapper').getComponent('saved-reports-chart'); var config = this.context.get('dashConfig'); var chartData = this.context.get('chartData'); var reportData = this.context.get('reportData'); var chartLabels = {groupLabel: config.groupLabel, seriesLabel: config.seriesLabel}; this.context.set('chartLabels', chartLabels); var title = dashlet.$('.dashlet-title'); const dashletGridWrapper = this.getComponent('dashlet-grid-wrapper'); dashletGridWrapper.$el.removeClass('dashlet-preview pt-4 px-2'); // This will allow scrolling when drilling thru from Report detail view // but will respect the dashlet setting when drilling thru from SRC config.allowScroll = true; dashlet.settings.set(config); dashlet.reportData.set('rawChartParams', config); dashlet.reportData.set('rawReportData', reportData); // set reportData's rawChartData to the chartData from the source chart // this will trigger chart.js' change:rawChartData and the chart will update dashlet.reportData.set('rawChartData', chartData); return this; }, /** * Refresh list and chart */ refreshListChart: function() { var drawer = this.closestComponent('drawer').getComponent('drillthrough-drawer'); drawer.updateList(); var dashlet = this.getComponent('dashlet-grid-wrapper').getComponent('saved-reports-chart'); dashlet.loadData(); } }) }, "report-side-drawer-preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Reports.ReportSideDrawerPreviewLayout * @alias SUGAR.App.view.layouts.ReportsReportSideDrawerPreviewLayout */ ({ // Report-side-drawer-preview Layout (base) /** * @inheritdoc */ render: function() { this._super('render'); this.renderPreview(); this.$('.preview-headerbar h1').toggleClass('ml-5', true); this.$('.preview-headerbar').toggleClass('justify-between', true); }, /** * Renders the preview dialog with the data from the current model and collection * @param model Model for the object to preview * @param newCollection Collection of related objects to the current model */ renderPreview: function(model, newCollection) { const listComponent = this._getListComponent(); let context = new app.Context(); context.set('model', this.model); context.set('module', listComponent.module); context.set('collection', listComponent.collection); const previewLayout = app.view.createLayout({ type: 'preview', name: 'preview', context: context, layout: this.layout, meta: { 'components': [ { view: 'preview-header', context: context, }, { view: 'preview', context: context, }, ], 'editable': true, } }); previewLayout.initComponents(); previewLayout.render(); this.$el.html(previewLayout.$el); this._disposeEvents(); }, /** * Retrieves the list component from the layout * @returns {View.View} */ _getListComponent: function() { const previewError = app.lang.get('LBL_PREVIEW_ERROR'); let listComponent = null; if (this.layout && this.layout.layout) { const listSide = this.layout.layout.getComponent('list-side'); if (listSide) { listComponent = listSide.getComponent('drillthrough-list'); } } if (!listComponent) { app.alert.show('preview_error', { level: 'error', messages: previewError }); } return listComponent; }, /** * Disposes of the events */ _disposeEvents: function() { app.events.off('preview:render', this.renderPreview, this); }, /** * @inheritdoc */ _dispose: function() { this._disposeEvents(); this._super('_dispose'); } }) }, "report-side-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.ReportSideDrawerLayout * @alias SUGAR.App.view.layouts.ReportsReportSideDrawerLayout * @extends View.Layout */ ({ // Report-side-drawer Layout (base) plugins: ['ShortcutSession'], shortcuts: [ 'Sidebar:Toggle', 'List:Headerpane:Create', 'List:Select:Down', 'List:Select:Up', 'List:Scroll:Left', 'List:Scroll:Right', 'List:Select:Open', 'List:Inline:Edit', 'List:Delete', 'List:Inline:Cancel', 'List:Inline:Save', 'List:Favorite', 'List:Follow', 'List:Preview', 'List:Select', 'SelectAll:Checkbox', 'SelectAll:Dropdown', 'Filter:Search', 'Filter:Create', 'Filter:Edit', 'Filter:Show' ], /** * This causes the focus drawer to close when a drawer with this * layout closes */ closeFocusDrawer: true, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); /** * Cache for enum and enum like values */ this.enums = {}; this._registerEvents(); }, /** * @inheritdoc */ render: function() { this._super('render'); this.togglePreview(); const recordlistComponent = this.getSideComp('recordlist', true); if (recordlistComponent) { // allow selected records to persist during sorting recordlistComponent.independentMassCollection = true; } }, /** * Show the preview component */ togglePreview: function(previewEnabled, model) { const previewError = app.lang.get('LBL_PREVIEW_ERROR'); const previewComponent = this.getSideComp('report-side-drawer-preview'); const chartComponent = this.getSideComp('report-side-drawer-chart'); if (!previewComponent || !chartComponent) { app.alert.show('preview_error', { level: 'error', messages: previewError }); return; } if (previewEnabled) { chartComponent.hide(); previewComponent.show(); previewComponent.model = model; previewComponent.render(); app.events.trigger('list:preview:decorate', model, this); } else { previewComponent.$el.empty(); previewComponent.hide(); chartComponent.show(); app.events.trigger('list:preview:decorate', null, this); } }, /** * Get component by name * @param {string} componentName * @param {boolean} drillComponent * @returns */ getSideComp: function(componentName, drillComponent = false) { let component = null; const rowHolder = this._getRowHolderComponent(); if (!rowHolder) { return component; } const drillIndex = 0; const reportIndex = 1; const targetIndex = drillComponent ? drillIndex : reportIndex; const listSide = rowHolder._components[targetIndex]; if (!listSide) { return component; } if (drillComponent) { const drillList = listSide.getComponent('drillthrough-list'); if (drillList) { component = drillList.getComponent(componentName); } } else { component = listSide.getComponent(componentName); } return component; }, /** * Get row holder component * @returns {View.View} rowHolderComponent */ _getRowHolderComponent: function() { let rowHolderComponent = null; const columnHeader = this.getComponent('column-holder'); if (!columnHeader) { return rowHolderComponent; } const rowHolder = columnHeader.getComponent('row-holder'); if (!rowHolder) { return rowHolderComponent; } return rowHolder; }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'report:side:drawer:list:refresh', this._refreshListChart, this); this.listenTo(app.events, 'preview:render', this._previewRecord, this); this.listenTo(app.events, 'preview:close', this._closePreview, this); }, /** * Preview a record * @param {Bean.Model} model */ _previewRecord: function(model) { model.fetch(); const previewComponent = this.getSideComp('report-side-drawer-preview'); const previewComponentVisible = previewComponent.$el.css('display') !== 'none'; const sameModel = previewComponent.model && previewComponent.model.id === model.id if (previewComponentVisible && sameModel) { this.togglePreview(false, model); } else { this.togglePreview(true, model); } }, /** * Close the preview */ _closePreview: function(event) { this.togglePreview(false); }, /** * Refresh list and chart */ _refreshListChart: function(dashConfigParams, chartState) { if (dashConfigParams) { this.context.set('dashConfig', dashConfigParams); } if (chartState) { this.context.set('chartState', chartState); } this.updateList(); }, /** * Override the default loadData method to allow for manually constructing * context for each component in layout. We are loading data from the * ReportAPI in public method updateList. We need to first get any enum data * so that we can translate to english * * @override */ loadData: function() { const enumsToFetch = this.context.get('enumsToFetch'); // Make requests for any enums here so they can happen while the drawer is still rendering if (!_.isEmpty(enumsToFetch) && _.isEmpty(this.enums)) { this._loadEnumOptions(enumsToFetch); } else { this.updateList(); } }, /** * Make a request for each enum like field so we can reverse lookup values later * * @param enumsToFetch * @private */ _loadEnumOptions: function(enumsToFetch) { const reportDef = this.context.get('reportData'); let count = enumsToFetch.length; const enumSuccess = function(key, data) { count--; // cache the values inverted to help with reverse lookup this.enums[key] = _.invert(data); // update if enum has repeated values if (_.keys(this.enums[key]).length !== _.keys(data).length) { this.enums[key] = {}; _.each(data, function(v, k) { if (_.isUndefined(this.enums[key][v])) { this.enums[key][v] = []; } this.enums[key][v].push(k); }, this); } // I love that I have to simulate Promise.all but anyways, once // we have all our enum data, then make the record list request if (count === 0) { this.updateList(); } }; _.each(enumsToFetch, function(field) { const module = reportDef.full_table_list[field.table_key].module; const key = field.table_key + ':' + field.name; app.api.enumOptions(module, field.name, { success: _.bind(enumSuccess, this, key) }); }, this); }, /** * Fetch report related records based on drawer context as defined in * saved-reports-chart dashlet or Report detail view with context containing * a filter definition based on a chart click event. This method will also * render the list component in layout after data is fetched. */ updateList: function() { const chartModule = this.context.get('chartModule'); const reportId = this.context.get('reportId'); const reportDef = this.context.get('reportData'); const params = this.context.get('dashConfig'); // At this point, we should have finished all translations and requests for translations so // we can finally build the filter in english const filterDef = SUGAR.charts.buildFilter(reportDef, params, this.enums); this.context.set('filterDef', filterDef); const useSavedFilters = this.context.get('useSavedFilters') || false; const useCustomReportDef = this.context.get('useCustomReportDef'); const endpoint = function(method, model, options, callbacks) { const params = _.extend( options.params || {}, {view: 'list', group_filters: filterDef, use_saved_filters: useSavedFilters} ); if (useCustomReportDef) { params.filtersDef = reportDef.filters_def; params.intelligent = reportDef.intelligent; } const url = app.api.buildURL('Reports', 'records', {id: reportId}, params); return app.api.call('read', url, null, callbacks); }; const callbacks = { success: _.bind(function(data) { if (this.disposed) { return; } this.context.trigger('refresh:count'); this.context.trigger('refresh:drill:labels'); }, this), error: function(o) { app.alert.show('listfromreport_loading', { level: 'error', messages: app.lang.get('ERROR_RETRIEVING_DRILLTHRU_DATA', 'Reports') }); }, complete: function(data) { app.alert.dismiss('listfromreport_loading'); } }; this.context.trigger('drawer:reports:list:updated'); let collection = this.context.get('collection'); collection.module = chartModule; collection.model = app.data.getBeanClass(chartModule); collection.setOption('endpoint', endpoint); collection.setOption('fields', this.context.get('fields')); collection.fetch(callbacks); let massCollection = this.context.get('mass_collection'); if (massCollection) { massCollection.setOption('endpoint', endpoint); } }, }) }, "report-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.ReportChartLayout * @alias SUGAR.App.view.layouts.BaseReportsReportChartLayout * @extends View.Layouts.Base.Layout */ ({ // Report-chart Layout (base) /** * Check if we can display the panel */ isValid: function() { return this.model.get('chart_type') !== 'none'; }, /** * Get the title for the panel */ getTitle: function() { const titleMapping = { none: 'LBL_NO_CHART', hBarF: 'LBL_HORIZ_BAR', hGBarF: 'LBL_HORIZ_GBAR', vBarF: 'LBL_VERT_BAR', vGBarF: 'LBL_VERT_GBAR', pieF: 'LBL_PIE', funnelF: 'LBL_FUNNEL', lineF: 'LBL_LINE', donutF: 'LBL_DONUT', treemapF: 'LBL_TREEMAP', }; const chartType = this.model.get('chart_type'); const chartLabel = titleMapping[chartType]; let label = app.lang.get(chartLabel, 'Reports'); label = label ? label : 'LBL_CHART'; return label; }, }) }, "report-panel-wrapper": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ReportsPanelWrapperLayout * @alias SUGAR.App.view.layouts.BaseReportsPanelWrapperLayout * @extends View.Layouts.Base.Layout */ ({ // Report-panel-wrapper Layout (base) /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); }, /** * Before init properties * * @param {Object} options */ _beforeInit: function(options) { this._orderNumber = options.orderNumber; this._containerMeta = false; this._isCollapsed = false; this._minimized = false; this._reportWidget = null; this._animationDuration = 300; this._pillHeight = 26; let pillWidths = { table: 120, filters: 110 }; const pillWidthByChartType = { hBarF: 210, hGBarF: 215, vBarF: 192, vGBarF: 192, pieF: 60, funnelF: 85, lineF: 65, donutF: 80, treemapF: 100, }; const chartType = options.layout.model.get('chart_type'); pillWidths.chart = pillWidthByChartType[chartType]; const panelId = options.layout._panelsDef[options.orderNumber].id; this._pillWidth = pillWidths[panelId]; this._minimizedPos = -1; this._maximizedData = {}; }, /** * Property initialization * * @param {Object} options */ _initProperties: function() { if (_.has(this, 'layout') && _.has(this.layout, 'layout')) { this._headerBar = this.layout.layout.getComponent('report-header'); } }, /** * Build report widget * * @param {Object} meta * * @returns {boolean} */ tryAddWidget: function(meta) { if (!_.has(meta, 'layout')) { return; } this._containerMeta = meta; this.initComponents(); this.render(); this._disposeReportWidget(); this._reportWidget = app.view.createLayout({ name: meta.layout.type, layout: this, context: this.context }); if (this._reportWidget.isValid && !this._reportWidget.isValid()) { return false; } this._reportWidget.initComponents(); this._reportWidget.render(); const widgetContainer = this.$('[widget-container="report-widget-container"]'); widgetContainer.append(this._reportWidget.$el); widgetContainer.addClass(this.model.get('report_type')); this._reportWidget.loadData(); this.listenTo(this._reportWidget, 'panel:widget:finished:loading', this.addPanelToGrid, this); return true; }, /** * Add the panel wrapper to gridstack * * @param {boolean} minimized * @param {boolean} collapsed */ addPanelToGrid: function(minimized, collapsed) { // uncomment this line if we want to only show widgets after loading is already done // this.context.trigger('panel:wrapper:finished:loading', this, this._containerMeta, this._orderNumber) if (this.context.get('previewMode')) { return; } if (collapsed) { this.collapse(true); } if (minimized) { this.minimize(true, false); // maybe change the second param to true so the animation is instant } }, /** * Return container meta * * @return {Object} */ getContainerMeta: function() { return this._containerMeta; }, /** * Checks if the panel is minimized * * @returns {boolean} */ isMinimized: function() { return this._minimized; }, /** * Enlarge panel * * @param {boolean} enlarge */ enlargePanel: function(enlarge) { this._containerMeta.enlarged = enlarge; }, /** * Set initial height * * @param {number} height */ setInitialHeight: function(height) { this._containerMeta.initialHeight = height; }, /** * Get initial height * * @return {number} */ getInitialHeight: function() { return this._containerMeta.initialHeight; }, /** * Notify listeners that the size has changed * */ manageSizeUpdated: function() { this.trigger('grid-panel:size:changed'); }, /** * Handle panel minimized * * @param {boolean} minimized * @param {boolean} instant */ minimize: function(minimized, instant) { if (this.context.get('previewMode') || this._minimized === minimized) { return; } this._minimized = minimized; this._containerMeta.minimized = minimized; this.$el.toggleClass('collapsed', this._minimized || this._isCollapsed); this.$('.thumbnail').toggleClass('collapsed', this._minimized || this._isCollapsed); const grid = this.layout.getGrid(); grid.resizable(this.$el, !this._minimized && !this._isCollapsed); grid.movable(this.$el, !this._minimized); if (this._minimized) { this._goTop(instant); } else { this._goRecord(instant); } this.trigger('panel:minimize', this._minimized, this._minimizedPos, this._containerMeta.id, instant); this.trigger('panel:collapse', this._minimized || this._isCollapsed); if (!minimized) { this._minimizedPos = -1; } }, /** * Put Widget back to record view * * @param {boolean} instant */ _goRecord: function(instant) { const headerBarHeight = this._headerBar.$('.headerpane').height(); const topPos = headerBarHeight + parseInt(this._headerBar.$('.record-cell').css('padding-top')) / 2; this.$el.css({ position: 'absolute', top: -topPos, }); this.$el.animate( this._maximizedData, instant ? 0 : this._animationDuration, _.bind(this._resetWidgetProperties, this) ); }, /** * Move widget to top bar * * @param {boolean} instant */ _goTop: function(instant) { this._minimizedPos = this.layout.getNumberOfMinimizedPanels() + 1; const rightMostButtonsWidth = this._headerBar.$('.btn-toolbar.pull-right').width(); const headerBarHeight = this._headerBar.$('.headerpane').height(); const paddingTop = this._headerBar.$('.record-cell').css('padding-top'); const topPos = headerBarHeight + parseInt(paddingTop) / 2; const leftOffset = this._getLeftOffset(); const leftPos = window.outerWidth - leftOffset - rightMostButtonsWidth; const parentWidth = this.$el.parent().width(); this._maximizedData = { top: this.$el.css('top'), left: (parseInt(this.$el.css('left')) / parentWidth * 100) + '%', width: (this.$el.width() / parentWidth * 100) + '%', height: this.$el.css('height'), }; const widgetOffset = this.$el.offset(); this.$el.css({ 'z-index': '9999', 'min-width': this._pillWidth, 'min-height': this._pillHeight, position: 'fixed', top: widgetOffset.top, left: widgetOffset.left, }); const sizeBeforeMinimize = { width: this.$el.width(), height: this.$el.height(), }; this.$el.removeClass('grid-stack-item'); this.$el.css(sizeBeforeMinimize); this.$el.animate({ top: topPos, left: leftPos, width: this._pillWidth, height: this._pillHeight, }, instant ? 0 : this._animationDuration, _.bind(this._notifyMinimize, this) ); }, /** * Get left offset * * @return {number} */ _getLeftOffset: function() { let pillWidthsOnTheRight = 0; _.each(this.layout._panels, function(panel) { if (panel.isMinimized() && panel._minimizedPos < this._minimizedPos) { pillWidthsOnTheRight += panel._pillWidth; } }, this); let leftOffset = this._pillWidth + pillWidthsOnTheRight; return leftOffset; }, /** * Whenever minimization is done we need to recalculate position */ _notifyMinimize: function() { this._setScreenDependentLeftPos(true); }, /** * Rearange top bar elements if one has been put back to record * * @param {number} panelsMinimized */ recalculateLeftPos: function(panelsMinimized) { if (this._minimizedPos <= panelsMinimized) { return; } this._minimizedPos = Math.max(1, this._minimizedPos - 1); this._setScreenDependentLeftPos(false); }, /** * Transform left attribute into a calculus so it fits all screen sizes * * @param {boolean} snap */ _setScreenDependentLeftPos: function(snap) { const rightMostButtonsWidth = this._headerBar.$('.btn-toolbar.pull-right').width(); if (snap) { const leftOffset = this._getLeftOffset() + rightMostButtonsWidth; this.$el.css('left', 'calc(100% - ' + leftOffset + 'px)'); } else { const leftOffset = this._getLeftOffset(); const leftPos = window.outerWidth - leftOffset - rightMostButtonsWidth; this.$el.animate({ left: leftPos, }, this._animationDuration, _.bind(this._setScreenDependentLeftPos, this, true) ); } }, /** * As soon as the widget is back on record, remove all given attributes */ _resetWidgetProperties: function() { this.$el.addClass('grid-stack-item'); this.$el.css({ 'z-index': '', 'min-width': '', 'min-height': '', top: '', left: '', width: '', height: '', position: '', }); }, /** * Handle panel collapsed/not collapsed * * @param {boolean} collapsed * @param {number} previousHeight */ collapse: function(collapsed, previousHeight) { if (this.context.get('previewMode') || this._isCollapsed === collapsed) { return; } this._isCollapsed = collapsed; this.$el.toggleClass('collapsed', collapsed); this.$('.thumbnail').toggleClass('collapsed', collapsed); this.collapseGrid(previousHeight); this.trigger('panel:collapse', collapsed); }, /** * Is collapsed * * @return {boolean} */ isCollapsed: function() { return this._isCollapsed; }, /** * Collapse or show the whole grid * * @param {number} previousHeight */ collapseGrid: function(previousHeight) { const grid = this.layout.getGrid(); const el = this.$el; const isCollapsed = el.hasClass('collapsed'); const node = el.data('_gridstack_node'); if (previousHeight) { node.height = previousHeight; } if (isCollapsed) { el .data('expand-min-height', parseInt(node.minHeight)) .data('expand-height', parseInt(node.height)); grid .resizable(el, false) .minHeight(el, null) .resize(el, null, 0); } else { grid .resizable(el, true) .minHeight(el, parseInt(el.data('expand-min-height'))) .resize(el, null, parseInt(el.data('expand-height'))); } }, /** * Toggle minimize button * * @param {boolean} show */ toggleMinimizeButton: function(show) { this.trigger('toggle:minimize:button', show); }, /** * Dispose subcomponent */ _disposeReportWidget: function() { if (this._reportWidget) { this._reportWidget.dispose(); this._reportWidget = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeReportWidget(); this._super('_dispose'); }, }) }, "drillthrough-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.DrillthroughDrawerLayout * @alias SUGAR.App.view.layouts.BaseReportsDrillthroughDrawerLayout * @extends View.Layout */ ({ // Drillthrough-drawer Layout (base) plugins: ['ShortcutSession'], shortcuts: [ 'Sidebar:Toggle', 'List:Headerpane:Create', 'List:Select:Down', 'List:Select:Up', 'List:Scroll:Left', 'List:Scroll:Right', 'List:Select:Open', 'List:Inline:Edit', 'List:Delete', 'List:Inline:Cancel', 'List:Inline:Save', 'List:Favorite', 'List:Follow', 'List:Preview', 'List:Select', 'SelectAll:Checkbox', 'SelectAll:Dropdown', 'Filter:Search', 'Filter:Create', 'Filter:Edit', 'Filter:Show' ], /** * This causes the focus drawer to close when a drawer with this * layout closes */ closeFocusDrawer: true, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); /** * Cache for enum and enum like values */ this.enums = {}; }, /** * Override the default loadData method to allow for manually constructing * context for each component in layout. We are loading data from the * ReportAPI in public method updateList. We need to first get any enum data * so that we can translate to english * * @override */ loadData: function() { var enumsToFetch = this.context.get('enumsToFetch'); // Make requests for any enums here so they can happen while the drawer is still rendering if (!_.isEmpty(enumsToFetch) && _.isEmpty(this.enums)) { this._loadEnumOptions(enumsToFetch); } else { this.updateList(); } }, /** * Make a request for each enum like field so we can reverse lookup values later * * @param enumsToFetch * @private */ _loadEnumOptions: function(enumsToFetch) { var reportDef = this.context.get('reportData'); var count = enumsToFetch.length; var enumSuccess = function(key, data) { count--; // cache the values inverted to help with reverse lookup this.enums[key] = _.invert(data); // update if enum has repeated values if (_.keys(this.enums[key]).length !== _.keys(data).length) { this.enums[key] = {}; _.each(data, function(v, k) { if (_.isUndefined(this.enums[key][v])) { this.enums[key][v] = []; } this.enums[key][v].push(k); }, this); } // I love that I have to simulate Promise.all but anyways, once // we have all our enum data, then make the record list request if (count === 0) { this.updateList(); } }; _.each(enumsToFetch, function(field) { var module = reportDef.full_table_list[field.table_key].module; var key = field.table_key + ':' + field.name; app.api.enumOptions(module, field.name, { success: _.bind(enumSuccess, this, key) }); }, this); }, /** * Fetch report related records based on drawer context as defined in * saved-reports-chart dashlet or Report detail view with context containing * a filter definition based on a chart click event. This method will also * render the list component in layout after data is fetched. */ updateList: function() { var chartModule = this.context.get('chartModule'); var reportId = this.context.get('reportId'); var reportDef = this.context.get('reportData'); var params = this.context.get('dashConfig'); // At this point, we should have finished all translations and requests for translations so // we can finally build the filter in english var filterDef = SUGAR.charts.buildFilter(reportDef, params, this.enums); this.context.set('filterDef', filterDef); var useSavedFilters = this.context.get('useSavedFilters') || false; const useCustomReportDef = this.context.get('useCustomReportDef'); var endpoint = function(method, model, options, callbacks) { var params = _.extend(options.params || {}, {view: 'list', group_filters: filterDef, use_saved_filters: useSavedFilters}); if (useCustomReportDef) { params.filtersDef = reportDef.filters_def; params.intelligent = reportDef.intelligent; } var url = app.api.buildURL('Reports', 'records', {id: reportId}, params); return app.api.call('read', url, null, callbacks); }; var callbacks = { success: _.bind(function(data) { if (this.disposed) { return; } this.context.trigger('refresh:count'); this.context.trigger('refresh:drill:labels'); }, this), error: function(o) { app.alert.show('listfromreport_loading', { level: 'error', messages: app.lang.get('ERROR_RETRIEVING_DRILLTHRU_DATA', 'Reports') }); }, complete: function(data) { app.alert.dismiss('listfromreport_loading'); } }; this.context.trigger('drawer:reports:list:updated'); var collection = this.context.get('collection'); collection.module = chartModule; collection.model = app.data.getBeanClass(chartModule); collection.setOption('endpoint', endpoint); collection.setOption('fields', this.context.get('fields')); collection.fetch(callbacks); var massCollection = this.context.get('mass_collection'); if (massCollection) { massCollection.setOption('endpoint', endpoint); } } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Reports.RecordLayout * @alias SUGAR.App.view.layouts.BaseReportsRecordLayout * @extends View.Views.Base.Layout */ ({ // Record Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initSavedReportsMeta(); }, /** * Initialize Saved Reports Meta */ _initSavedReportsMeta: function() { if (!this.model.get('id')) { return; } const reportId = this.model.get('id'); const params = { track: true, trackAction: 'detailview', }; const url = app.api.buildURL('Reports/activeSavedReport', reportId, {}, params); app.api.call('read', url, null, { success: _.bind(this._storeSavedReportsMeta, this), }); }, /** * Store Saved Reports * * @param {Array} savedReports */ _storeSavedReportsMeta: function(savedReports) { if (this.disposed) { return; } this.context.set('savedReportsMeta', savedReports); this.context.trigger('report:savedReportsMeta:sync:complete'); this._manageReportDefChanged(savedReports); }, /** * Take care of report def changes * * @param {Object} reportData */ _manageReportDefChanged: function(reportData) { const lastChangeInfo = reportData.lastChangeInfo; const seenDate = lastChangeInfo.lastReportSeenDate; const modifiedDate = lastChangeInfo.lastReportModifiedDate; // check if the report has been modified since the last time I saw the report if (!moment(seenDate).isBefore(moment(modifiedDate))) { return; } // reset state this._resetUserState(); // show notification if you're not the one that changed the report const currentUserId = lastChangeInfo.currentUserId; const modifiedUserId = lastChangeInfo.modifiedUserId; if (currentUserId !== modifiedUserId) { this._showNotification(); } }, /** * Reset last state */ _resetUserState: function() { const moduleReportId = this.module + ':' + this.context.get('modelId'); const orderByLastStateKey = app.user.lastState.buildKey('order-by', 'record-list', moduleReportId); app.user.lastState.remove(orderByLastStateKey); }, /** * Show report def changed notification */ _showNotification: function() { app.alert.show('modify_since_last_refresh', { level: 'info', messages: app.lang.get('LBL_UPDATES_SINCE_LAST_REFRESH', 'Reports'), autoClose: true, }); }, }) }, "report-side-drawer-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Reports.ReportSideDrawerPaneLayout * @alias SUGAR.App.view.layouts.ReportsReportSideDrawerPaneLayout * @extends SUGAR.App.view.layouts.ReportsReportsDrillthroughPaneLayout */ ({ // Report-side-drawer-chart Layout (base) extendsFrom: 'ReportsDrillthroughPaneLayout', }) }, "report-panel-preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Reports.ReportPanelPreviewLayout * @alias SUGAR.App.view.views.BaseReportsReportPanelPreviewLayout * @extends View.Views.Base.Reports.ReportPanelLayout */ ({ // Report-panel-preview Layout (base) extendsFrom: 'ReportsReportPanelLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initPanels(); }, /** * Init properties */ _initProperties: function() { const previewData = this.layout ? this.layout.options.def.previewData : {}; this._filtersData = previewData.filtersData; this._chartData = previewData.chartData; this._tableData = previewData.tableData; this._reportId = previewData.reportId; this._reportType = previewData.reportType; this.context.set({ previewData: previewData, previewMode: true, }); const contextModel = this.context.get('model'); contextModel.set({ report_type: this._reportType, chart_type: this._filtersData.chart_type, id: this._reportId, }); this._super('_initProperties'); }, /** * Set this._panels when component is initialized. */ _initPanels: function() { if (this._reportId) { this.model.set('id', this._reportId); } this.model.dataFetched = true; this._super('_initPanels'); }, }) } }} , "datas": {} }, "Forecasts":{"fieldTemplates": { "base": { "commitlog": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.CommitlogField * @alias SUGAR.App.view.fields.BaseForecastsCommitlogField * @extends View.Fields.Base.BaseField */ ({ // Commitlog FieldTemplate (base) /** * Stores the historical log of the Forecast entries */ commitLog: [], /** * Previous committed date value to display in the view */ previousDateEntered: '', initialize: function(options) { app.view.Field.prototype.initialize.call(this, options); this.on('show', function() { if (!this.disposed) { this.render(); } }, this); }, bindDataChange: function() { this.collection.on('reset', function() { this.hide(); this.buildCommitLog(); if (this.context.get('forecastType') === 'Direct') { this.show(); } }, this); this.context.on('forecast:commit_log:trigger', function() { if(!this.isVisible()) { this.show(); } else { this.hide(); } }, this); }, /** * Does the heavy lifting of looping through models to build the commit history */ buildCommitLog: function() { //Reset the history log this.commitLog = []; if(_.isEmpty(this.collection.models)) { return; } // get the first model so we can get the previous date entered var previousModel = _.first(this.collection.models); // parse out the previous date entered var dateEntered = new Date(Date.parse(previousModel.get('date_modified'))); if (dateEntered == 'Invalid Date') { dateEntered = previousModel.get('date_modified'); } // set the previous date entered in the users format this.previousDateEntered = app.date.format(dateEntered, app.user.getPreference('datepref') + ' ' + app.user.getPreference('timepref')); //loop through from oldest to newest to build the log correctly var loopPreviousModel = '', models = _.clone(this.collection.models).reverse(), selectedUser = this.view.context.get('selectedUser'), forecastType = app.utils.getForecastType(selectedUser.is_manager, selectedUser.showOpps); _.each(models, function(model) { this.commitLog.push(app.utils.createHistoryLog(loopPreviousModel, model, forecastType === 'Direct')); loopPreviousModel = model; }, this); //reset the order of the history log for display this.commitLog.reverse(); } }) }, "forecast-metric": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.forecastMetricField * @alias SUGAR.App.view.fields.BaseForecastsForecastMetricField * @extends View.Fields.Base.BaseField */ ({ // Forecast-metric FieldTemplate (base) /** * Attach toggle active function to clicking a metric */ events: { 'click .forecast-metric': 'toggleActive' }, /** * Holds if the users currency doesn't match the system default */ alternateCurrency: undefined, /** * Determines if field is active */ active: false, /** * Holds the count of records for this metric */ recordCount: 0, /** * Holds the currency converted value of the metric */ convertedValue: 0, /** * Holds the currency converted value of the metric */ convertedValueString: '0.00', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.alternateCurrency = (app.user.getPreference('currency_id') !== app.currency.getBaseCurrencyId()); }, /** * @inheritdoc */ _render: function() { this.formatAlternateCurrency(); this.recordCount = this.model.get(`${this.name}_count`); let activeMetric = this.model.get('active'); this.active = (_.isArray(activeMetric)) ? _.includes(activeMetric, this.name) : (activeMetric === this.name); this._super('_render'); }, /** * Formats alternateCurrency to the users perfered currecny string. */ formatAlternateCurrency: function() { if (this.alternateCurrency) { let userCurrencyID = app.user.getPreference('currency_id'); this.convertedValue = app.currency.convertAmount( this.model.get(this.name), app.currency.getBaseCurrencyId(), userCurrencyID ); this.convertedValueString = app.currency.formatAmountLocale( this.convertedValue, app.user.getPreference('currency_id') ); } }, /** * Switches the active metric when a metric is clicked. */ toggleActive: function() { if (!this.active) { this.model.set('lastActive', app.user.lastState.get(this.view.lastStateKey) || []); this.model.set('active', this.name); } } }) }, "quotapoint": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.QuotapointField * @alias SUGAR.App.view.fields.BaseForecastsQuotapointField * @extends View.Fields.Base.BaseField */ ({ // Quotapoint FieldTemplate (base) /** * The quota amount to display in the UI */ quotaAmount: undefined, /** * The current selected user object */ selectedUser: undefined, /** * The current selected timeperiod id */ selectedTimePeriod: undefined, /** * Hang on to the user-preferred currency id for formatting */ userCurrencyID: undefined, /** * Used by the resize function to wait a certain time before adjusting */ resizeDetectTimer: undefined, /** * @inheritdoc */ initialize: function(options) { app.view.Field.prototype.initialize.call(this, options); this.quotaAmount = 0.00; this.selectedUser = this.context.get('selectedUser'); this.selectedTimePeriod = this.context.get('selectedTimePeriod'); this.userCurrencyID = app.user.getPreference('currency_id'); //if user resizes browser, adjust datapoint layout accordingly $(window).on('resize.datapoints', _.bind(this.resize, this)); this.on('render', function() { this.resize(); return true; }, this); }, /** * @inheritdoc */ bindDataChange: function() { this.context.on('change:selectedUser', function(ctx, user) { this.selectedUser = user; // reload data when the selectedTimePeriod changes this.loadData({}); }, this); this.context.on('change:selectedTimePeriod', function(ctx, timePeriod) { this.selectedTimePeriod = timePeriod; // reload data when the selectedTimePeriod changes this.loadData({}); }, this); this.loadData(); }, /** * If this is a top-level manager, we need to add an event listener for * forecasts:worksheet:totals so the top-level manager's quota can update live * with changes done in the manager worksheet reflected here * * @param isTopLevelManager {Boolean} if the user is a top-level manager or not */ toggleTotalsListeners: function(isTopLevelManager) { if(isTopLevelManager) { this.hasListenerAdded = true; // Only for top-level manager whose quota can change on the fly this.context.on('forecasts:worksheet:totals', function(totals) { var quota = 0.00; if(_.has(totals, 'quota')) { quota = totals.quota; } else { quota = this.quotaAmount; } this.quotaAmount = quota; if (!this.disposed) { this.render(); } }, this); // if we're on the manager worksheet view, get the collection and calc quota if(!this.selectedUser.showOpps) { // in case this gets added after the totals event was dispatched var collection = app.utils.getSubpanelCollection(this.context, 'ForecastManagerWorksheets'), quota = 0.00; if (!_.isUndefined(collection) && !_.isUndefined(collection.models)) { _.each(collection.models, function(model) { quota = app.math.add(quota, model.get('quota')); }, this); } this.quotaAmount = quota; this.render(); } } else if(this.hasListenerAdded) { this.hasListenerAdded = false; this.context.off('forecasts:worksheet:totals', null, this); } }, /** * Builds widget url * * @return {*} url to call */ getQuotasURL: function() { var method = (this.selectedUser.is_manager && this.selectedUser.showOpps) ? 'direct' : 'rollup', url = 'Forecasts/' + this.selectedTimePeriod + '/quotas/' + method + '/' + this.selectedUser.id; return app.api.buildURL(url, 'read'); }, /** * Overrides loadData to load from a custom URL * * @inheritdoc */ loadData: function(options) { var url = this.getQuotasURL(), cb = { context: this, success: this.handleQuotaData, complete: options ? options.complete : null }; app.api.call('read', url, null, null, cb); }, /** * Success handler for the Quotas endpoint, sets quotaAmount to returned values and updates the UI * @param quotaData */ handleQuotaData: function(quotaData) { this.quotaAmount = quotaData.amount; // Check to see if we need to add an event listener to the context for the worksheet totals this.toggleTotalsListeners(quotaData.is_top_level_manager); // update the UI if (!this.disposed) { this.render(); } }, /** * Adjusts the layout */ adjustDatapointLayout: function(){ if(this.view.$el) { var thisView$El = this.view.$el, parentMarginLeft = thisView$El.find(".topline .datapoints").css("margin-left"), parentMarginRight = thisView$El.find(".topline .datapoints").css("margin-right"), timePeriodWidth = thisView$El.find(".topline .span4").outerWidth(true), toplineWidth = thisView$El.find(".topline ").width(), collection = thisView$El.find(".topline div.pull-right").children("span"), collectionWidth = parseInt(parentMarginLeft) + parseInt(parentMarginRight); collection.each(function(index){ collectionWidth += $(this).children("div.datapoint").outerWidth(true); }); //change width of datapoint div to span entire row to make room for more numbers if((collectionWidth+timePeriodWidth) > toplineWidth) { thisView$El.find(".topline div.hr").show(); thisView$El.find(".info .last-commit").find("div.hr").show(); thisView$El.find(".topline .datapoints").removeClass("span8").addClass("span12"); thisView$El.find(".info .last-commit .datapoints").removeClass("span8").addClass("span12"); thisView$El.find(".info .last-commit .commit-date").removeClass("span4").addClass("span12"); } else { thisView$El.find(".topline div.hr").hide(); thisView$El.find(".info .last-commit").find("div.hr").hide(); thisView$El.find(".topline .datapoints").removeClass("span12").addClass("span8"); thisView$El.find(".info .last-commit .datapoints").removeClass("span12").addClass("span8"); thisView$El.find(".info .last-commit .commit-date").removeClass("span12").addClass("span4"); var lastCommitHeight = thisView$El.find(".info .last-commit .commit-date").height(); thisView$El.find(".info .last-commit .datapoints div.datapoint").height(lastCommitHeight); } //adjust height of last commit datapoints var index = this.$el.index() + 1, width = this.$el.find("div.datapoint").outerWidth(), datapointLength = thisView$El.find(".info .last-commit .datapoints div.datapoint").length, sel = thisView$El.find('.last-commit .datapoints div.datapoint:nth-child('+index+')'); if (datapointLength > 2 && index <= 2 || datapointLength == 2 && index == 1) { $(sel).width(width-18); } else { $(sel).width(width); } } }, /** * Sets a timer to adjust the layout */ resize: function() { //The resize event is fired many times during the resize process. We want to be sure the user has finished //resizing the window that's why we set a timer so the code should be executed only once if (this.resizeDetectTimer) { clearTimeout(this.resizeDetectTimer); } this.resizeDetectTimer = setTimeout(_.bind(function() { this.adjustDatapointLayout(); }, this), 250); } }) }, "lastcommit": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.LastcommitField * @alias SUGAR.App.view.fields.BaseForecastsLastcommitField * @extends View.Fields.Base.BaseField */ ({ // Lastcommit FieldTemplate (base) commit_date: undefined, points: [], events: { 'click': 'triggerHistoryLog' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.points = []; // map what points we should display _.each(options.def.datapoints, function(point) { if (app.utils.getColumnVisFromKeyMap(point, 'forecastsWorksheet')) { this.points.push(point); } }, this); }, /** * Toggles the commit history log */ triggerHistoryLog: function() { this.$('#show_hide_history_log').toggleClass('sicon-chevron-down').toggleClass('sicon-chevron-up'); this.context.trigger('forecast:commit_log:trigger'); }, /** * @inheritdoc */ bindDataChange: function() { this.collection.on('reset', function() { // Get the latest commit model var model = _.first(this.collection.models); if (!_.isUndefined(model)) { this.commit_date = model.get('date_modified'); //FIXME: SS-2576 We should determine a better way to verify //server time and bowser having small differences in time let commitDate = app.date(this.commit_date); let currentTime = app.date(); if (currentTime.isBefore(commitDate)) { this.commit_date = currentTime.format(commitDate._f); } } else { this.commit_date = undefined; } if (!this.disposed) { this.render(); } //Toggles the arrow to keep arrow consistent when switching type of //worksheet if (this.context.get('forecastType') === 'Direct') { this.$('#show_hide_history_log').toggleClass('sicon-chevron-down').toggleClass('sicon-chevron-up'); } }, this); }, /** * Processes a Forecast collection's models into datapoints * @param {Bean} model * @returns {Array} * @deprecated since 12.1.0, this is no longer used */ processDataPoints: function(model) { var points = [], noAccessTemplate = app.template.getField('base', 'noaccess')(this); _.each(this.points, function(point) { // make sure we can view data for this point var point_data = {}; if (app.acl.hasAccess('read', 'ForecastWorksheets', app.user.get('id'), point)) { point_data.value = model.get(point) } else { point_data.error = noAccessTemplate; } points.push(point_data); }, this); return points; } }) }, "assignquota": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.AssignQuotaField * @alias SUGAR.App.view.fields.BaseForecastsAssignQuotaField * @extends View.Fields.Base.RowactionField */ ({ // Assignquota FieldTemplate (base) extendsFrom: 'RowactionField', /** * Should be this disabled if it's not rendered? */ disableButton: true, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'rowaction'; }, /** * @inheritdoc */ bindDataChange: function() { this.context.on('forecasts:worksheet:quota_changed', function() { this.disableButton = false; if (!this.disposed) { this.render(); } }, this); this.context.on('forecasts:worksheet:committed', function() { this.disableButton = true; if (!this.disposed) { this.render(); } }, this); this.context.on('forecasts:assign_quota', this.assignQuota, this); }, /** * We override this so we can always disable the field * * @inheritdoc */ _render: function() { this._super('_render'); // only set field as disabled if it's actually rendered into the dom // otherwise it will cause problems and not show correctly when disabled if (this.getFieldElement().length > 0) { this.setDisabled(this.disableButton); } }, /** * Only show this if the current user is a manager and we are on their manager view * * @inheritdoc */ hasAccess: function() { var su = (this.context.get('selectedUser')) || app.user.toJSON(), isManager = su.is_manager || false, showOpps = su.showOpps || false; return (su.id === app.user.get('id') && isManager && showOpps === false); }, /** * Run the XHR Request to Assign the Quotas * * @param {string} worksheetType What worksheet are we on * @param {object} selectedUser What user is calling the assign quota * @param {string} selectedTimeperiod Which timeperiod are we assigning quotas for */ assignQuota: function(worksheetType, selectedUser, selectedTimeperiod) { app.api.call('create', app.api.buildURL('ForecastManagerWorksheets/assignQuota'), { 'user_id': selectedUser.id, 'timeperiod_id': selectedTimeperiod }, { success: _.bind(function(o) { app.alert.dismiss('saving_quota'); app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get("LBL_FORECASTS_WIZARD_SUCCESS_TITLE", "Forecasts") + ":", messages: [app.lang.get('LBL_QUOTA_ASSIGNED', 'Forecasts')] }); this.disableButton = true; this.context.trigger('forecasts:quota_assigned'); if (!this.disposed) { this.render(); } }, this) }); } }) }, "datapoint": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Datapoints in the info pane for Forecasts * * @class View.Fields.Base.Forecasts.DatapointField * @alias SUGAR.App.view.fields.BaseForecastsDatapointField * @extends View.Fields.Base.BaseField */ ({ // Datapoint FieldTemplate (base) /** * Boolean track if current user is manager */ isManager: '', /** * Arrow Colors */ arrow: '', /** * The total we want to display */ total: 0, /** * The total for RLIs to display */ rliTotal: 0, /** * The last committed value for this datapoint */ lastCommit: 0, /** * Can we actually display this field and have the data binding on it */ hasAccess: true, /** * Do we have access from the ForecastWorksheet Level to show data here? */ hasDataAccess: true, /** * What to show when we don't have access to the data */ noDataAccessTemplate: undefined, /** * Holds the totals field name */ total_field: '', cteTag: '.forecast-value-input', /** * When true, the field will not be editable */ disableCTE: false, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['ClickToEdit']); this._super('initialize', [options]); this.total_field = this.total_field || this.name; this.hasAccess = app.utils.getColumnVisFromKeyMap(this.name, 'forecastsWorksheet'); this.hasDataAccess = app.acl.hasAccess('read', 'ForecastWorksheets', app.user.get('id'), this.name); if (this.hasDataAccess === false) { this.noDataAccessTemplate = app.template.getField('base', 'noaccess')(this); } // before we try and render, lets see if we can actually render this field this.before('render', function() { return this.hasAccess; }, this); //if user resizes browser, adjust datapoint layout accordingly $(window).on('resize.datapoints', _.bind(this.resize, this)); this.listenTo(this, 'render', function() { if (!this.hasAccess) { return false; } this.resize(); return true; }); }, /** * @inheritdoc * * Formats the value as a number string */ format: function(value) { if (this.tplName === 'edit') { return app.utils.formatNumberLocale(value); } return this._super('format', [value]); }, /** * @inheritdoc * * Unformats the value from a number string */ unformat: function(value) { let unformattedValue; if (this.tplName === 'edit') { unformattedValue = app.utils.unformatNumberStringLocale(value); } else { unformattedValue = app.currency.unformatAmountLocale(value); } if (_.isFinite(unformattedValue)) { var precision = this.def && this.def.precision || 6; return app.math.round(unformattedValue, precision, true); } return value; }, /** * @inheritdoc */ _render: function() { this.isManager = ('manager' === this.getUserCurrentRole()); // Set the correct arrow style depending on the current Forecast state this.arrow = this._getArrowIconColorClass(this.model.get(this.name), this.model.getSynced(this.name)); this.isRLIMode = (app.metadata.getModule('Opportunities', 'config').opps_view_by === 'RevenueLineItems'); this.checkEditAccess(); this._super('_render'); }, /** * If a user is viewing someone else's forecast page, it will disable the user's * ability to edit the commitment value. */ checkEditAccess: function() { if (this.context && app.user && this.context.get('selectedUser') && this.context.get('selectedUser').id === app.user.get('id')) { this.disableCTE = false; } else { this.disableCTE = true; } }, /** * Check to see if the worksheet needs commit * * @deprecated since 12.0, this is no longer used */ checkIfNeedsCommit: function() { // if the initial_total is an empty string (default value) don't run this if (!_.isEqual(this.initial_total, '') && app.math.isDifferentWithPrecision(this.total, this.initial_total)) { this.context.trigger('forecasts:worksheet:needs_commit', null); } }, /** * Overwrite this to only place the placeholder if we actually have access to view it * * @return {*} */ getPlaceholder: function() { if (this.hasAccess) { return this._super('getPlaceholder'); } return ''; }, /** * Adjusts the CSS for the datapoint */ adjustDatapointLayout: function() { if (this.hasAccess) { var parentMarginLeft = this.view.$('.topline .datapoints').css('margin-left'), parentMarginRight = this.view.$('.topline .datapoints').css('margin-right'), timePeriodWidth = this.view.$('.topline .span4').outerWidth(true), toplineWidth = this.view.$('.topline ').width(), collection = this.view.$('.topline div.pull-right').children('span'), collectionWidth = parseInt(parentMarginLeft) + parseInt(parentMarginRight); collection.each(function(index) { collectionWidth += $(this).children('div.datapoint').outerWidth(true); }); //change width of datapoint div to span entire row to make room for more numbers if ((collectionWidth + timePeriodWidth) > toplineWidth) { this.view.$('.topline div.hr').show(); this.view.$('.info .last-commit').find('div.hr').show(); this.view.$('.topline .datapoints').removeClass('span8').addClass('span12'); this.view.$('.info .last-commit .datapoints').removeClass('span8').addClass('span12'); this.view.$('.info .last-commit .commit-date').removeClass('span4').addClass('span12'); } else { this.view.$('.topline div.hr').hide(); this.view.$('.info .last-commit').find('div.hr').hide(); this.view.$('.topline .datapoints').removeClass('span12').addClass('span8'); this.view.$('.info .last-commit .datapoints').removeClass('span12').addClass('span8'); this.view.$('.info .last-commit .commit-date').removeClass('span12').addClass('span4'); var lastCommitHeight = this.view.$('.info .last-commit .commit-date').height(); this.view.$('.info .last-commit .datapoints div.datapoint').height(lastCommitHeight); } //adjust height of last commit datapoints let index = this.$el.index(); let width = this.$('div.datapoint').innerWidth(); // USE innerWidth? let datapointLength = this.view.$('.info .last-commit .datapoints div.datapoint').length; let sel = this.view.$('.last-commit .datapoints div.datapoint:nth-child(' + index + ')'); if (datapointLength > 2 && index <= 2 || datapointLength == 2 && index == 1) { // RTL was off 1px var widthMod = (app.lang.direction === 'rtl') ? 7 : 16; $(sel).width(width - widthMod); } else { // Minus 16 for padding-x 0.5rem (8px) $(sel).width(width - 16); } } }, /** * Resizes the datapoint on window resize */ resize: function() { //The resize event is fired many times during the resize process. We want to be sure the user has finished //resizing the window that's why we set a timer so the code should be executed only once if (this.resizeDetectTimer) { clearTimeout(this.resizeDetectTimer); } this.resizeDetectTimer = setTimeout(_.bind(function() { this.adjustDatapointLayout(); }, this), 250); }, /** * @inheritdoc */ bindDataChange: function() { if (!this.hasAccess) { return; } this.listenTo(this.context, 'change:selectedUser change:selectedTimePeriod', function() { this.lastCommit = 0; this.total = 0; this.rliTotal = 0; this.arrow = ''; }); this.listenTo(this.context, 'forecasts:commit-models:loaded', this._handleCommitModelsLoaded); this.listenTo(this.context, 'forecasts:worksheet:totals', this._onWorksheetTotals); this.listenTo(this.context, 'forecasts:worksheet:committed', this._onWorksheetCommit); this.listenTo(this.model, `change:${this.name}`, this._handleValueChanged); }, /** * Updates the last commited value for this datapoint when the last * commit model is loaded * * @private */ _handleCommitModelsLoaded: function() { let lastCommitModel = this.context.get('lastCommitModel'); if (lastCommitModel instanceof Backbone.Model) { this.lastCommit = lastCommitModel.get(this.name) || 0; } this.render(); }, /** * When the value is changed from its synced/initial value, signal the * context so it can enable the cancel/commit buttons properly * * @private */ _handleValueChanged: function() { let syncedValue = this.model.getSynced(this.name); let change = this.model.changedAttributes()[this.name]; if (!_.isEqual(syncedValue, change)) { this.context.trigger('forecasts:datapoint:changed'); } // Render is normally bound to data change on the model from // bindDataChange in field.js, but since the model can change // throughout the life of this field we need to render here this.render(); }, /** * Collection Reset Handler * @param {Backbone.Collection} collection * @private * * @deprecated since 12.0 this is no longer used */ _onCommitCollectionReset: function(collection) { // get the first line var model = _.first(collection.models); if (!_.isUndefined(model)) { this.initial_total = model.get(this.total_field); if (!this.disposed) { this.render(); } } }, /** * Worksheet Totals Handler * @param {Object} totals The totals from the worksheet * @param {String} type Which worksheet are we dealing with it * @private */ _onWorksheetTotals: function(totals, type) { if (this.disposed) { return; } var field = this.total_field; if (type == 'manager') { // split off '_case' field = field.split('_')[0] + '_adjusted'; } this.total = totals[field]; this.rliTotal = totals['rli_' + field]; this.render(); }, /** * What to do when the worksheet is committed * * @param {String} type What type of worksheet was committed * @param {Object} forecast What was committed for the timeperiod * @private */ _onWorksheetCommit: function(type, forecast) { if (this.disposed) { return; } this.arrow = ''; this.render(); }, /** * Returns the CSS classes for an up or down arrow icon * * @param {String|Number} newValue the new value * @param {String|Number} oldValue the previous value * @return {String} css classes for up or down arrow icons, if the values didn't change, returns '' * @private */ _getArrowIconColorClass: function(newValue, oldValue) { // Make sure newValue and oldValue are numbers. If not, default them // to 0 let newValueParsed = parseFloat(newValue); let newValueFinal = !_.isNaN(newValueParsed) ? newValueParsed : 0; let oldValueParsed = parseFloat(oldValue); let oldValueFinal = !_.isNaN(oldValueParsed) ? oldValueParsed : 0; // Convert the values to Bigs first before any comparisons are done let newValueBig = Big(newValueFinal); let oldValueBig = Big(oldValueFinal); // Determine which class the arrow icon should use let arrowIconClass = ''; if (app.math.isDifferentWithPrecision(newValueBig, oldValueBig)) { arrowIconClass = newValueBig.gt(oldValueBig) ? ' sicon-arrow-up font-green' : ' sicon-arrow-down font-red'; } return arrowIconClass; }, /** * Gets user current role (the role under which the user is viewing the worksheet at the moment) * @return {string} "manager" or "seller" * */ getUserCurrentRole: function() { let user = this.context.get('selectedUser') || app.user.toJSON(); let forecastType = app.utils.getForecastType(user.is_manager, user.showOpps); return (forecastType === 'Direct') ? 'seller' : 'manager'; }, /** * @inheritdoc */ _dispose: function() { // Clear any listeners $(window).off('resize.datapoints'); this.stopListening(); // make sure we've cleared the resize timer before navigating away clearInterval(this.resizeDetectTimer); this._super('_dispose'); } }) }, "reportingUsers": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.ReportingUsersField * @alias SUGAR.App.view.fields.BaseForecastsReportingUsersField * @extends View.Fields.Base.BaseField */ ({ // ReportingUsers FieldTemplate (base) /** * The JS Tree Object */ jsTree: {}, /** * The end point we need to hit */ reporteesEndpoint: '', /** * Current end point hit */ currentTreeUrl: '', /** * Current root It */ currentRootId: '', /** * Selected User Storage */ selectedUser: {}, /** * Has the base init selected the proper user? This is needed to prevent a double selectedUser change from fireing */ initHasSelected: false, /** * Previous user */ previousUserName: undefined, /** * Initialize the View * * @constructor * @param {Object} options */ initialize: function(options) { app.view.Field.prototype.initialize.call(this, options); this.reporteesEndpoint = app.api.buildURL("Forecasts/reportees") + '/'; this.selectedUser = this.context.get('selectedUser') || app.user.toJSON(); this.currentTreeUrl = this.reporteesEndpoint + this.selectedUser.id; this.currentRootId = this.selectedUser.id; }, /** * overriding _dispose to make sure custom added jsTree listener is removed * @private */ _dispose: function() { if (app.user.get('is_manager') && !_.isEmpty(this.jsTree)) { this.jsTree.off(); } app.view.Field.prototype._dispose.call(this); }, /** * Only run the render if the user is a manager as that is the only time we want the tree to display. */ render: function() { if (app.user.get('is_manager')) { app.view.Field.prototype.render.call(this); } }, /** * Clean up any left over bound data to our context */ unbindData: function() { app.view.Field.prototype.unbindData.call(this); }, /** * set up event listeners */ bindDataChange: function(){ this.context.on("forecasts:user:canceled", function(){ this.initHasSelected = false; this.selectJSTreeNode(this.previousUserName); this.initHasSelected = true; }, this); }, /** * Function to give a final check before rendering to see if we really need to render * Any time the selectedUser changes on context we run through this function to * see if we should render the tree again * * @param context * @param selectedUser {Object} the current selectedUser on the context */ checkRender: function(context, selectedUser) { // handle the case for user clicking MyOpportunities first this.selectedUser = selectedUser; if (selectedUser.showOpps) { var nodeId = (selectedUser.is_manager ? 'jstree_node_myopps_' : 'jstree_node_') + selectedUser.id; this.selectJSTreeNode(nodeId) // check before render if we're trying to re-render tree with a fresh root user // otherwise do not re-render tree // also make sure we're not re-rendering tree for a rep } else if (this.currentRootId != selectedUser.id) { if (selectedUser.is_manager) { // if user is a manager we'll be re-rendering the tree // no need to re-render the tree if not a manager because the dataset // stays the same this.currentRootId = selectedUser.id; this.currentTreeUrl = this.reporteesEndpoint + selectedUser.id; this.rendered = false; if (!this.disposed) { this.render(); } } else { // user is not a manager but if this event is coming from the worksheets // we need to "select" the user on the tree to show they're selected // create node ID var nodeId = 'jstree_node_' + selectedUser.id; // select node only if it is not the already selected node if (this.jsTree.jstree('get_selected').attr('id') != nodeId) { this.selectJSTreeNode(nodeId) } } } }, /** * Function that handles deselecting any selected nodes then selects the nodeId * * @param nodeId {String} the node id starting with "jstree_node_" */ selectJSTreeNode: function(nodeId) { // jstree kept trying to hold on to the root node css staying selected when // user clicked a user's name from the worksheet, so explicitly causing a deselection this.jsTree.jstree('deselect_all'); this.jsTree.jstree('select_node', '#' + nodeId); }, /** * Recursively step through the tree and for each node representing a tree node, run the data attribute through * the replaceHTMLChars function. This function supports n-levels of the tree hierarchy. * * @param data The data structure returned from the REST API Forecasts/reportees endpoint * @param ctx A reference to the view's context so that we may recursively call _recursiveReplaceHTMLChars * @return object The modified data structure after all the parent and children nodes have been stepped through * @private */ _recursiveReplaceHTMLChars: function(data, ctx) { _.each(data, function(entry, index) { //Scan for the nodes with the data attribute. These are the nodes we are interested in if (entry.data) { data[index].data = (function(value) { return value.replace(/&/gi, '&').replace(/</gi, '<').replace(/>/gi, '>').replace(/'/gi, '\'').replace(/"/gi, '"'); })(entry.data); if (entry.children) { //For each children found (if any) then call _recursiveReplaceHTMLChars again. Notice setting //childEntry to an Array. This is crucial so that the beginning _.each loop runs correctly. _.each(entry.children, function(childEntry, index2) { entry.children[index2] = ctx._recursiveReplaceHTMLChars([childEntry]); if (childEntry.attr.rel == 'my_opportunities' && childEntry.metadata.id == app.user.get('id')) { childEntry.data = app.utils.formatString(app.lang.get('LBL_MY_MANAGER_LINE', 'Forecasts'), [childEntry.data]); } }, this); } } }, this); return data; }, /** * Renders JSTree * @param ctx * @param options * @protected */ _render: function(ctx, options) { app.view.Field.prototype._render.call(this, ctx, options); var options = {}; // breaking out options as a proper object to allow for bind options.success = _.bind(function(data) { this.createTree(data); }, this); app.api.call('read', this.currentTreeUrl, null, options); }, createTree: function(data) { // make sure we're using an array // if the data coming from the endpoint is an array with one element // it gets converted to a JS object in the process of getting here if (!_.isArray(data)) { data = [ data ]; } let treeData = this._recursiveReplaceHTMLChars(data, this); let selectedUser = this.context.get('selectedUser'); let nodeId = (selectedUser.is_manager && selectedUser.showOpps ? 'jstree_node_myopps_' : 'jstree_node_') + selectedUser.id; treeData.ctx = this.context; this.jsTree = $(".jstree-sugar").jstree({ "plugins": ["json_data", "ui", "crrm", "types", "themes"], "json_data": { "data": treeData }, "ui": { // when the tree re-renders, initially select the root node initially_select: [nodeId] }, "types": { "types": { "types": { "parent_link": {}, "manager": {}, "my_opportunities": {}, "rep": {}, "root": {} } } } }).on("reselect.jstree", _.bind(function() { // this is needed to stop the double select when the tree is rendered this.initHasSelected = true; }, this)) .on("select_node.jstree", _.bind(function(event, data) { if (this.initHasSelected) { this.previousUserName = (this.selectedUser.is_manager && this.selectedUser.showOpps ? 'jstree_node_myopps_' : 'jstree_node_') + this.selectedUser.id; var jsData = data.inst.get_json(), nodeType = jsData[0].attr.rel, userData = jsData[0].metadata, showOpps = false; // if user clicked on a "My Opportunities" node // set this flag true if (nodeType == "my_opportunities" || nodeType == "rep") { showOpps = true } var selectedUser = { 'id': userData.id, 'user_name': userData.user_name, 'full_name': userData.full_name, 'first_name': userData.first_name, 'last_name': userData.last_name, 'reports_to_id': userData.reports_to_id, 'reports_to_name': userData.reports_to_name, 'is_manager': (nodeType != 'rep'), 'is_top_level_manager': (nodeType != 'rep' && _.isEmpty(userData.reports_to_id)), 'showOpps': showOpps, 'reportees': [] }; this.context.trigger('forecasts:user:changed', selectedUser, this.context); } }, this)); if (treeData) { var rootId = -1; if (treeData.length == 1) { // this case appears when "Parent" is not present rootId = treeData[0].metadata.id; } else if (treeData.length == 2) { // this case appears with a "Parent" link label in the return set // treeData[0] is the Parent link, treeData[1] is our root user node rootId = treeData[1].metadata.id; } this.currentRootId = rootId; } // add proper class onto the tree this.$el.find('#people').addClass("jstree-sugar"); } }) }, "rowaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Rowaction FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc * @return {boolean} */ hasAccess: function() { if (this.def.acl_action !== 'manager') { return true; } return 'manager' === this.getUserCurrentRole(); }, /** * Gets user current role (the role under which the user is viewing the worksheet at the moment) * @return {string} "manager" or "seller" * */ getUserCurrentRole: function() { let user = this.context.get('selectedUser') || app.user.toJSON(); let forecastType = app.utils.getForecastType(user.is_manager, user.showOpps); return (forecastType === 'Direct') ? 'seller' : 'manager'; }, }) }, "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.DateField * @alias SUGAR.App.view.fields.BaseForecastsDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * @inheritdoc * * flag to prevent datepicker entering into detail mode */ hidePickerAfterLoad: false, /** * @inheritdoc * * Add `ClickToEdit` plugin to the list of required plugins. */ _initPlugins: function() { this._super('_initPlugins'); if (this.options && this.options.def && this.options.def.click_to_edit) { this.plugins = _.union(this.plugins, [ 'ClickToEdit' ]); } return this; } }) }, "header-quotapoint": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Datapoints in the info pane for Forecasts * * @class View.Fields.Base.Forecasts.QuotaHeaderQuotapointField * @alias SUGAR.App.view.fields.BaseForecastsHeaderQuotapointField * Field * @extends View.Fields.Base.BaseForecastsQuotapointField */ ({ // Header-quotapoint FieldTemplate (base) extendsFrom: 'ForecastsQuotapointField', /** * @inheritdoc */ bindDataChange: function() { this.context.on('change:selectedUser', function(ctx, user) { //The context sometimes gets cleaned up from an asyncronous call. if (this.context) { this.selectedUser = user; // reload data when the selectedTimePeriod changes this.loadData({}); } }, this); this.context.on('change:selectedTimePeriod', function(ctx, timePeriod) { //The context sometimes gets cleaned up from an asyncronous call. if (this.context) { this.selectedTimePeriod = timePeriod; // reload data when the selectedTimePeriod changes this.loadData({}); } }, this); this.listenTo(this.context, 'forecasts:refreshList', this.loadData); this.loadData(); }, resize: function() { //We don't want to fire the parents resize function so we override it //with this empty function. } }) }, "button": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Forecasts.ButtonField * @alias SUGAR.App.view.fields.BaseForecastsButtonField * @extends View.Fields.Base.ButtonField */ ({ // Button FieldTemplate (base) extendsFrom: 'ButtonField', /** * Override so we can have a custom hasAccess for forecast to check on the header-pane buttons * * @inheritdoc */ hasAccess: function() { // this is a special use case for forecasts // currently the only buttons that set acl_action == 'current_user' are the save_draft and commit buttons // if it's not equal to 'current_user' then go up the prototype chain. if(this.def.acl_action == 'current_user') { var su = (this.context && this.context.get('selectedUser')) || app.user.toJSON(); return su.id === app.user.get('id'); } else { return this._super('hasAccess'); } } }) }, "fiscal-year": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Fiscal-year FieldTemplate (base) extendsFrom: 'EnumField', loadEnumOptions: function(fetch, callback) { this._super('loadEnumOptions', [fetch, callback]); var startYear = this.options.def.startYear; _.each(this.items, function(value, key, list) { list[key] = list[key].replace("{{year}}", startYear++); }, this); } }) }, "header-datapoint": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Datapoints in the info pane for Forecasts * * @class View.Fields.Base.Forecasts.HeaderDatapointField * @alias SUGAR.App.view.fields.BaseForecastsHeaderDatapointField * @extends View.Fields.Base.BaseField */ ({ // Header-datapoint FieldTemplate (base) /** * Can we actually display this field and have the data binding on it */ hasAccess: true, /** * Do we have access from the ForecastWorksheet Level to show data here? */ hasDataAccess: true, /** * What to show when we don't have access to the data */ noDataAccessTemplate: undefined, /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['ClickToEdit']); this._super('initialize', [options]); this.total_field = this.total_field || this.name; this.model = this.context.get('nextCommitModel'); this.hasAccess = app.utils.getColumnVisFromKeyMap(this.name, 'forecastsWorksheet'); this.hasDataAccess = app.acl.hasAccess('read', 'ForecastWorksheets', app.user.get('id'), this.name); if (this.hasDataAccess === false) { this.noDataAccessTemplate = app.template.getField('base', 'noaccess')(this); } }, /** * Overwrite this to only place the placeholder if we actually have access to view it * * @return {*} */ getPlaceholder: function() { if (this.hasAccess) { return this._super('getPlaceholder'); } return ''; }, }) } }} , "views": { "base": { "config-forecast-by": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigForecastByView * @alias SUGAR.App.view.layouts.BaseForecastsConfigForecastByView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-forecast-by View (base) extendsFrom: 'ConfigPanelView', /** * @inheritdoc */ _updateTitleValues: function() { this.titleSelectedValues = this.model.get('forecast_by'); } }) }, "config-scenarios": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigScenariosView * @alias SUGAR.App.view.layouts.BaseForecastsConfigScenariosView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-scenarios View (base) extendsFrom: 'ConfigPanelView', /** * Holds ALL possible different scenarios */ scenarioOptions: [], /** * Holds the scenario objects that should start selected by default */ selectedOptions: [], /** * Holds the select2 instance of the default scenario that users cannot change */ defaultSelect2: undefined, /** * Holds the select2 instance of the options that users can add/remove */ optionsSelect2: undefined, /** * The default key used for the "Amount" value in forecasts, right now it is "likely" but users will be able to * change that in admin to be best or worst * * todo: eventually this will be moved to config settings where users can select their default forecasted value likely/best/worst */ defaultForecastedAmountKey: 'show_worksheet_likely', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.selectedOptions = []; this.scenarioOptions = []; // set up scenarioOptions _.each(this.meta.panels[0].fields, function(field) { var obj = { id: field.name, text: app.lang.get(field.label, 'Forecasts') } // Check if this field is the one we don't want users to delete if(field.name == this.defaultForecastedAmountKey) { obj['locked'] = true; } this.scenarioOptions.push(obj); // if this should be selected by default and it is not the undeletable scenario, push it to selectedOptions if(this.model.get(field.name) == 1) { // push fields that should be selected to selectedOptions this.selectedOptions.push(obj); } }, this); }, /** * Empty function as the title values have already been set properly * with the change:scenarios event handler * * @inheritdoc */ _updateTitleValues: function() { }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:scenarios', function(model) { var arr = []; if(model.get('show_worksheet_likely')) { arr.push(app.lang.get('LBL_FORECASTS_CONFIG_WORKSHEET_SCENARIOS_LIKELY', 'Forecasts')); } if(model.get('show_worksheet_best')) { arr.push(app.lang.get('LBL_FORECASTS_CONFIG_WORKSHEET_SCENARIOS_BEST', 'Forecasts')); } if(model.get('show_worksheet_worst')) { arr.push(app.lang.get('LBL_FORECASTS_CONFIG_WORKSHEET_SCENARIOS_WORST', 'Forecasts')); } this.titleSelectedValues = arr.join(', '); this.updateTitle(); }, this); // trigger the change event to set the title when this gets added this.model.trigger('change:scenarios', this.model); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('.select2-container-disabled').width('auto'); this.$('.select2-search-field').css('display','none'); // handle setting up select2 options var isRTL = app.lang.direction === 'rtl'; this.optionsSelect2 = this.$('#scenariosSelect').select2({ data: this.scenarioOptions, multiple: true, width: "100%", containerCssClass: "select2-choices-pills-close", escapeMarkup: function(m) { return m; }, initSelection : _.bind(function (element, callback) { callback(this.selectedOptions); }, this) }); this.optionsSelect2.select2('val', this.selectedOptions); this.optionsSelect2.on('change', _.bind(this.handleScenarioModelChange, this)); }, /** * Event handler for the select2 dropdown changing selected items * * @param {jQuery.Event} evt select2 change event */ handleScenarioModelChange: function(evt) { var changedEnabled = [], changedDisabled = [], allOptions = []; // Get the options that changed and set the model _.each($(evt.target).val().split(','), function(option) { changedEnabled.push(option); this.model.set(option, true, {silent: true}); }, this); // Convert all scenario options into a flat array of ids _.each(this.scenarioOptions, function(option) { allOptions.push(option.id); }, this); // Take all options and return an array without the ones that changed to true changedDisabled = _.difference(allOptions, changedEnabled); // Set any options that weren't changed to true to false _.each(changedDisabled, function(option) { this.model.set(option, false, {silent: true}); }, this); this.model.trigger('change:scenarios', this.model); }, /** * Formats pill selections * * @param {Object} item selected item */ formatCustomSelection: function(item) { return '<a class="select2-choice-filter" rel="'+ item.id + '" href="javascript:void(0)">'+ item.text +'</a>'; }, /** * @inheritdoc * * Remove custom listeners off select2 instances */ _dispose: function() { // remove event listener from select2 if (this.defaultSelect2) { this.defaultSelect2.off(); this.defaultSelect2.select2('destroy'); this.defaultSelect2 = null; } if (this.optionsSelect2) { this.optionsSelect2.off(); this.optionsSelect2.select2('destroy'); this.optionsSelect2 = null; } this._super('_dispose'); } }) }, "forecast-metrics": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsForecastMetricsView * @alias SUGAR.App.view.layouts.BaseForecastsForecastMetricsView * @extends View.View */ ({ // Forecast-metrics View (base) className: 'inline-block forecast-metrics-container', metrics: [], /** * Key name for the last state */ lastStateKey: 'Forecasts:last-metric', /** * @inheritdoc */ initialize: function(options) { if (_.isUndefined(options.meta[options.name])) { options.meta = app.metadata.getView(options.module, options.name); } if (app.lang.direction === 'rtl') { // reverse the forecast-metrics options.meta['forecast-metrics'].reverse(); } this._super('initialize', [options]); this.metrics = this.buildMetrics(); //Create a new model to store the metric values. this.model = new Backbone.Model(); const lastMetric = app.user.lastState.get(this.lastStateKey) || []; this.listenTo(this.model, 'change:active', this._handleActiveMetricsChange); this.setActiveMetrics(lastMetric); }, /** * @inheritdoc */ bindDataChange: function() { this.listenTo(this.context, 'change:selectedUser', this.loadData); this.listenTo(this.context, 'change:selectedTimePeriod', this.loadData); this.listenTo(this.context, 'change:forecastType', this.loadData); this.listenTo(this.context, 'opportunities:record:saved', this.loadData); this.listenTo(this.layout.layout, 'filter:apply', this.loadData); let filterComp = this.layout.layout.getComponent('filter') || {}; if (!_.isUndefined(filterComp)) { this.listenTo(filterComp, 'filter:apply', this.loadData); this.listenTo(filterComp, 'filter:change:filter', this.loadData); } }, /** * Sets the active metric on the model */ setActiveMetrics: function(activeMetrics) { if (_.isEmpty(activeMetrics)) { activeMetrics = []; let defaultMetrics = _.filter(this.meta['forecast-metrics'], function(metric) { return metric.isDefaultFilter || false; }, this); _.each(defaultMetrics, function(defaultMetric) { activeMetrics.push(defaultMetric.name); }, this); } this.model.set('lastActive', app.user.lastState.get(this.lastStateKey) || []); this.model.set('active', activeMetrics); }, /** * Tells the parent layout that the selected active metric has changed * * @private */ _handleActiveMetricsChange: function() { // For now, we allow one metric to be selected. In the future, this may // change to allow multiple let activeMetrics = []; let active = this.model.get('active'); if (_.isArray(active)) { _.each(active, function(a) { if (_.has(this.metrics, a)) { activeMetrics.push(this.metrics[a]); } }, this); } else { activeMetrics.push(this.metrics[active]); } this.layout.layout.trigger('forecast:metric:active', activeMetrics); this.render(); if (this.lastStateKey) { app.user.lastState.set(this.lastStateKey, active); } }, /** * Loads data from forecasts metrics api. */ loadData: function() { this.toggleLoader(true); let url = app.api.buildURL('Forecasts/metrics'); let selectedUser = this.context.get('selectedUser'); let data = { 'filter': this.getListFilter(), 'module': this.layout.layout.meta.context.listViewModule || 'Opportunities', 'user_id': selectedUser ? selectedUser.id : '', 'time_period': this.context.get('selectedTimePeriod'), 'type': this.context.get('forecastType'), 'metrics': this.metrics }; let callbacks = { success: _.bind(function(data) { let activeMetricNames = _.isArray(this.model.get('active')) ? this.model.get('active') : [this.model.get('active')]; let activeMetricsCount = 0; _.each(data.metrics, function(metric) { if (_.includes(activeMetricNames, metric.name)) { activeMetricsCount += metric.values.count; } this.model.set(`${metric.name}_count`, metric.values.count); this.model.set(metric.name, metric.values.sum); },this); this.layout.layout.trigger('metric:count:fetched', activeMetricsCount); app.events.trigger('metric:data:ready'); },this), complete: _.bind(function() { this.toggleLoader(false); },this) }; app.api.call('create', url, data, callbacks); }, /** * Builds metric defintions out of forecast-metrics metadata * * @return array */ buildMetrics: function() { let metrics = {}; _.each(this.meta['forecast-metrics'], function(metric) { metrics[metric.name] = this.buildMetric(metric); },this); return metrics; }, /** * Returns a single metric from metric meta data. * @param metric * * @return object */ buildMetric: function(metric) { return { 'name': metric.name, 'filter': metric.filter, 'sum_fields': metric.sumFields }; }, /** * This method will return an array of filters from the list view. * * @return array */ getListFilter: function() { let filterComp = this.layout.layout.getComponent('filter') || {}; if (!_.isEmpty(filterComp)) { return filterComp.collection.filterDef || []; } return []; }, /** * Show/Hide metric item SVG-loader */ toggleLoader: function(show) { this.$el.find('.forecast-metric').toggleClass('metric-skeleton-loader', show); }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); this._super('_dispose'); } }) }, "forecast-metrics-help": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsForecastMetricsHelpView * @alias SUGAR.App.view.layouts.BaseForecastsForecastMetricsHelpView * @extends View.View */ ({ // Forecast-metrics-help View (base) className: 'metrics-help-button inline-block absolute', events: { 'click .metrics-help': 'showHelpModal', }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.helpButton = this.$('.metrics-help'); }, /** * Info/Guide/Help button click event listener. */ showHelpModal: function() { if (!app.isSynced) { return; } if (this.helpButton.hasClass('disabled')) { return; } // For bwc modules and the About page, handle the help click differently. if (this.layoutName === 'bwc' || this.layoutName === 'about') { this.bwcHelpClicked(); return; } if (!this._helpLayout || this._helpLayout.disposed) { this._createHelpLayout(); } this._helpLayout.toggle(); }, /** * Creates the help layout. * * @param {jQuery} button The Help button. * @private */ _createHelpLayout: function() { this._helpLayout = app.view.createLayout({ module: app.controller.context.get('module'), type: 'metrics-help', button: this.helpButton, }); this._helpLayout.initComponents(); this.listenTo(this._helpLayout, 'show hide', function(view, active) { this.helpButton.toggleClass('active', active); }); }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); $(window).off('resize'); app.events.off('metric:data:ready'); this._super('_dispose'); } }) }, "list-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsListHeaderpaneView * @alias SUGAR.App.view.layouts.BaseForecastsListHeaderpaneView * @extends View.Views.Base.ListHeaderpaneView */ ({ // List-headerpane View (base) extendsFrom: 'HeaderpaneView', plugins: ['FieldErrorCollection'], /** * If Forecasts' data sync is complete and we can render buttons * @type Boolean */ forecastSyncComplete: false, /** * Holds the prefix string that is rendered before the same of the user * @type String */ forecastWorksheetLabel: '', /** * Timeperiod model */ tpModel: undefined, /** * Current quarter label id * * @type String */ currentTimePeriodId: undefined, /** * Current default quarter * */ lastQuarter: 4, /** * Current default timezone (always GMT) * */ defaultTimeZone: 'Etc/GMT', /** * @inheritdoc */ initialize: function(options) { this.tpModel = new Backbone.Model(); this._super('initialize', [options]); this.currentTimePeriodId = this.context.get('selectedTimePeriod'); this.resetSelection(this.currentTimePeriodId); // Update label for worksheet let selectedUser = this.context.get('selectedUser'); if (selectedUser) { this._title = this._getForecastWorksheetLabel(selectedUser); } }, /** * @inheritdoc */ bindDataChange: function() { this.tpModel.on('change', function(model) { let selectedTimePeriodId = model.get('selectedTimePeriod'); this.context.trigger( 'forecasts:timeperiod:changed', model, this.getField('selectedTimePeriod').tpTooltipMap[selectedTimePeriodId]); }, this); this.context.on('forecasts:timeperiod:canceled', function() { this.resetSelection(this.tpModel.previous('selectedTimePeriod')); }, this); this.layout.context.on('forecasts:sync:start', function() { this.forecastSyncComplete = false; }, this); this.layout.context.on('forecasts:sync:complete', function() { this.forecastSyncComplete = true; }, this); this.context.on('change:selectedUser', function(model, changed) { app.user.lastState.set('Forecasts:selected-user', changed); this._title = this._getForecastWorksheetLabel(changed); if (!this.disposed) { this.render(); } }, this); this.context.on('plugin:fieldErrorCollection:hasFieldErrors', function(collection, hasErrors) { if(this.fieldHasErrorState !== hasErrors) { this.fieldHasErrorState = hasErrors; } }, this); this.context.on('button:print_button:click', function() { window.print(); }, this); this._super('bindDataChange'); }, /** * Gets the current worksheet type * @return {string} Either "Rollup" or "Direct". Returns empty string if current user could not be found * @private */ _getWorksheetType: function() { let selectedUser = this.context.get('selectedUser'); if (!selectedUser) { return ''; } return app.utils.getForecastType(selectedUser.is_manager, selectedUser.showOpps); }, /** * Gets the correct language label dependent on "Rollup" vs "Direct" worksheet * @param {*} selectedUser The current user whose worksheet is being viewed, stored in this.context * @return {string} * @private */ _getForecastWorksheetLabel: function(selectedUser) { return this._getWorksheetType() === 'Rollup' ? app.lang.get('LBL_RU_TEAM_FORECAST_HEADER', this.module, {name: selectedUser.full_name}) : app.lang.get('LBL_FDR_FORECAST_HEADER', this.module, {name: selectedUser.full_name} ); }, /** * @inheritdoc */ _renderHtml: function() { if(!this._title) { var user = this.context.get('selectedUser') || app.user.toJSON(); this._title = user.full_name; } this._super('_renderHtml'); this.listenTo(this.getField('selectedTimePeriod'), 'render', function() { this.markCurrentTimePeriod(this.tpModel.get('selectedTimePeriod')); }, this); }, /** * @inheritdoc */ _dispose: function() { if(this.layout.context) { this.layout.context.off('forecasts:sync:start', null, this); this.layout.context.off('forecasts:sync:complete', null, this); } this.stopListening(); this._super('_dispose'); }, /** * Sets the timeperiod to the selected timeperiod, used primarily for resetting * the dropdown on nav cancel * * @param String timeperiodId */ resetSelection: function(timeperiodId) { this.tpModel.set({selectedTimePeriod: timeperiodId}, {silent: true}); _.find(this.fields, function(field) { if (_.isEqual(field.name, 'selectedTimePeriod')) { field.render(); return true; } }); }, /** * Get year and quarter * * @param {Object} currentDate timeperiodId * @return array */ getQuarter: function(currentDate) { currentDate = currentDate instanceof app.date ? currentDate : app.date(); const forecastCnf = app.metadata.getModule('Forecasts', 'config') || {}; const forecastStartDateStr = forecastCnf.timeperiod_start_date || app.date().startOf('year').formatServer(true); const forecastStartDate = app.date(forecastStartDateStr); const forecastsTimeperiod = forecastCnf.timeperiod_fiscal_year || null; //extract fiscal start date components const fiscalStartMonth = forecastStartDate.month(); const fiscalStartDay = forecastStartDate.date(); let fiscalYear = currentDate.year(); let fiscalYearStart = app.date(currentDate).set({month: fiscalStartMonth, date: fiscalStartDay}); //adjust year based on forecast time settings if (forecastsTimeperiod === 'current_year' && currentDate.isBefore(fiscalYearStart)) { fiscalYear -= 1; } else if (forecastsTimeperiod === 'next_year' && currentDate.isSameOrAfter(fiscalYearStart)) { fiscalYear += 1; } const quarter = this.determineQuarter(currentDate, fiscalYearStart); return [fiscalYear, quarter]; }, /** * Determine the fiscal quarter for the given date * @param {Object} presentDate * @param {Object} yearStart * * @return {number} fiscal quarter */ determineQuarter: function(presentDate, yearStart) { let currentDate = new Date(presentDate.toLocaleString()); let fiscalYearStart = new Date(yearStart.toLocaleString()); //extracting the fiscal year start month and day //0-based (Jan = 0, Dec = 11) const fiscalStartMonth = fiscalYearStart.getUTCMonth(); const fiscalStartDay = fiscalYearStart.getUTCDate(); const currentYear = currentDate.getUTCFullYear(); //determine the fiscal year start date for the given year let fiscalYearStartDate = new Date(Date.UTC(currentYear, fiscalStartMonth, fiscalStartDay)); //if the current date is before the fiscal start date of the current year, adjust to the previous fiscal year if (currentDate < fiscalYearStartDate) { fiscalYearStartDate = new Date(Date.UTC(currentYear - 1, fiscalStartMonth, fiscalStartDay)); } //define the fiscal quarter start dates properly const fiscalQuarterStartDates = [ fiscalYearStartDate, // Q1 Start this.addMonths(fiscalYearStartDate, 3), // Q2 Start this.addMonths(fiscalYearStartDate, 6), // Q3 Start this.addMonths(fiscalYearStartDate, 9) // Q4 Start ]; //get the timezone from the user preferences if not use GMT let timezone = app.user.getPreference('timezone') || this.defaultTimeZone; const options = {timeZone: timezone}; let currentDateToCompare = new Date(currentDate.toLocaleString('en-US', options)); //identify which quarter the current date falls into using UTC-based comparison for (let i = 0; i < 4; i++) { let fiscalQuarterStart = new Date(fiscalQuarterStartDates[i].toLocaleString('en-US', options)); //compare currentDate and fiscalQuarterStartDates[i] using UTC timestamps if (currentDateToCompare.getTime() < fiscalQuarterStart.getTime()) { //return correct quarter (1-based index) return i; } } return this.lastQuarter; }, /** * add months taking in consideration varying month lengths * * @param {Object} date * @param {number} months * * @return {Object} startOftheQuarter */ addMonths: function(date, months) { let startOftheQuarter = new Date(date); startOftheQuarter.setUTCMonth(startOftheQuarter.getUTCMonth() + months); return startOftheQuarter; }, /** * Get month and year * * @param {Object} d timeperiodId * @return string */ getMonth: function(d) { d = d || app.date(); return d.format('MMMM YYYY'); }, /** * Mark the current time period with 'Current' label * * @param String selectedTimePeriodId */ markCurrentTimePeriod: function(selectedTimePeriodId) { let listTimePeriods = this.getField('selectedTimePeriod') ? this.getField('selectedTimePeriod').items : null; if (!listTimePeriods) { return; } let timePeriodInterval = app.metadata.getModule('Forecasts', 'config').timeperiod_leaf_interval; let currentTimePeriod = timePeriodInterval === 'Quarter' ? this.getQuarter().join(' Q') : this.getMonth(); let currentTimePeriodId = _.findKey(listTimePeriods, item => item === currentTimePeriod); if (!currentTimePeriodId) { return; } let currentTimePeriodText = app.lang.get('LBL_CURRENT', this.module) + ' (' + listTimePeriods[currentTimePeriodId] + ')'; listTimePeriods[currentTimePeriodId] = currentTimePeriodText; if (selectedTimePeriodId === currentTimePeriodId) { this.$('.quarter-picker .forecastsTimeperiod .select2-chosen').text(currentTimePeriodText); } } }) }, "pipeline-metrics": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Forecasts.PipelineMetricsView * @alias SUGAR.App.view.views.ForecastsPipelineMetricsView * @extends View.View */ ({ // Pipeline-metrics View (base) plugins: ['Dashlet'], className: 'pipeline-metrics', events: { 'click .metric-descriptions-close': 'toggleMetricDefinitions' }, /** * Contains the metadata for the set of available metrics by metric name */ availableMetrics: {}, /** * Contains the subset of available metrics that are configured to be shown * via dashlet config */ metrics: {}, /** * List of calculated metrics keys */ calculatedMetrics: [ 'quota', 'commitment', 'quota_coverage', 'gap_quota', 'pct_won_quota', 'quota_gap_coverage', 'commitment_coverage', 'gap_commitment', 'commitment_gap_coverage', 'pct_won_commitment', 'forecast_coverage', 'gap_forecast', 'forecast_gap_coverage', 'pct_won_forecast' ], /** * Boolean storing whether Forecasts are available to the user */ _forecastsIsAvailable: false, /** * Boolean storing true if this instance represents the configuration view */ _isConfig: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._forecastsIsAvailable = this._checkForecastsAvailability(); this._isConfig = this.meta.config; }, /** * Checks to make sure the user has access to Forecasts * * @return {bool} true if Forecasts has been set up and the user has access to it * @private */ _checkForecastsAvailability: function() { let forecastsConfig = app.metadata.getModule('Forecasts', 'config') || {}; return forecastsConfig.is_setup && app.acl.hasAccess('read', 'Forecasts'); }, /** * Implements the initDashlet function of the Dashlet plugin. Initializes * any necessary dashlet configurations */ initDashlet: function() { this.availableMetrics = this.getAvailableMetrics(); this.metrics = this.getSelectedMetrics(); if (this._isConfig) { this._initDashletConfig(); } else { this._initDashletDisplay(); } }, /** * Initializes the dashlet configuration view * * @private */ _initDashletConfig: function() { // Load the metrics options let configFields = _.get(this.dashletConfig, ['panels', 'dashlet_settings', 'fields']); let metricsFieldDef = _.findWhere(configFields, {name: 'metrics'}); if (metricsFieldDef) { metricsFieldDef.options = {}; _.each(this.availableMetrics, function(def, name) { metricsFieldDef.options[name] = app.lang.get(def.label || '', 'Forecasts'); }, this); } this.layout.before('dashletconfig:save', this._validateConfig, this); this.listenTo(this.settings, 'change:metrics', this._handleConfigMetricsChange); }, /** * Initilizes the dashlet main view * * @private */ _initDashletDisplay: function() { this._startAutoRefresh(); let appContext = app.controller.context; if (appContext.get('module') === 'Forecasts' && appContext.get('layout') === 'records') { let updateContext = () => { let selectedUser = appContext.get('selectedUser'); this.context.set({ selectedUserId: selectedUser ? selectedUser.id : app.user.id, selectedUserType: appContext.get('forecastType'), selectedTimePeriodId: appContext.get('selectedTimePeriod') }); }; updateContext(); this.listenTo(appContext,'filter:selectedUser:changed filter:selectedTimePeriod:changed', updateContext); this.listenTo(appContext, 'forecasts:refreshList opportunities:record:saved', this.loadData); } else { this.context.set({ selectedUserId: app.user.get('id'), selectedUserType: app.user.get('is_manager') ? 'Rollup' : 'Direct', selectedTimePeriodId: appContext.get('selectedTimePeriod') || '', }); if (!appContext.get('selectedTimePeriod')) { let self = this; // get the current timeperiod const url = app.api.buildURL('TimePeriods', 'current', {}, {}); app.api.call('read', url, null, { success: function(data) { if (_.isEmpty(data)) { return; } self.context.set({ selectedTimePeriodId: data.id, }); self.loadData(); }, error: function(err) { app.logger.error('Cannot get the current timeperiod: ' + JSON.stringify(err)); } }); } } this.listenTo(this.context, 'change:selectedUserId change:selectedUserType change:selectedTimePeriodId', this.loadData); }, /** * Handles when the user changes the configuration option that determines * the set of metrics shown on the dashlet * * @private */ _handleConfigMetricsChange: function() { let template = app.template.getView('pipeline-metrics.metric-descriptions', this.module); this.$el.find('.metric-descriptions').replaceWith(template({ metrics: _.pick(this.availableMetrics, this.settings.get('metrics')) })); }, /** * Validates the values entered into the dashlet configuration when saving * * @return {boolean} true if the config settings are valid; false otherwise * @private */ _validateConfig: function() { let result = true; // Validate the metrics field let metricsField = this.getField('metrics'); if (metricsField) { metricsField.$el.removeClass('error'); if (_.isEmpty(this.settings.get('metrics'))) { metricsField.$el.addClass('error'); app.alert.show('dashlet_pipeline_invalid_config', { level: 'warning', messages: app.lang.get('LBL_PIPELINE_METRICS_DASHLET_CONFIG_METRICS_REQUIRED', 'Forecasts'), }); result = false; } } return result; }, /** * Initializes the timer that refreshes dashlet data at the interval * defined in dashlet configuration * * @private */ _startAutoRefresh: function() { this._stopAutoRefresh(); let refreshInterval = (this.settings.get('refresh_interval') || 0) * 60000; if (refreshInterval > 0) { this._autoRefreshId = setInterval(_.bind(this.loadData, this), refreshInterval); } }, /** * Cancels the auto-refresh interval timer * * @private */ _stopAutoRefresh: function() { if (this._autoRefreshId) { clearInterval(this._autoRefreshId); this._autoRefreshId = null; } }, /** * Returns the full set of metrics definitions available to this dashlet * * @return {Object} a map of {metric name} => {metric definition} */ getAvailableMetrics: function() { let metrics = this._getAvailableForecastMetrics(); _.each(this.calculatedMetrics, key => { metrics[key] = { name: key, label: app.lang.get(`LBL_METRIC_LABEL_${key.toUpperCase()}`, this.module), helpText: app.lang.get(`LBL_METRIC_HELP_${key.toUpperCase()}`, this.module) }; }, this); return metrics; }, /** * Returns the metrics from the forecast-metrics metadata formatted as * needed for this dashlet * * @private */ _getAvailableForecastMetrics: function() { let availableForecastMetrics = {}; // Pre-format the labels of the forecast-metrics to include the correct // labels based on current field and dom option labels let metrics = _.get(app.metadata.getView('Forecasts', 'forecast-metrics'), 'forecast-metrics') || {}; let forecastStage = app.lang.get('LBL_COMMIT_STAGE_FORECAST', 'Opportunities'); _.each(metrics, function(metric) { metric.helpText = app.lang.get(metric.helpText, 'Forecasts', { forecastStage: forecastStage, commitStageValue: app.lang.getAppListStrings(metric.commitStageDom)[metric.commitStageDomOption] || '' }); metric.label = app.lang.get(metric.label, 'Forecasts'); availableForecastMetrics[metric.name] = metric; }, this); return availableForecastMetrics; }, /** * Returns the set of metrics definitions selected for this dashlet * * @return {Object} a map of {metric name} => {metric definition} */ getSelectedMetrics: function() { let selectedMetrics = this.settings.get('metrics') || []; if (_.isEmpty(selectedMetrics)) { let configFields = _.get(this.dashletConfig, ['panels', 'dashlet_settings', 'fields']); let metricsFieldDef = _.findWhere(configFields, {name: 'metrics'}); let maxMetrics = metricsFieldDef && metricsFieldDef.maximumSelectionSize || 1; selectedMetrics = _.first(_.keys(this.availableMetrics), maxMetrics); this.settings.set('metrics', selectedMetrics); } return _.pick(this.availableMetrics, selectedMetrics); }, /** * @inheritdoc */ _render: function() { if (!this._forecastsIsAvailable) { this.hasConfigAccess = app.acl.hasAccess('admin', 'Forecasts') || app.acl.hasAccess('developer', 'Forecasts'); this.tplName = 'noaccess'; this.template = app.template.getView(`pipeline-metrics.${this.tplName}`, this.module); } this._super('_render'); }, /** * Toggles the visibility of the metric definitions container */ toggleMetricDefinitions: function() { let metricsContainer = this.$el.find('.metric-descriptions-container'); if (metricsContainer.hasClass('hide')) { metricsContainer.css('top', this.el.offsetTop); metricsContainer.removeClass('hide'); this.layout.$el.find('.toggle-metric-definitions-btn .sicon').addClass('text-[--sicon-hover-color]'); } else { metricsContainer.addClass('hide'); this.layout.$el.find('.toggle-metric-definitions-btn .sicon').removeClass('text-[--sicon-hover-color]'); } }, /** * Loads the metrics data for the dashlet * * @param options */ loadData: function(options) { if (this._isConfig || !this._forecastsIsAvailable || _.keys(this.metrics).length === 0) { if (options && options.complete) { options.complete(); } return; } this._loadMetrics(options && options.complete); }, /** * Loads all of the metrics configured for this dashlet * * @param {Function} callback optional callback to run when all metrics are loaded * @private */ _loadMetrics: function(callback) { _.each(this._activeMetricRequests, function(request) { app.api.abortRequest(request.uid); }); this._activeMetricRequests = {}; _.each(this.metrics, function(metric) { this._loadMetric(metric, callback); }, this); }, /** * Loads a single metric * @param metric * @param {Function} callback optional callback to run when all metrics are loaded * @private */ _loadMetric: function(metric, callback) { metric.loading = true; this.$el.find(`.plm-${metric.name}`).addClass('metric-skeleton-loader'); // Build the request arguments let args = { user_id: this.context.get('selectedUserId'), type: this.context.get('selectedUserType'), time_period: this.context.get('selectedTimePeriodId'), metrics: [metric.name] } // Build the request callbacks let callbacks = { success: (metricData) => { this._handleMetricLoadSuccess(metric, metricData); }, error: () => { this._handleMetricLoadError(metric); }, complete: (request) => { this._handleMetricLoadComplete(metric, request, callback); } }; let url = app.api.buildURL('Forecasts/metrics/named'); let request = app.api.call('create', url, args, callbacks); this._activeMetricRequests[request.uid] = request; }, /** * Handles when a single metric has been calculated * * @param metric * @param metricData * @private */ _handleMetricLoadSuccess: function(metric, metricData) { let metricResults = metricData && metricData[metric.name] || {}; switch (metricResults.type) { case 'currency': metric.results = this._formatCurrencyMetricResult(metricResults.value || 0); break; case 'ratio': metric.results = this._formatRatioMetricResult(metricResults.value || 0); break; case 'float': metric.results = this._formatFloatMetricResult(metricResults.value || 0); break; default: metric.results = this._formatNumberMetricResult(metricResults.value || 0); } }, /** * Handles when a single metric encounters an error while being calculated * @param {Object} metric the metric that produced the loading error * @private */ _handleMetricLoadError: function(metric) { metric.results = this._formatCurrencyMetricResult(0); }, /** * Handles when a single metric has completely finished loading * * @param {Object} metric the loaded metric * @param {Object} request the request object used to load the Object * @param {Function} callback optional callback function to run when all * metrics have been loaded * @private */ _handleMetricLoadComplete: function(metric, request, callback) { metric.loading = false; delete this._activeMetricRequests[request.uid]; if (_.isEmpty(this._activeMetricRequests) && _.isFunction(callback)) { callback(); } this._rerenderMetric(metric); }, /** * Formats the results of a currency metric * @param {number} amount the currency amount to format * @return {Object} an object containing the formatted metric information * @private */ _formatCurrencyMetricResult: function(amount) { amount = _.isFinite(amount) ? amount : 0; let systemCurrency = app.currency.getBaseCurrencyId(); let userPrefCurrency = app.user.getPreference('currency_id'); if (systemCurrency === userPrefCurrency) { return { value: app.currency.formatAmountLocale(amount, systemCurrency, 0), tooltip: app.currency.formatAmountLocale(amount, systemCurrency) }; } else { let convertedAmount = app.currency.convertAmount(amount, systemCurrency, userPrefCurrency); return { value: app.currency.formatAmountLocale(amount, systemCurrency, 0), convertedValue: app.currency.formatAmountLocale(convertedAmount, userPrefCurrency, 0), tooltip: `${app.currency.formatAmountLocale(amount, systemCurrency)} | ` + `${app.currency.formatAmountLocale(convertedAmount, userPrefCurrency)}` }; } }, /** * Formats the results of a 'ratio' metric * @param {number} value the ratio value to format * @return {Object} an object containing the formatted metric information: value & tooltip * @private */ _formatRatioMetricResult: function(value) { let percentage = (value || 0) * 100; return { value: `${app.utils.formatNumber(percentage, 0, 0)}%`, tooltip: `${app.utils.formatNumberLocale(percentage)}%` }; }, /** * Formats the results of a 'float' metric * @param {number} value the float value to format * @return {Object} an object containing the formatted metric information: value & tooltip * @private */ _formatFloatMetricResult: function(value) { let outputValue = (value) ? `${app.utils.formatNumber(value, 0, 1)}` : 0; return { value: `${outputValue}x`, tooltip: `${app.utils.formatNumberLocale(value)}x` }; }, /** * Formats the results of a 'number' metric * @param {number} value the number value to format * @return {Object} an object containing the formatted metric information: value & tooltip * @private */ _formatNumberMetricResult: function(value) { return { value: `${app.utils.formatNumber(value, 0, 0)}`, tooltip: `${app.utils.formatNumberLocale(value)}` }; }, /** * Re-renders the given metric in the dashlet * * @param {Object} metric the metric to re-render * @private */ _rerenderMetric: function(metric) { let metricTemplate = app.template.getView('pipeline-metrics.metric', this.module); if (_.isFunction(metricTemplate)) { this.$el.find(`.plm-${metric.name}`).replaceWith(metricTemplate(metric)); } }, /** * @inheritdoc */ _dispose: function() { if (this.layout) { this.layout.offBefore('dashletconfig:save', this._validateConfig, this); } this.stopListening(); this._stopAutoRefresh(); this._super('_dispose'); } }) }, "config-ranges": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigRangesView * @alias SUGAR.App.view.layouts.BaseForecastsConfigRangesView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-ranges View (base) extendsFrom: 'ConfigPanelView', events: { 'click #btnAddCustomRange a': 'addCustomRange', 'click #btnAddCustomRangeWithoutProbability a': 'addCustomRange', 'click .addCustomRange': 'addCustomRange', 'click .removeCustomRange': 'removeCustomRange', 'keyup input[type=text]': 'updateCustomRangeLabel', 'change :radio': 'selectionHandler' }, /** * Holds the fields metadata */ fieldsMeta: {}, /** * used to hold the metadata for the forecasts_ranges field, used to manipulate and render out as the radio buttons * that correspond to the fieldset for each bucket type. */ forecastRangesField: {}, /** * Used to hold the buckets_dom field metadata, used to retrieve and set the proper bucket dropdowns based on the * selection for the forecast_ranges */ bucketsDomField: {}, /** * Used to hold the category_ranges field metadata, used for rendering the sliders that correspond to the range * settings for each of the values contained in the selected buckets_dom dropdown definition. */ categoryRangesField: {}, /** * Holds the values found in Forecasts Config commit_stages_included value */ includedCommitStages: [], //TODO-sfa remove this once the ability to map buckets when they get changed is implemented (SFA-215). /** * This is used to determine whether we need to lock the module or not, based on whether forecasts has been set up already */ disableRanges: false, /** * Used to keep track of the selection as it changes so that it can be used to determine how to hide and show the * sub-elements that contain the fields for setting the category ranges */ selectedRange: '', /** * a placeholder for the individual range sliders that will be used to build the range setting */ fieldRanges: {}, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // parse get the fields metadata _.each(_.first(this.meta.panels).fields, function(field) { this.fieldsMeta[field.name] = field; if (field.name === 'category_ranges') { // get rid of the name key so it doesn't mess up the other fields delete this.fieldsMeta.category_ranges.name; } }, this); // init the fields from metadata this.forecastRangesField = this.fieldsMeta.forecast_ranges; this.bucketsDomField = this.fieldsMeta.buckets_dom; this.categoryRangesField = this.fieldsMeta.category_ranges; // get the included commit stages this.includedCommitStages = this.model.get('commit_stages_included'); // set the values for forecastRangesField and bucketsDomField from the model, so it can be set to selected properly when rendered this.forecastRangesField.value = this.model.get('forecast_ranges'); this.bucketsDomField.value = this.model.get('buckets_dom'); // This will be set to true if the forecasts ranges setup should be disabled this.disableRanges = this.model.get('has_commits'); this.selectedRange = this.model.get('forecast_ranges'); }, /** * @inheritdoc */ _updateTitleValues: function() { var forecastRanges = this.model.get('forecast_ranges'), rangeObjs = this.model.get(forecastRanges + '_ranges'), tmpArr = [], str = '', aSort = function(a, b) { if (a.min < b.min) { return -1; } else if (a.min > b.min) { return 1; } } // Get the keys into an object _.each(rangeObjs, function(value, key) { if(key.indexOf('without_probability') === -1) { tmpArr.push({ min: value.min, max: value.max }); } }); tmpArr.sort(aSort); _.each(tmpArr, function(val) { str += val.min + '% - ' + val.max + '%, '; }); this.titleSelectedValues = str.slice(0, str.length - 2); this.titleSelectedRange = app.lang.getAppListStrings('forecasts_config_ranges_options_dom')[forecastRanges]; }, /** * @inheritdoc */ _updateTitleTemplateVars: function() { this.titleTemplateVars = { title: this.titleViewNameTitle, message: this.titleSelectedRange, selectedValues: this.titleSelectedValues, viewName: this.name }; }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:show_binary_ranges change:show_buckets_ranges change:show_custom_buckets_ranges', function() { this.updateTitle(); }, this ); this.model.on('change:forecast_ranges', function(model) { this.updateTitle(); if(model.get('forecast_ranges') === 'show_custom_buckets') { this.updateCustomRangesCheckboxes(); } }, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); // after the view renders, check for the range that has been selected and // trigger the change event on its element so that it shows this.$(':radio:checked').trigger('change'); if(this.model.get('forecast_ranges') === 'show_custom_buckets') { this.updateCustomRangesCheckboxes(); } }, /** * Handles when the radio buttons change * * @param {jQuery.Event} event */ selectionHandler: function(event) { var newValue = $(event.target).val(), oldValue = this.selectedRange, bucket_dom = this.bucketsDomField.options[newValue], elToHide = this.$('#' + oldValue + '_ranges'), elToShow = this.$('#' + newValue + '_ranges'); // now set the new selection, so that if they change it, // we can later hide the things we are about to show. this.selectedRange = newValue; if(elToShow.children().length === 0) { if(newValue === 'show_custom_buckets') { this._customSelectionHandler(newValue, elToShow); } else { this._selectionHandler(newValue, elToShow); } // use call to set context back to the view for connecting the sliders this.connectSliders.call(this, newValue, this.fieldRanges); } if(elToHide) { elToHide.toggleClass('hide', true); } if(elToShow) { elToShow.toggleClass('hide', false); } // set the forecast ranges and associated dropdown dom on the model this.model.set({ forecast_ranges: newValue, buckets_dom: bucket_dom }); }, /** * Selection handler for standard ranges (two and three ranges) * * @param {Object} elementVal value of the radio button that was clicked * @param {Object} showElement the jQuery-wrapped html element from selectionHandler * @private */ _selectionHandler: function(elementVal, showElement) { var bucketDomStrings = app.lang.getAppListStrings(this.bucketsDomField.options[elementVal]); // add the things here... this.fieldRanges[elementVal] = {}; this.safeAppend(showElement, '<p>' + app.lang.get('LBL_FORECASTS_CONFIG_' + elementVal.toUpperCase() + '_RANGES_DESCRIPTION', 'Forecasts', this) + '</p>'); _.each(bucketDomStrings, function(label, key) { if(key != 'exclude') { var rangeField, model = new Backbone.Model(), fieldSettings; // get the value in the current model and use it to display the slider model.set(key, this.model.get(elementVal + '_ranges')[key]); // build a range field fieldSettings = { view: this, def: this.fieldsMeta.category_ranges[key], viewName: 'edit', context: this.context, module: this.module, model: model, meta: app.metadata.getField({name: 'range', module: this.module}) }; rangeField = app.view.createField(fieldSettings); this.safeAppend(showElement, '<b>' + label + ':</b>').append(rangeField.el); rangeField.render(); // now give the view a way to get at this field's model, so it can be used to set the value on the // real model. this.fieldRanges[elementVal][key] = rangeField; // this gives the field a way to save to the view's real model. It's wrapped in a closure to allow us to // ensure we have everything when switching contexts from this handler back to the view. rangeField.sliderDoneDelegate = function(category, key, view) { return function(value) { this.view.updateRangeSettings(category, key, value); }; }(elementVal, key, this); } }, this); this.safeAppend(showElement, '<p>' + app.lang.get('LBL_FORECASTS_CONFIG_RANGES_EXCLUDE_INFO', 'Forecasts') + '</p>'); }, /** * Selection handler for custom ranges * * @param {Object} elementVal value of the radio button that was clicked * @param {Object} showElement the jQuery-wrapped html element from selectionHandler * @private */ _customSelectionHandler: function(elementVal, showElement) { var bucketDomOptions = {}, elValRanges = elementVal + '_ranges', bucketDomStrings = app.lang.getAppListStrings(this.bucketsDomField.options[elementVal]), rangeField, _ranges = _.clone(this.model.get(elValRanges)); this.fieldRanges[elementVal] = {}; this.safeAppend(showElement, '<p>' + app.lang.get('LBL_FORECASTS_CONFIG_' + elementVal.toUpperCase() + '_RANGES_DESCRIPTION', 'Forecasts', this) + '</p>'); // if custom bucket isn't defined save default values if(!this.model.has(elValRanges)) { this.model.set(elValRanges, {}); } _.each(bucketDomStrings, function(label, key) { if (_.isUndefined(_ranges[key])) { // the range doesn't exist, so we add it to the ranges _ranges[key] = {min: 0, max: 100, in_included_total: false}; } else { // the range already exists, update the in_included_total value _ranges[key].in_included_total = (_.contains(this.includedCommitStages, key)); } bucketDomOptions[key] = label; }, this); this.model.set(elValRanges, _ranges); // save key and label of custom range from the language file to model // then we can add or remove ranges and save it on backend side // bind handler on change to validate data this.model.set(elementVal + '_options', bucketDomOptions); this.model.on('change:' + elementVal + '_options', function(event) { this.validateCustomRangeLabels(elementVal); }, this); // create layout, create placeholders for different types of custom ranges this._renderCustomRangesLayout(showElement, elementVal); // render custom ranges _.each(bucketDomStrings, function(label, key) { rangeField = this._renderCustomRange(key, label, showElement, elementVal); // now give the view a way to get at this field's model, so it can be used to set the value on the // real model. this.fieldRanges[elementVal][key] = rangeField; }, this); // if there are custom ranges not based on probability hide add button on the top of block if(this._getLastCustomRangeIndex(elementVal, 'custom')) { this.$('#btnAddCustomRange').hide(); } // if there are custom ranges not based on probability hide add button on the top of block if(this._getLastCustomRangeIndex(elementVal, 'custom_without_probability')) { this.$('#btnAddCustomRangeWithoutProbability').hide(); } }, /** * Render layout for custom ranges, add placeholders for different types of ranges * * @param {Object} showElement the jQuery-wrapped html element from selectionHandler * @param {string} category type for the ranges 'show_binary' etc. * @private */ _renderCustomRangesLayout: function(showElement, category) { var template = app.template.getView('config-ranges.customRangesDefault', 'Forecasts'), mdl = { category: category }; this.safeAppend(showElement, template(mdl)); }, /** * Creates a new custom range field and renders it in showElement * * @param {string} key * @param {string} label * @param {Object} showElement the jQuery-wrapped html element from selectionHandler * @param {string} category type for the ranges 'show_binary' etc. * @private * @return {View.field} new created field */ _renderCustomRange: function(key, label, showElement, category) { var customType = key, customIndex = 0, isExclude = false, // placeholder to insert custom range currentPlaceholder = showElement, rangeField, model = new Backbone.Model(), fieldSettings, lastCustomRange; // define type of new custom range based on name of range and choose placeholder to insert // custom_default: include, upside or exclude // custom - based on probability // custom_without_probability - not based on probability if(key.substring(0, 26) == 'custom_without_probability') { customType = 'custom_without_probability'; customIndex = key.substring(27); currentPlaceholder = this.$('#plhCustomWithoutProbability'); } else if(key.substring(0, 6) == 'custom') { customType = 'custom'; customIndex = key.substring(7); currentPlaceholder = this.$('#plhCustom'); } else if(key.substring(0, 7) == 'exclude') { customType = 'custom_default'; currentPlaceholder = this.$('#plhExclude'); isExclude = true; } else { customType = 'custom_default'; currentPlaceholder = this.$('#plhCustomDefault'); } // get the value in the current model and use it to display the slider model.set(key, this.model.get(category + '_ranges')[key]); // get the field definition from var fieldDef = this.fieldsMeta.category_ranges[key] || this.fieldsMeta.category_ranges[customType]; // build a range field fieldSettings = { view: this, def: _.clone(fieldDef), viewName: 'forecastsCustomRange', context: this.context, module: this.module, model: model, meta: app.metadata.getField({name: 'range', module: this.module}) }; // set up real range name fieldSettings.def.name = key; // set up view fieldSettings.def.view = 'forecastsCustomRange'; // enable slider fieldSettings.def.enabled = true; rangeField = app.view.createField(fieldSettings); currentPlaceholder.append(rangeField.el); rangeField.label = label; rangeField.customType = customType; // added + to make sure customIndex is numeric rangeField.customIndex = +customIndex; rangeField.isExclude = isExclude; rangeField.in_included_total = (_.contains(this.includedCommitStages, key)); rangeField.category = category; if(key == 'include') { rangeField.isReadonly = true; } rangeField.render(); // enable slider after render rangeField.$(rangeField.fieldTag).noUiSlider('enable'); // hide add button for previous custom range not based on probability lastCustomRange = this._getLastCustomRange(category, rangeField.customType); if(lastCustomRange) { lastCustomRange.$('.addCustomRange').parent().hide(); } // add error class if the range has an empty label if(_.isEmpty(rangeField.label)) { rangeField.$('.control-group').addClass('error'); } else { rangeField.$('.control-group').removeClass('error'); } // this gives the field a way to save to the view's real model. It's wrapped in a closure to allow us to // ensure we have everything when switching contexts from this handler back to the view. rangeField.sliderDoneDelegate = function(category, key, view) { return function(value) { this.view.updateRangeSettings(category, key, value); }; }(category, key, this); return rangeField; }, /** * Returns the index of the last custom range or 0 * * @param {string} category type for the ranges 'show_binary' etc. * @param {string} customType * @return {number} * @private */ _getLastCustomRangeIndex: function(category, customType) { var lastCustomRangeIndex = 0; // loop through all ranges, if there are multiple ranges with the same customType, they'll just overwrite // each other's index and after the loop we'll have the final index left if(this.fieldRanges[category]) { _.each(this.fieldRanges[category], function(range) { if(range.customType == customType && range.customIndex > lastCustomRangeIndex) { lastCustomRangeIndex = range.customIndex; } }, this); } return lastCustomRangeIndex; }, /** * Returns the last created custom range object, if no range object, return upside/include * for custom type and exclude for custom_without_probability type * * @param {string} category type for the ranges 'show_binary' etc. * @param {string} customType * @return {*} * @private */ _getLastCustomRange: function(category, customType) { if(!_.isEmpty(this.fieldRanges[category])) { var lastCustomRange = undefined; // loop through all ranges, if there are multiple ranges with the same customType, they'll just overwrite // each other on lastCustomRange and after the loop we'll have the final one left _.each(this.fieldRanges[category], function(range) { if(range.customType == customType && (_.isUndefined(lastCustomRange) || range.customIndex > lastCustomRange.customIndex)) { lastCustomRange = range; } }, this); if(_.isUndefined(lastCustomRange)) { // there is not custom range - use default ranges if(customType == 'custom') { // use upside or include lastCustomRange = this.fieldRanges[category].upside || this.fieldRanges[category].include; } else { // use exclude lastCustomRange = this.fieldRanges[category].exclude; } } } return lastCustomRange; }, /** * Adds a new custom range field and renders it in specific placeholder * * @param {jQuery.Event} event click */ addCustomRange: function(event) { var self = this, category = $(event.currentTarget).data('category'), customType = $(event.currentTarget).data('type'), categoryRange = category + '_ranges', categoryOptions = category + '_options', ranges = _.clone(this.model.get(categoryRange)), bucketDomOptions = _.clone(this.model.get(categoryOptions)); if (_.isUndefined(category) || _.isUndefined(customType) || _.isUndefined(ranges) || _.isUndefined(bucketDomOptions)) { return false; } var showElement = (customType == 'custom') ? this.$('#plhCustom') : this.$('#plhCustomWithoutProbability'), label = app.lang.get('LBL_FORECASTS_CUSTOM_RANGES_DEFAULT_NAME', 'Forecasts'), rangeField, lastCustomRange = this._getLastCustomRange(category, customType), lastCustomRangeIndex = this._getLastCustomRangeIndex(category, customType); lastCustomRangeIndex++; // setup key for the new range var key = customType + '_' + lastCustomRangeIndex; // set up min/max values for new custom range if (customType != 'custom') { // if range is without probability setup min and max values to 0 ranges[key] = { min: 0, max: 0, in_included_total: false }; } else if (ranges.exclude.max - ranges.exclude.min > 3) { // decrement exclude range to insert new range ranges[key] = { min: parseInt(ranges.exclude.max, 10) - 1, max: parseInt(ranges.exclude.max, 10), in_included_total: false }; ranges.exclude.max = parseInt(ranges.exclude.max, 10) - 2; if (this.fieldRanges[category].exclude.$el) { this.fieldRanges[category].exclude.$(this.fieldRanges[category].exclude.fieldTag) .noUiSlider('move', {handle: 'upper', to: ranges.exclude.max}); } } else if (ranges[lastCustomRange.name].max - ranges[lastCustomRange.name].min > 3) { // decrement previous range to insert new range ranges[key] = { min: parseInt(ranges[lastCustomRange.name].min, 10), max: parseInt(ranges[lastCustomRange.name].min, 10) + 1, in_included_total: false }; ranges[lastCustomRange.name].min = parseInt(ranges[lastCustomRange.name].min, 10) + 2; if (lastCustomRange.$el) { lastCustomRange.$(lastCustomRange.fieldTag) .noUiSlider('move', {handle: 'lower', to: ranges[lastCustomRange.name].min}); } } else { ranges[key] = { min: parseInt(ranges[lastCustomRange.name].min, 10) - 2, max: parseInt(ranges[lastCustomRange.name].min, 10) - 1, in_included_total: false }; } this.model.set(categoryRange, ranges); rangeField = this._renderCustomRange(key, label, showElement, category); if(rangeField) { this.fieldRanges[category][key] = rangeField; } bucketDomOptions[key] = label; this.model.set(categoryOptions, bucketDomOptions); // adding event listener to new custom range rangeField.$(':checkbox').each(function() { var $el = $(this); $el.on('click', _.bind(self.updateCustomRangeIncludeInTotal, self)); app.accessibility.run($el, 'click'); }); if(customType == 'custom') { // use call to set context back to the view for connecting the sliders this.$('#btnAddCustomRange').hide(); this.connectSliders.call(this, category, this.fieldRanges); } else { // hide add button form top of block and for previous ranges not based on probability this.$('#btnAddCustomRangeWithoutProbability').hide(); _.each(this.fieldRanges[category], function(item) { if(item.customType == customType && item.customIndex < lastCustomRangeIndex && item.$el) { item.$('.addCustomRange').parent().hide(); } }, this); } // update checkboxes this.updateCustomRangesCheckboxes(); }, /** * Removes a custom range from the model and view * * @param {jQuery.Event} event click * @return void */ removeCustomRange: function(event) { var category = $(event.currentTarget).data('category'), fieldKey = $(event.currentTarget).data('key'), categoryRanges = category + '_ranges', categoryOptions = category + '_options', ranges = _.clone(this.model.get(categoryRanges)), bucketDomOptions = _.clone(this.model.get(categoryOptions)); if (_.isUndefined(category) || _.isUndefined(fieldKey) || _.isUndefined(this.fieldRanges[category]) || _.isUndefined(this.fieldRanges[category][fieldKey]) || _.isUndefined(ranges) || _.isUndefined(bucketDomOptions)) { return false; } var range, previousCustomRange, lastCustomRangeIndex, lastCustomRange; range = this.fieldRanges[category][fieldKey]; if (_.indexOf(['include', 'upside', 'exclude'], range.name) != -1) { return false; } if(range.customType == 'custom') { // find previous renge and reassign range values form removed to it _.each(this.fieldRanges[category], function(item) { if(item.customType == 'custom' && item.customIndex < range.customIndex) { previousCustomRange = item; } }, this); if(_.isUndefined(previousCustomRange)) { previousCustomRange = (this.fieldRanges[category].upside) ? this.fieldRanges[category].upside : this.fieldRanges[category].include; } ranges[previousCustomRange.name].min = +ranges[range.name].min; if(previousCustomRange.$el) { previousCustomRange.$(previousCustomRange.fieldTag).noUiSlider('move', {handle: 'lower', to: ranges[previousCustomRange.name].min}); } } // update included ranges this.includedCommitStages = _.without(this.includedCommitStages, range.name) // removing event listener for custom range range.$(':checkbox').off('click'); // remove view for the range this.fieldRanges[category][range.name].remove(); delete ranges[range.name]; delete this.fieldRanges[category][range.name]; delete bucketDomOptions[range.name]; this.model.set(categoryOptions, bucketDomOptions); this.model.set(categoryRanges, ranges); lastCustomRangeIndex = this._getLastCustomRangeIndex(category, range.customType); if(range.customType == 'custom') { // use call to set context back to the view for connecting the sliders if (lastCustomRangeIndex == 0) { this.$('#btnAddCustomRange').show(); } this.connectSliders.call(this, category, this.fieldRanges); } else { // show add button for custom range not based on probability if(lastCustomRangeIndex == 0) { this.$('#btnAddCustomRangeWithoutProbability').show(); } } lastCustomRange = this._getLastCustomRange(category, range.customType); if(lastCustomRange.$el) { lastCustomRange.$('.addCustomRange').parent().show(); } // update checkboxes this.updateCustomRangesCheckboxes(); }, /** * Change a label for a custom range in the model * * @param {jQuery.Event} event keyup */ updateCustomRangeLabel: function(event) { var category = $(event.target).data('category'), fieldKey = $(event.target).data('key'), categoryOptions = category + '_options', bucketDomOptions = _.clone(this.model.get(categoryOptions)); if (category && fieldKey && bucketDomOptions) { bucketDomOptions[fieldKey] = $(event.target).val(); this.model.set(categoryOptions, bucketDomOptions); } }, /** * Validate labels for custom ranges, if it is invalid add error style for input * * @param {string} category type for the ranges 'show_binary' etc. */ validateCustomRangeLabels: function(category) { var opts = this.model.get(category + '_options'), hasErrors = false, range; _.each(opts, function(label, key) { range = this.fieldRanges[category][key]; if(_.isEmpty(label.trim())) { range.$('.control-group').addClass('error'); hasErrors = true; } else { range.$('.control-group').removeClass('error'); } }, this); var saveBtn = this.layout.layout.$('[name=save_button]'); if(saveBtn) { if(hasErrors) { // if there are errors, disable the save button saveBtn.addClass('disabled'); } else if(!hasErrors && saveBtn.hasClass('disabled')) { // if there are no errors and the save btn is disabled, enable it saveBtn.removeClass('disabled'); } } }, /** * Change in_included_total value for custom range in model * * @param {Backbone.Event} event change */ updateCustomRangeIncludeInTotal: function(event) { var category = $(event.target).data('category'), fieldKey = $(event.target).data('key'), categoryRanges = category + '_ranges', ranges; if (category && fieldKey) { ranges = _.clone(this.model.get(categoryRanges)); if (ranges && ranges[fieldKey]) { if (fieldKey !== 'exclude' && fieldKey.indexOf('custom_without_probability') == -1) { var isChecked = $(event.target).is(':checked'); ranges[fieldKey].in_included_total = isChecked; if(isChecked) { // silently add this range to the includedCommitStages this.includedCommitStages.push(fieldKey); } else { // silently remove this range from includedCommitStages this.includedCommitStages = _.without(this.includedCommitStages, fieldKey) } this.model.set('commit_stages_included', this.includedCommitStages); } else { ranges[fieldKey].in_included_total = false; } this.model.set(categoryRanges, ranges); this.updateCustomRangesCheckboxes(); } } }, /** * Iterates through custom ranges checkboxes and enables/disables * checkboxes so users can only select certain checkboxes to include ranges */ updateCustomRangesCheckboxes: function() { var els = this.$('#plhCustomDefault :checkbox, #plhCustom :checkbox'), len = els.length, $el, fieldKey, i; for(i = 0; i < len; i++) { $el = $(els[i]); fieldKey = $el.data('key'); //disable the checkbox $el.attr('disabled', true); // remove any click event listeners $el.off('click'); // looking specifically for checkboxes that are not the 'include' checkbox but that are // the last included commit stage range or the first non-included commit stage range if(fieldKey !== 'include' && (i == this.includedCommitStages.length - 1 || i == this.includedCommitStages.length)) { // enable the checkbox $el.attr('disabled', false); // add new click event listener $el.on('click', _.bind(this.updateCustomRangeIncludeInTotal, this)); app.accessibility.run($el, 'click'); } } }, /** * Updates the setting in the model for the specific range types. * This gets triggered when the range slider after the user changes a range * * @param {string} category type for the ranges 'show_binary' etc. * @param {string} range - the range being set, i. e. `include`, `exclude` or `upside` for `show_buckets` category * @param {number} value - the value being set */ updateRangeSettings: function(category, range, value) { var catRange = category + '_ranges', setting = _.clone(this.model.get(catRange)); if (category == 'show_custom_buckets') { value.in_included_total = setting[range].in_included_total || false; } setting[range] = value; this.model.set(catRange, setting); }, /** * Graphically connects the sliders to the one below, so that they move in unison when changed, based on category. * * @param {string} ranges - the forecasts category that was selected, i. e. 'show_binary' or 'show_buckets' * @param {Object} sliders - an object containing the sliders that have been set up in the page. This is created in the * selection handler when the user selects a category type. */ connectSliders: function(ranges, sliders) { var rangeSliders = sliders[ranges]; var probabilitySliders = [rangeSliders.include]; var customSliders = _.sortBy(_.filter( rangeSliders, function(item) { return item.customType == 'custom'; } ), function(item) { return parseInt(item.customIndex, 10); } ); if (rangeSliders.upside) { probabilitySliders.push(rangeSliders.upside); } probabilitySliders = _.union( probabilitySliders, customSliders ); if (rangeSliders.exclude) { probabilitySliders.push(rangeSliders.exclude); } if (probabilitySliders.length) { for (var i = 0; i < probabilitySliders.length; i++) { if (probabilitySliders[i].def) { var offset = 0; if (ranges == 'show_custom_buckets') { offset = 1; } probabilitySliders[i].def.minRange = probabilitySliders.length - i - offset; probabilitySliders[i].def.maxRange = 100 - i; } probabilitySliders[i].connectedSlider = (probabilitySliders[i + 1]) ? probabilitySliders[i + 1] : null; probabilitySliders[i].connectedToSlider = (probabilitySliders[i - 1]) ? probabilitySliders[i - 1] : null; probabilitySliders[i].sliderChangeDelegate = function(value, populateEvent) { // lock the upper handle to 100, as per UI/UX requirements to show a dual slider if (this.name == 'include') { this.$(this.fieldTag).noUiSlider('move', {handle: 'upper', to: this.def.maxRange}); } else if (this.name == 'exclude') { this.$(this.fieldTag).noUiSlider('move', {handle: 'lower', to: this.def.minRange}); } //Bounds the range of handles to prevent users from moving //impossible values. if (value.min < this.def.minRange) { this.$(this.fieldTag).noUiSlider('move', {handle: 'lower', to: this.def.minRange}); } if (value.max < this.def.minRange) { this.$(this.fieldTag).noUiSlider('move', {handle: 'upper', to: this.def.minRange}); } if (value.min > this.def.maxRange) { this.$(this.fieldTag).noUiSlider('move', {handle: 'lower', to: this.def.maxRange}); } if (value.max > this.def.maxRange) { this.$(this.fieldTag).noUiSlider('move', {handle: 'upper', to: this.def.maxRange}); } value.min = this.$(this.fieldTag).noUiSlider('value')[0]; value.max = this.$(this.fieldTag).noUiSlider('value')[1]; if (this.connectedSlider) { var connectedSliderEl = this.connectedSlider.$(this.connectedSlider.fieldTag); connectedSliderEl.noUiSlider('move', {handle: 'upper', to: value.min - 1}); if (value.min <= connectedSliderEl.noUiSlider('value')[0] + 1) { connectedSliderEl.noUiSlider('move', {handle: 'lower', to: value.min - 1}); connectedSliderEl.noUiSlider('move', {handle: 'upper', to: value.min - 1}); } if (_.isUndefined(populateEvent) || populateEvent == 'down') { this.connectedSlider.sliderChangeDelegate.call(this.connectedSlider, { min: connectedSliderEl.noUiSlider('value')[0], max: connectedSliderEl.noUiSlider('value')[1] }, 'down'); } } if (this.connectedToSlider) { var connectedToSliderEl = this.connectedToSlider.$(this.connectedToSlider.fieldTag); connectedToSliderEl.noUiSlider('move', {handle: 'lower', to: value.max + 1}); if (value.max >= connectedToSliderEl.noUiSlider('value')[1] - 1) { connectedToSliderEl.noUiSlider('move', {handle: 'upper', to: value.max + 1}); connectedToSliderEl.noUiSlider('move', {handle: 'lower', to: value.max + 1}); } if (_.isUndefined(populateEvent) || populateEvent == 'up') { this.connectedToSlider.sliderChangeDelegate.call(this.connectedToSlider, { min: connectedToSliderEl.noUiSlider('value')[0], max: connectedToSliderEl.noUiSlider('value')[1] }, 'up'); } } if (ranges == 'show_binary' && this.name == 'include') { this.view.setExcludeValueForLastSlider(value, ranges, rangeSliders.include); } else if (ranges == 'show_buckets' && this.name == 'upside') { this.view.setExcludeValueForLastSlider(value, ranges, rangeSliders.upside); } }; } } }, /** * Provides a way for the last of the slider fields in the view, to set the value for the exclude range. * * @param {Object} value the range value of the slider * @param {string} ranges the selected config range * @param {Object} slider the slider */ setExcludeValueForLastSlider: function(value, ranges, slider) { var excludeRange = { min: 0, max: 100 }, settingName = ranges + '_ranges', setting = _.clone(this.model.get(settingName)); excludeRange.max = value.min - 1; excludeRange.min = slider.def.minRange - 1; setting.exclude = excludeRange; this.model.set(settingName, setting); }, safeAppend: function(el, value) { return el.append(DOMPurify.sanitize(value)); } }) }, "info": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Forecasts.InfoView * @alias SUGAR.App.view.views.BaseForecastsInfoView * @extends View.View */ ({ // Info View (base) /** * @inheritdoc * */ initialize: function(options) { if (app.lang.direction === 'rtl') { options.template = app.template.getView('info.info-rtl', 'Forecasts'); // reverse the datapoints options.meta.datapoints.reverse(); } this._super("initialize", [options]); // Use the next commit model as this view's model this.model = this.context.get('nextCommitModel'); }, /** * @inheritdoc * */ bindDataChange: function(){ this.listenTo(this.context, 'forecasts:worksheet:totals:initialized', this._handleWorksheetTotalsInitialized); this.listenTo(this.context, 'forecasts:commit-models:loaded', this._handleCommitModelsLoaded); this.listenTo(this.context, 'button:cancel_button:click', this._handleCancelClicked); this.listenTo(this.context, 'change:selectedUser', this.loadIncludeData); this.listenTo(this.context, 'change:selectedTimePeriod', this.loadIncludeData); this.listenTo(this.context, 'change:forecastType', this.loadIncludeData); }, /** * @inheritdoc * */ loadData: function() { this._super('loadData'); this.loadIncludeData(); }, /** * Loads the included Opp and RLI data for the currently viewed forecast. */ loadIncludeData: function() { if (this.context.get('forecastType') === 'Direct') { let totals = {}; let forecastConfig = app.metadata.getModule('Forecasts').config; let oppFields = {}; _.each(this.meta.datapoints, function(datapoint) { oppFields[datapoint.name] = datapoint.total_field || datapoint.name; }); let oppRequest = this.getRequest('Opportunities', oppFields); let data = { 'requests': [oppRequest] }; if (forecastConfig.forecast_by === 'RevenueLineItems') { let rliFields = {}; _.each(this.meta.datapoints, function(datapoint) { rliFields[datapoint.name] = datapoint.name; }); let rliRequests = this.getRequest('RevenueLineItems', rliFields); data.requests.push(rliRequests); } let url = app.api.buildURL('bulk'); let callbacks = { success: _.bind(function(data) { let totals = {}; _.each(this.meta.datapoints, function(datapoint) { totals[datapoint.name] = _.first(data).contents.metrics[datapoint.name].values.sum; }); if (forecastConfig.forecast_by === 'RevenueLineItems') { _.each(this.meta.datapoints, function(datapoint) { totals['rli_' + datapoint.name] = data[1].contents.metrics[datapoint.name].values.sum; }); } this.syncedTotals = totals; this._syncDatapointValues(); this.context.trigger('forecasts:worksheet:totals', totals, 'test'); },this) }; app.api.call('create', url, data, callbacks); } }, /** * Gets the request used in a bulk request for a module specific metric * request * * @param {string} module * @param {string} fields * @return {Object} An object that represents an api request for the bulk * api. */ getRequest: function(module, fields) { let selectedUser = this.context.get('selectedUser'); let url = app.api.buildURL('Forecasts/metrics'); url = url.substr(5); let metrics = this.buildMetrics(fields); let data = { 'filter': [], 'module': module, 'user_id': selectedUser ? selectedUser.id : '', 'time_period': this.context.get('selectedTimePeriod'), 'type': this.context.get('forecastType'), 'metrics': metrics }; return { 'url': url, 'method': 'POST', 'data': data }; }, /** * Returns the metrics for an array of fields * * @param {Array} fields * @return Array */ buildMetrics: function(fields) { let metrics = []; _.each(fields, function(field, name) { metrics.push(this.buildMetric(name, field)); }, this); return metrics; }, /** * Returns the metric which filters on included commit stages * * @param {string} name * @param {string} sumField * @return {Object} */ buildMetric: function(name, sumField) { let includeStages = app.metadata.getModule('Forecasts').config.commit_stages_included || ['include']; let filter = [ { 'commit_stage': { '$in': includeStages } } ]; return { 'name': name, 'filter': filter, 'sum_fields': sumField }; }, /** * Handles when the layout's commit models have been loaded * * @private */ _handleCommitModelsLoaded: function() { this._syncDatapointValues(); }, /** * Handles when the totals of the worksheet records are initially * loaded and calculated * * @param {Object} totals * @private */ _handleWorksheetTotalsInitialized: function(totals) { this.syncedTotals = totals; this._syncDatapointValues(); }, /** * Takes the last committed model (if applicable) and determines if this values should * be used at the synced/baseline values for the datapoint fields * or initiate that value at 0 * * @private */ _syncDatapointValues: function() { // Get the last commit model let lastCommitModel = this.context.get('lastCommitModel'); // Sync any last committed datapoint values if necessary let valuesToSync = {}; if (lastCommitModel instanceof Backbone.Model) { _.each(this.meta.datapoints, function(datapoint) { valuesToSync[datapoint.name] = lastCommitModel.get(datapoint.name); }, this); } else if (this.syncedTotals) { _.each(this.meta.datapoints, function(datapoint) { valuesToSync[datapoint.name] = 0; }, this); } this._setNextCommitModel(valuesToSync); }, /** * Set next committed model data for the datapoint fields * * @param {Object} valuesToSync * @private */ _setNextCommitModel: function(valuesToSync) { // Get the next commit model let nextCommitModel = this.context.get('nextCommitModel'); nextCommitModel.setSyncedAttributes(valuesToSync); nextCommitModel.set(valuesToSync); }, /** * Handles when the edit cancel button is clicked in the Forecasts view * @private */ _handleCancelClicked: function() { // Revert the next commit model's attributes let nextCommitModel = this.context.get('nextCommitModel'); if (nextCommitModel instanceof Backbone.Model) { nextCommitModel.revertAttributes(); } }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); this._super('_dispose'); } }) }, "config-worksheet-columns": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigWorksheetColumnsView * @alias SUGAR.App.view.layouts.BaseForecastsConfigWorksheetColumnsView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-worksheet-columns View (base) extendsFrom: 'ConfigPanelView', /** * Holds the select2 reference to the #wkstColumnSelect element */ wkstColumnsSelect2: undefined, /** * Holds the default/selected items */ selectedOptions: [], /** * Holds all items */ allOptions: [], /** * The field object id/label for likely_case */ likelyFieldObj: {}, /** * The field object id/label for best_case */ bestFieldObj: {}, /** * The field object id/label for worst_case */ worstFieldObj: {}, /** * @inheritdoc */ initialize: function(options) { // patch metadata for if opps_view_by is Opportunities, not RLIs if (app.metadata.getModule('Opportunities', 'config').opps_view_by === 'Opportunities') { _.each(_.first(options.meta.panels).fields, function(field) { if (field.label_module && field.label_module === 'RevenueLineItems') { field.label_module = 'Opportunities'; } }); } this._super('initialize', [options]); this.allOptions = []; this.selectedOptions = []; var cfgFields = this.model.get('worksheet_columns'), index = 0; // set up scenarioOptions _.each(this.meta.panels[0].fields, function(field) { var obj = { id: field.name, text: app.lang.get(field.label, this._getLabelModule(field.name, field.label_module)), index: index, locked: field.locked || false }, cField = _.find(cfgFields, function(cfgField) { return cfgField == field.name; }, this), addFieldToFullList = true; // save the field objects if (field.name == 'best_case') { this.bestFieldObj = obj; addFieldToFullList = (this.model.get('show_worksheet_best') === 1); } else if (field.name == 'likely_case') { this.likelyFieldObj = obj; addFieldToFullList = (this.model.get('show_worksheet_likely') === 1); } else if (field.name == 'worst_case') { this.worstFieldObj = obj; addFieldToFullList = (this.model.get('show_worksheet_worst') === 1); } if (addFieldToFullList) { this.allOptions.push(obj); } // If the current field being processed was found in the config fields, if (!_.isUndefined(cField)) { // push field to defaults this.selectedOptions.push(obj); } index++; }, this); }, /** * Empty function as the title values have already been set properly * with the change:worksheet_columns event handler * * @inheritdoc */ _updateTitleValues: function() { }, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:worksheet_columns', function() { var arr = [], cfgFields = this.model.get('worksheet_columns'), metaFields = this.meta.panels[0].fields; _.each(metaFields, function(metaField) { _.each(cfgFields, function(field) { if (metaField.name == field) { arr.push( app.lang.get( metaField.label, this._getLabelModule(metaField.name, metaField.label_module) ) ); } }, this); }, this); this.titleSelectedValues = arr.join(', '); // Handle truncating the title string and adding "..." this.titleSelectedValues = this.titleSelectedValues.slice(0, 50) + '...'; this.updateTitle(); }, this); // trigger the change event to set the title when this gets added this.model.trigger('change:worksheet_columns'); this.model.on('change:scenarios', function() { // check model settings and update select2 options if (this.model.get('show_worksheet_best')) { this.addOption(this.bestFieldObj); } else { this.removeOption(this.bestFieldObj); } if (this.model.get('show_worksheet_likely')) { this.addOption(this.likelyFieldObj); } else { this.removeOption(this.likelyFieldObj); } if (this.model.get('show_worksheet_worst')) { this.addOption(this.worstFieldObj); } else { this.removeOption(this.worstFieldObj); } // force render this._render(); // update the model, since a field was added or removed var arr = []; _.each(this.selectedOptions, function(field) { arr.push(field.id); }, this); this.model.set('worksheet_columns', arr); }, this); }, /** * Adds a field object to allOptions & selectedOptions if it is not found in those arrays * * @param {Object} fieldObj */ addOption: function(fieldObj) { if (!_.contains(this.allOptions, fieldObj)) { this.allOptions.splice(fieldObj.index, 0, fieldObj); this.selectedOptions.splice(fieldObj.index, 0, fieldObj); } }, /** * Removes a field object to allOptions & selectedOptions if it is not found in those arrays * * @param {Object} fieldObj */ removeOption: function(fieldObj) { this.allOptions = _.without(this.allOptions, fieldObj); this.selectedOptions = _.without(this.selectedOptions, fieldObj); }, /** * @inheritdoc */ _render: function() { this._super('_render'); // handle setting up select2 options this.wkstColumnsSelect2 = this.$('#wkstColumnsSelect').select2({ data: this.allOptions, multiple: true, containerCssClass: 'select2-choices-pills-close', initSelection: _.bind(function(element, callback) { callback(this.selectedOptions); }, this) }); this.wkstColumnsSelect2.select2('val', this.selectedOptions); this.wkstColumnsSelect2.on('change', _.bind(this.handleColumnModelChange, this)); }, /** * Handles the select2 adding/removing columns * * @param {Object} evt change event from the select2 selected values */ handleColumnModelChange: function(evt) { // did we add something? if so, lets add it to the selectedOptions if (!_.isUndefined(evt.added)) { this.selectedOptions.push(evt.added); } // did we remove something? if so, lets remove it from the selectedOptions if (!_.isUndefined(evt.removed)) { this.selectedOptions = _.without(this.selectedOptions, evt.removed); } this.model.set('worksheet_columns', evt.val); }, /** * @inheritdoc * * Remove custom listeners off select2 instances */ _dispose: function() { if (this.wkstColumnsSelect2) { this.wkstColumnsSelect2.off(); this.wkstColumnsSelect2.select2('destroy'); this.wkstColumnsSelect2 = null; } this._super('_dispose'); }, /** * Re-usable method to get the module label for the column list * * @param {String} fieldName The field we are currently looking at * @param {String} setModule If the metadata has a module set it will be passed in here * @return {string} * @private */ _getLabelModule: function(fieldName, setModule) { var labelModule = setModule || 'Forecasts'; if (fieldName === 'parent_name') { // when we have the parent_name, pull the label from the module we are forecasting by labelModule = this.model.get('forecast_by'); } return labelModule; } }) }, "config-timeperiods": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigTimeperiodsView * @alias SUGAR.App.view.layouts.BaseForecastsConfigTimeperiodsView * @extends View.Views.Base.ConfigPanelView */ ({ // Config-timeperiods View (base) extendsFrom: 'ConfigPanelView', /** * Holds the moment.js date object * @type Moment */ tpStartDate: undefined, /** * If the Fiscal Year field is displayed, this holds the reference to the field */ fiscalYearField: undefined, /** * Holds the timeperiod_fiscal_year metadata so it doesn't render until the view needs it */ fiscalYearMeta: undefined, /** * Holds the default value for number of timeperiods shown from the current that are displayed ( forward&backward ) */ timeperiodShownDefault: '02', /** * @inheritdoc */ initialize: function(options) { // remove the fiscal year metadata since we cant use the enabled check var fieldsMeta = _.filter(_.first(options.meta.panels).fields, function(field) { if (field.name === 'timeperiod_fiscal_year') { this.fiscalYearMeta = _.clone(field); } // return all fields except fiscal year return field.name !== 'timeperiod_fiscal_year'; }, this); // put updated fields back into options _.first(options.meta.panels).fields = fieldsMeta; this._super('initialize', [options]); // check if Forecasts is set up, if so, make the timeperiod field readonly if (!this.model.get('is_setup')) { _.each(fieldsMeta, function(field) { if (field.name == 'timeperiod_start_date') { field.click_to_edit = true; } }, this); } this.tpStartDate = this.model.get('timeperiod_start_date'); if (this.tpStartDate) { // convert the tpStartDate to a Moment object this.tpStartDate = app.date(this.tpStartDate); } }, /** * @inheritdoc */ _updateTitleValues: function() { this.titleSelectedValues = (this.tpStartDate) ? this.tpStartDate.formatUser(true) : ''; }, /** * Checks the timeperiod start date to see if it's 01/01 to know * if we need to display the Fiscal Year field or not */ checkFiscalYearField: function() { // moment.js months are zero-based: 0 = January if (this.tpStartDate.month() !== 0 || (this.tpStartDate.month() === 0 && this.tpStartDate.date() !== 1)) { // if the start date's month isn't Jan, // or it IS Jan but a date other than the 1st, add the field this.addFiscalYearField(); } else if (this.fiscalYearField) { this.model.set({ timeperiod_fiscal_year: null }); this.removeFiscalYearField(); } }, /** * @inheritdoc */ bindDataChange: function() { if (this.model) { this.model.once('change', function(model) { // on a fresh install with no demo data, // this.model has the values and the param model is undefined if (_.isUndefined(model)) { model = this.model; } }, this); this.model.on('change:timeperiod_start_date', function(model) { this.tpStartDate = app.date(model.get('timeperiod_start_date')); this.checkFiscalYearField(); this.titleSelectedValues = this.tpStartDate.formatUser(true); this.updateTitle(); }, this); } }, /** * Creates the fiscal-year field and adds it to the DOM */ addFiscalYearField: function() { if (!this.fiscalYearField) { // set the value so the fiscal-year field chooses its first option // in the dropdown this.model.set({ timeperiod_fiscal_year: 'current_year' }); var $el = this.$('#timeperiod_start_date_subfield'); if ($el) { var fiscalYearFieldMeta = this.updateFieldMetadata(this.fiscalYearMeta), fieldSettings = { view: this, def: fiscalYearFieldMeta, viewName: 'edit', context: this.context, module: this.module, model: this.model, meta: app.metadata.getField({name: 'enum', module: this.module}) }; this.fiscalYearField = app.view.createField(fieldSettings); $el.html(this.fiscalYearField.el); this.fiscalYearField.render(); } } else { if (this.tpStartDate instanceof app.date) { let currentSelectedYear = this.tpStartDate.year(); if (this.fiscalYearField.items && this.fiscalYearField.items.current_year) { //using base 10 to explicitly prevent unexpected behavior const currentYear = parseInt(this.fiscalYearField.items.current_year, 10); if (!isNaN(currentYear) && currentYear !== currentSelectedYear) { this.fiscalYearField.items = { current_year: currentSelectedYear, next_year: currentSelectedYear + 1 }; this.fiscalYearField.def.startYear = currentSelectedYear; this.model.set('timeperiod_fiscal_year', 'current_year'); this.fiscalYearField.render(); } } } } }, /** * Takes the default fiscal-year metadata and adds any dynamic values * Done in function form in case this field ever needs to be extended with * more than just 2 years * * @param {Object} fieldMeta The field's metadata * @return {Object} */ updateFieldMetadata: function(fieldMeta) { fieldMeta.startYear = this.tpStartDate.year(); return fieldMeta; }, /** * Disposes the fiscal-year field and removes it from the DOM */ removeFiscalYearField: function() { this.model.set({ timeperiod_fiscal_year: null }); this.fiscalYearField.dispose(); this.fiscalYearField = null; this.$('#timeperiod_start_date_subfield').html(''); }, /** * @inheritdoc * * Sets up a binding to the start month dropdown to populate the day drop down on change * * @param {View.Field} field * @private */ _renderField: function(field) { field = this._setUpTimeperiodConfigField(field); // check for all fields, if forecast is setup, set to detail/readonly mode if (this.model.get('is_setup')) { field.options.def.view = 'detail'; } else if (field.name == 'timeperiod_start_date') { // if this is the timeperiod_start_date field and Forecasts is not setup field.options.def.click_to_edit = true; } this._super('_renderField', [field]); if (field.name == 'timeperiod_start_date') { if (this.model.get('is_setup')) { var year = this.model.get('timeperiod_start_date').substring(0, 4), str, $el; if (this.model.get('timeperiod_fiscal_year') === 'next_year') { year++; } str = app.lang.get('LBL_FISCAL_YEAR', 'Forecasts') + ': ' + year; $el = this.$('#timeperiod_start_date_sublabel'); if ($el) { $el.html(str); } } else { this.tpStartDate = app.date(this.model.get('timeperiod_start_date')); this.checkFiscalYearField(); } } }, /** * Sets up the fields with the handlers needed to properly get and set their values for the timeperiods config view. * * @param {View.Field} field the field to be setup for this config view. * @return {*} field that has been properly setup and augmented to function for this config view. * @private */ _setUpTimeperiodConfigField: function(field) { switch (field.name) { case 'timeperiod_shown_forward': case 'timeperiod_shown_backward': return this._setUpTimeperiodShowField(field); case 'timeperiod_interval': return this._setUpTimeperiodIntervalBind(field); default: return field; } }, /** * Sets up the timeperiod_shown_forward and timeperiod_shown_backward dropdowns to set the model and values properly * * @param {View.Field} field The field being set up. * @return {*} The configured field. * @private */ _setUpTimeperiodShowField: function(field) { // ensure Date object gets an additional function field.events = _.extend({'change input': '_updateSelection'}, field.events); field.bindDomChange = function() {}; field._updateSelection = function(event) { var value = $(event.currentTarget).val(); this.def.value = value; this.model.set(this.name, value); }; // force value to a string so hbs has helper will match the dropdown correctly let fieldValue = this.model.get(field.name); if (field.hasOwnProperty('def') && _.isObject(field.def.options)) { let fieldOptionsKey = _.findKey(field.def.options, enumVal => enumVal === fieldValue); fieldValue = fieldOptionsKey || this.timeperiodShownDefault; } else { fieldValue = this.timeperiodShownDefault; } this.model.set(field.name, fieldValue, {silent: true}); field.def.value = this.model.get(field.name) || this.timeperiodShownDefault; return field; }, /** * Sets up the change event on the timeperiod_interval drop down to maintain the interval selection * and push in the default selection for the leaf period * * @param {View.Field} field the dropdown interval field * @return {*} * @private */ _setUpTimeperiodIntervalBind: function(field) { field.def.value = this.model.get(field.name); // ensure selected day functions like it should field.events = _.extend({'change input': '_updateIntervals'}, field.events); field.bindDomChange = function() {}; if (typeof(field.def.options) == 'string') { field.def.options = app.lang.getAppListStrings(field.def.options); } /** * function that updates the selected interval * @param {Event} event * @private */ field._updateIntervals = function(event) { //get the timeperiod interval selector var selected_interval = $(event.currentTarget).val(); this.def.value = selected_interval; this.model.set(this.name, selected_interval); this.model.set('timeperiod_leaf_interval', selected_interval == 'Annual' ? 'Quarter' : 'Month'); }; return field; } }) }, "metrics-info": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * View for managing the help component's header bar. * * @class View.Views.Base.ForecastsMetricsInfoView * @alias SUGAR.App.view.layouts.BaseForecastsMetricsInfoView * @extends View.View */ ({ // Metrics-info View (base) /** * List of the metric boxes */ guide: [], /** * @inheritdoc */ _render: function() { this._formatHelpText(); this._super('_render'); }, /** * Helper function to clean and formate the help text with the correct(in case renamed) * Forecast Stage field name and Commit Stage Values ('Include', 'Exclude' and 'Upside') * * @private */ _formatHelpText: function() { this.guide = app.metadata.getView('Forecasts','forecast-metrics')['forecast-metrics']; this.guide.forEach(ele => { let commitStage = app.lang.getAppListStrings(ele.commitStageDom)[ele.commitStageDomOption]; ele.helpText = app.lang.getModString( ele.helpText, 'Forecasts', { forecastStage: app.lang.get('LBL_COMMIT_STAGE_FORECAST', 'Opportunities'), commitStageValue: commitStage, }); }); } }) }, "forecast-pipeline": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Forecast-pipeline View (base) extendsFrom: 'SalesPipelineView', /** * Is the forecast Module setup?? */ forecastSetup: 0, /** * Holds the forecast isn't set up message if Forecasts hasn't been set up yet */ forecastsNotSetUpMsg: undefined, /** * Track if current user is manager. */ isManager: false, /** * @inheritDoc */ initialize: function(options) { options.meta.type = 'sales-pipeline'; options.meta = _.extend({}, app.metadata.getView(this.module, 'sales-pipeline'), options.meta); this._super('initialize', [options]); } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsConfigHeaderButtonsView * @alias SUGAR.App.view.layouts.BaseForecastsConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * @inheritdoc */ _beforeSaveConfig: function() { var ctxModel = this.context.get('model'); // Set config settings before saving ctxModel.set({ is_setup:true, show_forecasts_commit_warnings: true }); // update the commit_stages_included property and // remove 'include_in_totals' from the ranges so it doesn't get saved if(ctxModel.get('forecast_ranges') == 'show_custom_buckets') { var ranges = ctxModel.get('show_custom_buckets_ranges'), labels = ctxModel.get('show_custom_buckets_options'), commitStages = [], finalLabels = []; ctxModel.unset('commit_stages_included'); _.each(ranges, function(range, key) { if (range.in_included_total) { commitStages.push(key); } delete range.in_included_total; finalLabels.push([key, labels[key]]); }, this); ctxModel.set({ commit_stages_included: commitStages, show_custom_buckets_ranges: ranges, show_custom_buckets_options: finalLabels }, {silent: true}); } }, /** * @inheritdoc */ cancelConfig: function() { if (app.metadata.getModule('Forecasts', 'config').is_setup) { return this._super('cancelConfig'); } if (this.triggerBefore('cancel')) { if (app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } // Redirect to Admin panel if Forecasts has not been set up app.router.navigate('#Administration', {trigger: true}); } }, /** * @inheritdoc */ _saveConfig: function() { var url = app.api.buildURL(this.module, 'config'); app.api.call('create', url, this.model.attributes, { success: _.bind(function() { if (app.drawer.count()) { this.showSavedConfirmation(); // close the drawer and return to Forecasts app.drawer.close(this.context, this.context.get('model')); // Forecasts requires a refresh, always, so we force it Backbone.history.loadUrl(app.api.buildURL(this.module)); } else { app.router.navigate(this.module, {trigger: true}); } }, this), error: _.bind(function() { this.getField('save_button').setDisabled(false); }, this) } ); } }) }, "forecasts-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Dashlet that displays a chart */ ({ // Forecasts-chart View (base) plugins: ['Dashlet'], /** * This is the values model for the template */ values: new Backbone.Model(), className: 'forecasts-chart-wrapper', /** * Hold the initOptions if we have to call the Forecast/init end point cause we are not on Forecasts */ initOptions: null, /** * The context of the ForecastManagerWorksheet Module if one exists */ forecastManagerWorksheetContext: undefined, /** * The context of the ForecastWorksheet Module if one exists */ forecastWorksheetContext: undefined, /** * Track if current user is manager. */ isManager: false, /** * @inheritdoc */ initialize: function(options) { // after we init, find and bind to the Worksheets Contexts this.once('init', this.findWorksheetContexts, this); this.once('render', function() { this.parseCollectionForData(); }, this); this.isManager = app.user.get('is_manager'); this._super('initialize', [options]); if (!this.meta.config) { var ctx = this.context.parent, user = ctx.get('selectedUser') || app.user.toJSON(), showMgr = ctx.get('model').get('forecastType') == 'Rollup'; this.values.set({ user_id: user.id, display_manager: showMgr, show_target_quota: (user.is_manager && !user.is_top_level_manager), ranges: ctx.get('selectedRanges') || ['include'], timeperiod_id: ctx.get('selectedTimePeriod'), dataset: 'likely', group_by: 'forecast', no_data: true }); } }, /** * Specific code to run after a dashlet Init Code has ran */ initDashlet: function() { var fieldOptions, cfg = app.metadata.getModule('Forecasts', 'config'); fieldOptions = app.lang.getAppListStrings(this.dashletConfig.dataset.options); this.dashletConfig.dataset.options = {}; if (cfg.show_worksheet_worst && app.acl.hasAccess('view', 'ForecastWorksheets', app.user.get('id'), 'worst_case')) { this.dashletConfig.dataset.options['worst'] = fieldOptions['worst']; } if (cfg.show_worksheet_likely) { this.dashletConfig.dataset.options['likely'] = fieldOptions['likely']; } if (cfg.show_worksheet_best && app.acl.hasAccess('view', 'ForecastWorksheets', app.user.get('id'), 'best_case')) { this.dashletConfig.dataset.options['best'] = fieldOptions['best']; } // Hide dataset drop-down if there is only one option. this.dashletConfig.show_dataset = true; if (_.size(this.dashletConfig.dataset.options) <= 1) { this.dashletConfig.show_dataset = false; } }, /** * Loop though the parent context children context to find the worksheet, if they exist */ findWorksheetContexts: function() { // loop though the children context looking for the ForecastWorksheet and ForecastManagerWorksheet Modules _.filter(this.context.parent.children, function(item) { if (item.get('module') == 'ForecastWorksheets') { this.forecastWorksheetContext = item; return true; } else if (item.get('module') == 'ForecastManagerWorksheets') { this.forecastManagerWorksheetContext = item; return true; } return false; }, this); var collection; if (this.forecastWorksheetContext) { // listen for collection change events collection = this.forecastWorksheetContext.get('collection'); if (collection) { collection.on('change', this.repWorksheetChanged, this); collection.on('reset', function(collection) { this.parseCollectionForData(collection); }, this); } } if (this.forecastManagerWorksheetContext) { // listen for collection change events collection = this.forecastManagerWorksheetContext.get('collection'); if (collection) { collection.on('change', this.mgrWorksheetChanged, this); collection.on('reset', function(collection) { this.parseCollectionForData(collection); }, this); } } }, /** * Figure out which way we need to parse a collection * * @param {Backbone.Collection} [collection] */ parseCollectionForData: function(collection) { if (this.meta.config) { return; } // get the field var field = this.getField('paretoChart'); if(field && !field.hasServerData()) { // if the field does not have any data, wait for the xhr call to run and then just call this // method again field.once('chart:pareto:rendered', this.parseCollectionForData, this); return; } if (!_.isUndefined(this.forecastManagerWorksheetContext)) { if (this.values.get('display_manager')) { this.parseManagerWorksheet(collection || this.forecastManagerWorksheetContext.get('collection')); } } }, /** * Parses a chart data collection for the Rep worksheet * * @param {Backbone.Collection} collection */ parseRepWorksheet: function(collection) { var field = this.getField('paretoChart'); if(field) { var serverData = field.getServerData(); serverData.data = collection.map(function(item) { var i = { id: item.get('id'), forecast: item.get('commit_stage'), probability: item.get('probability'), sales_stage: item.get('sales_stage'), likely: app.currency.convertWithRate(item.get('likely_case'), item.get('base_rate')), date_closed_timestamp: parseInt(item.get('date_closed_timestamp')) }; if (!_.isUndefined(this.dashletConfig.dataset.options['best'])) { i.best = app.currency.convertWithRate(item.get('best_case'), item.get('base_rate')); } if (!_.isUndefined(this.dashletConfig.dataset.options['worst'])) { i.worst = app.currency.convertWithRate(item.get('worst_case'), item.get('base_rate')); } return i; }, this); field.setServerData(serverData, true); } }, /** * Parses a chart data collection for the Manager worksheet * * @param {Backbone.Collection} collection */ parseManagerWorksheet: function(collection) { var field = this.getField('paretoChart'); if(field) { var serverData = field.getServerData(); serverData.data = collection.map(function(item) { var i = { id: item.get('id'), user_id: item.get('user_id'), name: item.get('name'), likely: app.currency.convertWithRate(item.get('likely_case'), item.get('base_rate')), likely_adjusted: app.currency.convertWithRate(item.get('likely_case_adjusted'), item.get('base_rate')), quota: app.currency.convertWithRate(item.get('quota'), item.get('base_rate')) }; if (!_.isUndefined(this.dashletConfig.dataset.options['best'])) { i.best = app.currency.convertWithRate(item.get('best_case'), item.get('base_rate')); i.best_adjusted = app.currency.convertWithRate(item.get('best_case_adjusted'), item.get('base_rate')); } if (!_.isUndefined(this.dashletConfig.dataset.options['worst'])) { i.worst = app.currency.convertWithRate(item.get('worst_case'), item.get('base_rate')); i.worst_adjusted = app.currency.convertWithRate(item.get('worst_case_adjusted'), item.get('base_rate')); } return i; }, this); serverData.quota = _.reduce(serverData.data, function(memo, item) { return app.math.add(memo, item.quota, undefined, true); }, 0); field.setServerData(serverData); } }, /** * Handler for when the Rep Worksheet Changes * @param {Object} model */ repWorksheetChanged: function(model) { // get what we are currently filtered by // find the item in the serverData var changed = model.changed, changedField = _.keys(changed), field = this.getField('paretoChart'), serverData = field.getServerData(); // if the changedField is date_closed, we need to adjust the timestamp as well since SugarLogic doesn't work // on list views yet if (changedField.length == 1 && changedField[0] == 'date_closed') { // convert this into the timestamp changedField.push('date_closed_timestamp'); changed.date_closed_timestamp = Math.round(+app.date.parse(changed.date_closed).getTime() / 1000); model.set('date_closed_timestamp', changed.date_closed_timestamp, {silent: true}); } if (_.contains(changedField, 'likely_case')) { changed.likely = app.currency.convertWithRate(changed.likely_case, model.get('base_rate')); delete changed.likely_case; } if (_.contains(changedField, 'best_case')) { changed.best = app.currency.convertWithRate(changed.best_case, model.get('base_rate')); delete changed.best_case; } if (_.contains(changedField, 'worst_case')) { changed.worst = app.currency.convertWithRate(changed.worst_case, model.get('base_rate')); delete changed.worst_case; } if (_.contains(changedField, 'commit_stage')) { changed.forecast = changed.commit_stage; delete changed.commit_stage; } _.find(serverData.data, function(record, i, list) { if (model.get('id') == record.id) { list[i] = _.extend({}, record, changed); return true; } return false; }); field.setServerData(serverData, _.contains(changedField, 'probability')); }, /** * Handler for when the Manager Worksheet Changes * @param {Object} model */ mgrWorksheetChanged: function(model) { var fieldsChanged = _.keys(model.changed), changed = model.changed, field = this.getField('paretoChart'); if(field && field.hasServerData()) { var serverData = field.getServerData(); if (_.contains(fieldsChanged, 'quota')) { var q = parseInt(serverData.quota, 10); q = app.math.add(app.math.sub(q, model.previous('quota')), model.get('quota')); serverData.quota = q; } else { var f = _.first(fieldsChanged), fieldChartName = f.replace('_case', ''); // find the user _.find(serverData.data, function(record, i, list) { if (model.get('user_id') == record.user_id) { list[i][fieldChartName] = changed[f]; return true; } return false; }); } field.setServerData(serverData); } }, /** * When loadData is called, find the paretoChart field, if it exist, then have it render the chart * * @inheritdoc */ loadData: function(options) { var field = this.getField('paretoChart'); if (!_.isUndefined(field)) { field.once('chart:pareto:rendered', this.parseCollectionForData, this); field.renderChart(options); } if (options && _.isFunction(options.complete)) { options.complete(); } }, /** * Called after _render */ toggleRepOptionsVisibility: function() { this.$('div.groupByOptions').toggleClass('hide', this.values.get('display_manager') === true); }, /** * @inheritdoc */ bindDataChange: function() { // on the off chance that the init has not run yet. var meta = this.meta || this.initOptions.meta; if (meta.config) { return; } this.values.on('change:title', function(model, title) { this.layout.setTitle(app.lang.get(this.meta.label) + title); }, this); this.on('render', function() { var field = this.getField('paretoChart'), dashToolbar = this.layout.getComponent('dashlet-toolbar'); // if we have a dashlet-toolbar, then make it do the refresh icon while the chart is loading from the // server if (dashToolbar) { field.before('chart:pareto:render', function() { this.$("[data-action=loading]").removeClass(this.cssIconDefault).addClass(this.cssIconRefresh); }, dashToolbar); field.on('chart:pareto:rendered', function() { this.$("[data-action=loading]").removeClass(this.cssIconRefresh).addClass(this.cssIconDefault); }, dashToolbar); } this.toggleRepOptionsVisibility(); this.parseCollectionForData(); }, this); var ctx = this.context.parent; ctx.on('change:selectedUser', function(context, user) { var displayMgr = ctx.get('model').get('forecastType') == 'Rollup', showTargetQuota = (displayMgr && !user.is_top_level_manager); this.values.set({ user_id: user.id, display_manager: displayMgr, show_target_quota: showTargetQuota }); this.toggleRepOptionsVisibility(); }, this); ctx.on('change:selectedTimePeriod', function(context, timePeriod) { this.values.set({timeperiod_id: timePeriod}); }, this); ctx.on('change:selectedRanges', function(context, value) { this.values.set({ranges: value}); }, this); }, /** * @inheritdoc */ unbindData: function() { var ctx = this.context.parent; if (ctx) { ctx.off(null, null, this); } if (this.forecastManagerWorksheetContext && this.forecastManagerWorksheetContext.get('collection')) { this.forecastManagerWorksheetContext.get('collection').off(null, null, this); } if (this.forecastWorksheetContext && this.forecastWorksheetContext.get('collection')) { this.forecastWorksheetContext.get('collection').off(null, null, this); } if (this.context) { this.context.off(null, null, this); } if (this.values) { this.values.off(null, null, this); } this._super('unbindData'); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Forecasts.PreviewView * @alias SUGAR.App.view.views.BaseForecastsPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * Track the original model passed in from the worksheet, this is needed becuase of how the base preview works */ originalModel: undefined, _delegateEvents: function() { app.events.on('preview:render', this._renderPreview, this); app.events.on('preview:close', this.closePreview, this); this._super('_delegateEvents'); }, /** * @inheritdoc */ closePreview: function() { if (!_.isUndefined(this.originalModel)) { this.originalModel = undefined; this._super('closePreview'); } }, /** * Override _renderPreview to pull in the _module and id when we are running a fetch * * @param model * @param collection * @param fetch * @param previewId * @param dontClose overrides triggering preview:close * @private */ _renderPreview: function(model, collection, fetch, previewId, dontClose){ var self = this; dontClose = dontClose || false; // If there are drawers there could be multiple previews, make sure we are only rendering preview for active drawer if(app.drawer && !app.drawer.isActive(this.$el)){ return; //This preview isn't on the active layout } // Close preview if we are already displaying this model if(!dontClose && this.originalModel && model && (this.originalModel.get("id") == model.get("id") && previewId == this.previewId)) { // Remove the decoration of the highlighted row app.events.trigger("list:preview:decorate", false); // Close the preview panel app.events.trigger('preview:close'); return; } if (model) { // Get the corresponding detail view meta for said module. // this.meta needs to be set before this.getFieldNames is executed. this.meta = app.metadata.getView(model.get('parent_type') || model.get('_module'), 'record') || {}; this.meta = this._previewifyMetadata(this.meta); } if (fetch) { let mdl = app.data.createBean(model.get('_module'), {'id': model.get('id')}); this.originalModel = model; mdl.fetch({ //Show alerts for this request showAlerts: true, success: function(model) { self.renderPreview(model, collection); } }); } else { this.renderPreview(model, collection); } this.previewId = previewId; }, /** * Show previous and next buttons groups on the view. * * This gets called everytime the collection gets updated. It also depends * if we have a current model or layout. * * TODO we should check if we have the preview open instead of doing a bunch * of if statements. */ showPreviousNextBtnGroup: function () { if (!this.model || !this.layout || !this.collection) { return; } var collection = this.collection; if (!collection.size()) { this.layout.hideNextPrevious = true; } // use the originalModel if one is defined, if not fall back to the basic model var model = this.originalModel || this.model; var recordIndex = collection.indexOf(collection.get(model.id)); this.layout.previous = collection.models[recordIndex-1] ? collection.models[recordIndex-1] : undefined; this.layout.next = collection.models[recordIndex+1] ? collection.models[recordIndex+1] : undefined; this.layout.hideNextPrevious = _.isUndefined(this.layout.previous) && _.isUndefined(this.layout.next); // Need to rerender the preview header this.layout.trigger("preview:pagination:update"); }, /** * Renders the preview dialog with the data from the current model and collection * @param model Model for the object to preview * @param newCollection Collection of related objects to the current model */ renderPreview: function(model, newCollection) { if(newCollection) { this.collection.reset(newCollection.models); } if (model) { this.model = app.data.createBean(model.module, model.toJSON()); this.render(); // TODO: Remove when pagination on activity streams is fixed. if (this.previewModule && this.previewModule === "Activities") { this.layout.hideNextPrevious = true; this.layout.trigger("preview:pagination:update"); } // Open the preview panel app.events.trigger("preview:open",this); // Highlight the row // use the original model when going to the list:preview:decorate event app.events.trigger("list:preview:decorate", this.originalModel, this); } }, /** * Switches preview to left/right model in collection. * @param {String} data direction Direction that we are switching to, either 'left' or 'right'. * @param index Optional current index in list * @param id Optional * @param module Optional */ switchPreview: function(data, index, id, module) { var self = this, currModule = module || this.model.module, currID = id || this.model.get("postId") || this.model.get("id"), // use the originalModel vs the model currIndex = index || _.indexOf(this.collection.models, this.collection.get(this.originalModel.get('id'))); if( this.switching || this.collection.models.length < 2) { // We're currently switching previews or we don't have enough models, so ignore any pagination click events. return; } this.switching = true; // get the id from the specific module if (data.direction === 'left' && (currID === _.first(this.collection.models).get('id')) || data.direction === 'right' && (currID === _.last(this.collection.models).get('id'))) { this.switching = false; return; } else { // We can increment/decrement data.direction === "left" ? currIndex -= 1 : currIndex += 1; // If there is no target_id, we don't have access to that activity record // The other condition ensures we're previewing from activity stream items. if( _.isUndefined(this.collection.models[currIndex].get("target_id")) && this.collection.models[currIndex].get("activity_data") ) { currID = this.collection.models[currIndex].id; this.switching = false; this.switchPreview(data, currIndex, currID, currModule); } else { var targetModule = this.collection.models[currIndex].get("target_module") || currModule; this.model = app.data.createBean(targetModule); if( _.isUndefined(this.collection.models[currIndex].get("target_id")) ) { // get the id this.model.set('id', this.collection.models[currIndex].get('id')); } else { this.model.set("postId", this.collection.models[currIndex].get("id")); this.model.set("id", this.collection.models[currIndex].get("target_id")); } this.originalModel = this.collection.models[currIndex]; this.model.fetch({ //Show alerts for this request showAlerts: true, success: function(model) { model.set("_module", targetModule); self.model = null; //Reset the preview app.events.trigger('preview:render', model, null, false, currID, true); self.switching = false; } }); } } } }) }, "commitment-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsCommitmentHeaderpaneView * @alias SUGAR.App.view.layouts.BaseForecastsCommitmentHeaderpaneView * @extends View.Views.Base.ListHeaderpaneView */ ({ // Commitment-headerpane View (base) extendsFrom: 'HeaderpaneView', plugins: ['FieldErrorCollection'], /** * If the Save button should be hidden or not * @type Boolean */ saveBtnDisabled: true, /** * If the Commit button should be disabled or not * @type Boolean */ commitBtnDisabled: true, /** * Flag for if the Cancel button should be hidden or not * @type Boolean */ cancelBtnHidden: true, /** * If any fields in the view have errors or not * @type Boolean */ fieldHasErrorState: false, /** * The Save Draft Button Field * @type View.Fields.Base.ButtonField */ saveDraftBtnField: null, /** * The Commit Button Field * @type View.Fields.Base.ButtonField */ commitBtnField: null, /** * Cancel button * @type View.Fields.Base.ButtonField */ cancelBtnField: null, /** * If Forecasts' data sync is complete and we can render buttons * @type Boolean */ forecastSyncComplete: false, /** * Commit button tooltip labels * @type Object */ commitBtnTooltips: {}, /** * Save button labels * @type Object */ saveBtnLabels: {}, /** * Holds the prefix string that is rendered before the same of the user * @type String */ forecastWorksheetLabel: '', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.commitBtnTooltips = { 'Rollup': app.lang.get('LBL_COMMIT_TOOLTIP_MGR', this.module), 'Direct': app.lang.get('LBL_COMMIT_TOOLTIP_REP', this.module), }; let moduleName = app.metadata.getModule('Opportunities', 'config').opps_view_by; let translatedModule = app.lang.get('LBL_MODULE_NAME', moduleName); this.saveBtnLabels = { 'Rollup': app.lang.get('LBL_SAVE_LABEL_MGR', this.module), 'Direct': `${app.lang.get('LBL_SAVE_LABEL_REP', this.module)}${translatedModule}`, }; }, /** * @inheritdoc */ bindDataChange: function() { this.listenTo(this.layout.context, 'forecasts:sync:start', function() { this.forecastSyncComplete = false; this.setButtonStates(); }); this.listenTo(this.layout.context, 'forecasts:sync:complete', function() { this.forecastSyncComplete = true; this.setButtonStates(); }); this.listenTo(this.context, 'change:selectedUser', function(model, changed) { this._getWorksheetType(); if (!this.disposed) { this.render(); } }); this.on('render', function() { // switching from mgr to rep leaves $el null, so make sure we grab a fresh reference // to the field if it's there but $el is null in the current reference if (!this.commitBtnField || (this.commitBtnField && _.isNull(this.commitBtnField.$el))) { // get reference to the Commit button Field this.commitBtnField = this.getField('commit_button'); } this.saveDraftBtnField = this.getField('save_draft_button'); this.cancelBtnField = this.getField('cancel_button'); this.saveDraftBtnField.hide(); this.commitBtnField.setDisabled(); this.cancelBtnField.hide(); }, this); this.listenTo(this.context, 'plugin:fieldErrorCollection:hasFieldErrors', function(collection, hasErrors) { if (this.fieldHasErrorState !== hasErrors) { this.fieldHasErrorState = hasErrors; this.setButtonStates(); } }); this.listenTo(this.context, 'forecasts:worksheet:is_dirty', (worksheetType, isDirty) => { isDirty = !isDirty; if (this.saveBtnDisabled !== isDirty || this.commitBtnDisabled !== isDirty || this.cancelBtnHidden !== isDirty ) { this.saveBtnDisabled = isDirty; this.commitBtnDisabled = isDirty && this.context.get('lastCommitModel') instanceof Backbone.Model; this.cancelBtnHidden = isDirty; this.setButtonStates(); } }); let allBtnEvents = 'button:commit_button:click button:save_draft_button:click button:cancel_button:click'; this.listenTo(this.context, allBtnEvents, () => { if (!this.saveBtnDisabled || !this.commitBtnDisabled || !this.cancelBtnHidden) { this.saveBtnDisabled = true; this.commitBtnDisabled = this.context.get('lastCommitModel') instanceof Backbone.Model; this.cancelBtnHidden = true; this.setButtonStates(); } }); this.listenTo(this.context, 'forecasts:worksheet:saved', function(totalSaved, worksheetType, wasDraft) { if (wasDraft === true && this.commitBtnDisabled) { this.commitBtnDisabled = false; this.setButtonStates(); } }, this); this.listenTo(this.context, 'forecasts:worksheet:needs_commit', function(worksheetType) { if (this.commitBtnDisabled) { this.commitBtnDisabled = false; this.setButtonStates(); } }, this); // When a forecast datapoint value is changed, we want to enable/show // the cancel and commit buttons, but not the save draft button. this.listenTo(this.context, 'forecasts:datapoint:changed', function() { if (this.cancelBtnHidden || this.commitBtnDisabled) { this.cancelBtnHidden = false; this.commitBtnDisabled = false; this.setButtonStates(); } }); this._super('bindDataChange'); }, /** * Sets the appropriate button states */ setButtonStates: function() { // make sure all data sync has finished before updating button states if (this.forecastSyncComplete) { // fieldHasErrorState trumps the disabled flags, but when it's cleared // revert back to whatever states the buttons were in if (this.fieldHasErrorState) { this.cancelBtnField.hide(); this.saveDraftBtnField.hide(); this.commitBtnField.setDisabled(true); this.commitBtnField.$('.commit-button').tooltip(); } else { this.commitBtnField.setDisabled(this.commitBtnDisabled); if (this.cancelBtnHidden) { this.cancelBtnField.hide(); } else { this.cancelBtnField.show(); } if (this.saveBtnDisabled) { this.saveDraftBtnField.hide(); } else { this.saveDraftBtnField.show(); } if (!this.commitBtnDisabled) { this.commitBtnField.$('.commit-button').tooltip('dispose'); } else { this.commitBtnField.$('.commit-button').tooltip(); } } } else { // disable buttons while syncing if (this.saveDraftBtnField) { this.saveDraftBtnField.hide(); } if (this.commitBtnField) { this.commitBtnField.setDisabled(true); } if (this.cancelBtnField) { this.cancelBtnField.hide(); } } let worksheetType = this._getWorksheetType(); if (worksheetType) { this.$('.commit-button').attr('data-original-title', this.commitBtnTooltips[worksheetType]); this.$('.save-draft-button').text(this.saveBtnLabels[worksheetType]); } }, /** * Gets the current worksheet type * @return {string} Either "Rollup" or "Direct". Returns empty string if current user could not be found * @private */ _getWorksheetType: function() { let selectedUser = this.context.get('selectedUser'); if (!selectedUser) { return ''; } return app.utils.getForecastType(selectedUser.is_manager, selectedUser.showOpps); }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); this._super('_dispose'); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "filterpanel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Forecasts.FilterpanelLayout * @alias SUGAR.App.view.layouts.BaseForecastsFilterpanelLayout * @extends View.Layouts.Base.FilterpanelLayout */ ({ // Filterpanel Layout (base) extendsFrom: 'FilterpanelLayout', /** * Add forecasts:refreshlist event when refresh button was clicked * * @private */ _refreshList: function() { this._super('_refreshList'); this.context.trigger('forecasts:refreshList'); }, }) }, "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ForecastsConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseForecastsConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'ConfigDrawerLayout', /** * @inheritdoc * * Checks Forecasts ACLs to see if the User is a system admin * or if the user has a developer role for the Forecasts module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().Forecasts, isSysAdmin = (app.user.get('type') == 'admin'), isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isDev); }, /** * Checks Forecasts config metadata to see if the correct Sales Stage Won/Lost settings are present * * @inheritdoc */ _checkModuleConfig: function() { return app.utils.checkForecastConfig(); } }) }, "tabbed-layout": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Forecasts.TabbedLayoutLayout * @alias SUGAR.App.view.layouts.BaseForecastsTabbedLayoutLayout * @extends View.Layouts.Base.TabbedLayoutLayout */ ({ // Tabbed-layout Layout (base) /** * @inheritdoc */ activeTab: 0, /** * Key name for the last state */ lastStateKey: 'Forecasts:last-visited-tab', /** * Selector of the a container */ parentContainer: '.forecasts-main', /** * Show if Opp tab was visited */ oppFirstVisit: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initTabs(); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this._resizeDebounce = _.debounce(_.bind(this._resize, this), 50); $(window).on('resize.' + this.cid, this._resizeDebounce); this.listenTo(app.events, 'metric:data:ready', this._resizeDebounce); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('li[data-bs-toggle="tab"]').on('shown.bs.tab', _.bind(this.tabClickHandle, this)); // Observe resizing on the headerpane if (this.resizeOb) { this.resizeOb.disconnect(); } let lhPane = $('.listheaderpane').get(0); if (lhPane instanceof Element) { this.resizeOb = new ResizeObserver(_.bind(this.reHeightParentContainer, this)); this.resizeOb.observe(lhPane); } }, /** * Change css propperties of different Forecasts elements on resize event */ _resize: function() { if (!this.$el) { return; } const windowWidth = $(window).width(); const main = this.$el.closest(this.parentContainer); const headerpane = main.find('.headerpane'); const flexList = main.find('.paginated-flex-list'); const metrics = main.find('.forecast-metrics'); const metricsHeight = metrics.outerHeight(); flexList.css({ height: `calc(100% - ${metricsHeight}px - 2rem)`, }); headerpane.css({ width: (windowWidth <= 768) ? `${main.width()}px` : '', }); }, /** * Change css propperties of parent container element */ reHeightParentContainer: function() { if (!this.$el) { return; } const main = this.$el.closest(this.parentContainer); const headerpane = main.find('.headerpane'); const headerpaneHeight = headerpane.outerHeight(); main.css({ top: headerpaneHeight, height: `calc(100% - ${headerpaneHeight}px)`, }); }, /** * Handler for the tab clicking * * @param {Event} e */ tabClickHandle: function(e) { let index; let parentElement = $(e.currentTarget).parent('li'); if ($(e.currentTarget).has('data-tab-index')) { index = $(e.currentTarget).attr('data-tab-index'); } else if (parentElement && parentElement.has('data-tab-index')) { index = parentElement.attr('data-tab-index'); } if (index && this.lastStateKey) { this.activeTab = parseInt(index); app.user.lastState.set(this.lastStateKey, this.activeTab); } if (this.activeTab === 1) { if (this.oppFirstVisit) { this.oppFirstVisit = false; //re-render to fix columns size this.getComponent('filterpanel') .getComponent('list') .render(); } window.dispatchEvent(new Event('resize')); } }, /** * Initialize tabs */ _initTabs: function() { const lastVisitTab = app.user.lastState.get(this.lastStateKey); if (!_.isUndefined(lastVisitTab)) { this.activeTab = parseInt(lastVisitTab); if (this.activeTab !== 1) { this.oppFirstVisit = true; } } }, /** * @inheritdoc */ _dispose: function() { $(window).off('resize.' + this.cid); this.stopListening(); this._super('_dispose'); }, }) }, "filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Forecasts.FilterLayout * @alias SUGAR.App.view.layouts.BaseForecastsFilterLayout * @extends View.Layouts.Base.FilterLayout */ ({ // Filter Layout (base) extendsFrom: 'BaseFilterLayout', /** * A list of activeMetrics * * @property {Array} */ activeMetrics: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.activeMetrics = []; }, /** * @inheritdoc */ bindDataChange: function() { this.listenTo( this.layout.context, 'filter:selectedTimePeriod:changed filter:selectedUser:changed', this._retriggerFilter ); this.listenTo(this.layout, 'forecast:metric:active', this._handleActiveMetricsChange); }, /** * Sets the activeMetrics class property when the metrics are changed * @param array activeMetrics list of active metrics * @private */ _handleActiveMetricsChange: function(activeMetrics) { this.activeMetrics = activeMetrics; this._retriggerFilter(); }, /** * Gets the query string from the quicksearch bar and triggers the filter:apply event to reapply the filters * @private */ _retriggerFilter: function() { let query = this.$('input.search-name').val() || ''; this.trigger('filter:apply', query, undefined, {showAlerts: false}); }, /** * @inheritdoc * * Passes in Forecast-specific data to the filter API */ _getCollectionParams() { let forecastContext = this.layout.context; let selectedUser = forecastContext.get('selectedUser') || {}; return { user_id: selectedUser.id || '', type: forecastContext.get('forecastType') || '', time_period: forecastContext.get('selectedTimePeriod') || '', metrics: this.activeMetrics }; }, /** * @inheritdoc */ _dispose: function() { this.stopListening(); this._super('_dispose'); } }) }, "records": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Forecasts Records Layout * * @class View.Layouts.Base.Forecasts.RecordsLayout * @alias SUGAR.App.view.layouts.BaseForecastsRecordsLayout * @extends View.Layouts.Base.RecordsLayout * * Events * * forecasts:worksheet:committed * on: this.context * by: commitForecast * when: after a successful Forecast Commit */ ({ // Records Layout (base) /** * bool to store if a child worksheet is dirty */ isDirty: false, /** * worksheet type */ worksheetType: '', /** * the forecast navigation message */ navigationMessage: "", /** * The options from the initialize call */ initOptions: undefined, /** * Are the event already bound to the context and the models? */ eventsBound: false, /** * Overrides the Layout.initialize function and does not call the parent so we can defer initialization * until _onceInitSelectedUser is called * * @inheritdoc */ initialize: function(options) { this.initOptions = options; this._super('initialize', [options]); this.syncInitData(); this.context.set('nextCommitModel', app.data.createBean('Forecasts')); }, /** * @inheritdoc */ initComponents: function() { }, /** * Overrides loadData to defer it running until we call it in _onceInitSelectedUser * * @inheritdoc */ loadData: function() { }, /** * @inheritdoc */ bindDataChange: function() { // we need this here to track when the selectedTimeperiod changes and then also move it up to the context // so the recordlists can listen for it. if (!_.isUndefined(this.model) && this.eventsBound == false) { this.eventsBound = true; this.collection.on('reset', function() { // get the first model and set the last commit date var lastCommit = _.first(this.collection.models); var commitDate = undefined; if (lastCommit instanceof Backbone.Model && lastCommit.has('date_modified')) { commitDate = lastCommit.get('date_modified'); } this.context.set({'currentForecastCommitDate': commitDate}); this._setCommitModelsOnContext(); }, this); // since the selected user change on the context, update the model this.context.on('change:selectedUser', function(model, changed) { var update = { 'selectedUserId': changed.id, 'forecastType': app.utils.getForecastType(changed.is_manager, changed.showOpps) }; this.model.set(update); this.context.trigger('filter:selectedUser:changed'); }, this); // if the model changes, run a fetch this.model.on('change', function() { // clear this out as something on the model changed, // this will be set once the collection resets // set the value to null since it can be undefined this.context.set({'currentForecastCommitDate' : null}, {silent: true}); this.collection.fetch(); }, this); this.context.on('change:selectedTimePeriod', function() { // clear this out if the timeperiod changed on the context, // this will be set once the collection resets // set the value to null since it can be undefined this.context.set({'currentForecastCommitDate' : null}, {silent: true}); this.collection.fetch(); }, this); // listen on the context for a commit trigger this.context.on('forecasts:worksheet:commit', function(user, worksheet_type, forecast_totals) { this.commitForecast(user, worksheet_type, forecast_totals); }, this); //listen for the worksheets to be dirty/clean this.context.on("forecasts:worksheet:dirty", function(type, isDirty) { this.isDirty = isDirty; this.worksheetType = type; }, this); //listen for the worksheet navigation messages this.context.on("forecasts:worksheet:navigationMessage", function(message) { this.navigationMessage = message; }, this); //listen for the user to change this.context.on("forecasts:user:changed", function(selectedUser, context) { if (this.isDirty) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get(this.navigationMessage, 'Forecasts').split('<br>'), onConfirm: _.bind(function() { app.utils.getSelectedUsersReportees(selectedUser, context); }, this), onCancel: _.bind(function() { this.context.trigger('forecasts:user:canceled'); }, this) }); } else { app.utils.getSelectedUsersReportees(selectedUser, context); } }, this); //handle timeperiod change events this.context.on('forecasts:timeperiod:changed', function(model, startEndDates) { // create an anonymous function to combine the two calls where this is used var onSuccess = _.bind(function() { this.context.set('selectedTimePeriod', model.get('selectedTimePeriod')); app.user.lastState.set('Forecasts:time-period', model.get('selectedTimePeriod')); this._saveTimePeriodStatEndDates(startEndDates['start'], startEndDates['end']); this.context.trigger('filter:selectedTimePeriod:changed'); }, this); if (this.isDirty) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get(this.navigationMessage, 'Forecasts').split('<br>'), onConfirm: onSuccess, onCancel: _.bind(function() { this.context.trigger('forecasts:timeperiod:canceled'); }, this) }); } else { // call the on success handler onSuccess(); } }, this); } }, /** * When the previous commits are loaded, sets the last commit model on the * context. Also creates a fresh model to store data for the next commit * * @private */ _setCommitModelsOnContext: function() { this.context.set('lastCommitModel', _.first(this.collection.models) || null); // The worksheet is reloading, so clear the attributes on the model let nextCommitModel = this.context.get('nextCommitModel'); nextCommitModel.setSyncedAttributes({}); nextCommitModel.clear(); this.context.trigger('forecasts:commit-models:loaded'); }, /** * Utility Method to handle saving of the timeperiod start and end dates so we can use them in other parts * of the forecast application * * @param {String} startDate Start Date * @param {String} endDate End Date * @param {Boolean} [doSilent] When saving to the context, should this be silent to supress events * @return {Object} The object that is saved to the context if the context is there. * @private */ _saveTimePeriodStatEndDates: function(startDate, endDate, doSilent) { // if do silent is not passed in or it's not a boolean, then just default it to false, so the events will fire if (_.isUndefined(doSilent) || !_.isBoolean(doSilent)) { doSilent = false; } var userPref = app.date.convertFormat(app.user.getPreference('datepref')), systemPref = 'YYYY-MM-DD', dateObj = { start: app.date(startDate, [userPref, systemPref]).format(systemPref), end: app.date(endDate, [userPref, systemPref]).format(systemPref) }; if (!_.isUndefined(this.context)) { this.context.set( 'selectedTimePeriodStartEnd', dateObj, {silent: doSilent} ); } return dateObj; }, /** * Opens the Forecasts Config drawer */ openConfigDrawer: function() { // if there is no drawer open, then we need to open the drawer. if(app.drawer._components.length == 0) { // trigger the forecast config by going to the config route, while replacing what // is currently there so when we use app.route.goBack() from the cancel button app.router.navigate('Forecasts/config', {replace: true, trigger: true}); } }, /** * Get the Forecast Init Data from the server * * @param {Object} options */ syncInitData: function(options) { var callbacks, url; options = options || {}; // custom success handler options.success = _.bind(function(data) { // Add Forecasts-specific stuff to the app.user object app.user.set(data.initData.userData); if (data.initData.forecasts_setup === 0) { // Immediately open the config drawer so user can set up config this.openConfigDrawer(); } else { this.initForecastsModule(data); } }, this); // since we have not initialized the view yet, pull the model from the initOptions.context var model = this.initOptions.context.get('model'); callbacks = app.data.getSyncCallbacks('read', model, options); this.trigger("data:sync:start", 'read', model, options); url = app.api.buildURL("Forecasts/init", null, null, options.params); var params = {}, cfg = app.metadata.getModule('Forecasts', 'config'); if (cfg && cfg.is_setup === 0) { // add no-cache header if forecasts isnt set up yet params = { headers: { 'Cache-Control': 'no-cache' } }; } app.api.call("read", url, null, callbacks, params); }, /** * Process the Forecast Data * * @param {Object} data contains the data passed back from Forecasts/init endpoint */ initForecastsModule: function(data) { var ctx = this.initOptions.context; // we watch for the first selectedUser change to actually init the Forecast Module case then we know we have // a proper selected user ctx.once('change:selectedUser', this._onceInitSelectedUser, this); // lets see if the user has ranges selected, so lets generate the key from the filters var ranges_key = app.user.lastState.buildKey('worksheet-filter', 'filter', 'ForecastWorksheets'), default_selection = app.user.lastState.get(ranges_key) || data.defaultSelections.ranges; let lastTimePeriod = app.user.lastState.get('Forecasts:time-period'); let defaultTimePeriod = data.defaultSelections.timeperiod_id.id; let timeperiodId = lastTimePeriod || defaultTimePeriod; // if the time period from local storage is not in the options, set it to the default if (!_.has(data.timePeriodOptions,timeperiodId)) { timeperiodId = defaultTimePeriod; app.user.lastState.set('Forecasts:time-period', timeperiodId); } // set items on the context from the initData payload ctx.set({ // set the value to null since it can be undefined currentForecastCommitDate: null, selectedTimePeriod: timeperiodId, selectedRanges: default_selection, selectedTimePeriodStartEnd: this._saveTimePeriodStatEndDates( data.defaultSelections.timeperiod_id.start, data.defaultSelections.timeperiod_id.end, true ), _isInvalidModel: _.bind(this._isInvalidModel, this) }, {silent: true}); const selectedUser = app.user.lastState.get('Forecasts:selected-user') || app.user.toJSON(); selectedUser.reportees = []; ctx.get('model').set({'selectedTimePeriod': timeperiodId}, {silent: true}); // set the selected user to the context app.utils.getSelectedUsersReportees(selectedUser, ctx); }, /** * Check if the model is in the selected range (included/excluded) * @param model * @return {boolean} * @private */ _isInvalidModel: function(model) { let range = this.context.get('selectedRanges'); if (_.isArray(range) && range.length > 0) { return !range.includes(model.get('commit_stage')); } return false; }, /** * Event handler for change:selectedUser * Triggered once when the user is set for the first time. After setting user it calls * the init of the records layout * * @param {Backbone.Model} model the model from the change event * @param {String} change the updated selectedUser value from the change event * @private */ _onceInitSelectedUser: function(model, change) { // init the recordlist view app.view.Layout.prototype.initialize.call(this, this.initOptions); app.view.Layout.prototype.initComponents.call(this); // set the selected user and forecast type on the model this.model.set('selectedUserId', change.id, {silent: true}); this.model.set('forecastType', app.utils.getForecastType(change.is_manager, change.showOpps)); // bind the collection sync to our custom sync this.collection.sync = _.bind(this.sync, this); // load the data app.view.Layout.prototype.loadData.call(this); if (this.eventsBound === false) { // bind the data change this.bindDataChange(); } // render everything if (!this.disposed) this.render(); }, /** * Custom sync method used by this.collection * * @param {String} method * @param {Backbone.Model} model * @param {Object} options */ sync: function(method, model, options) { var callbacks, url; options = options || {}; options.params = options.params || {}; var args_filter = [], filter = null; if (this.context.has('selectedTimePeriod')) { args_filter.push({"timeperiod_id": this.context.get('selectedTimePeriod')}); } if (this.model.has('selectedUserId')) { args_filter.push({"user_id": this.model.get('selectedUserId')}); args_filter.push({"forecast_type": this.model.get('forecastType')}); } if (!_.isEmpty(args_filter)) { filter = {"filter": args_filter}; } options.params.order_by = 'date_entered:DESC'; options.fields = _.union(options.fields || [], ['likely_case', 'best_case', 'worst_case']); options = app.data.parseOptionsForSync(method, model, options); // custom success handler options.success = _.bind(function(data) { if (!this.disposed) { this.collection.reset(data); } }, this); callbacks = app.data.getSyncCallbacks(method, model, options); // if there's a 412 error dismiss the custom loading alert this.collection.once('data:sync:error', function() { app.alert.dismiss('worksheet_loading'); }, this); this.collection.trigger("data:sync:start", method, model, options); url = app.api.buildURL("Forecasts/filter", null, null, options.params); app.api.call("create", url, filter, callbacks); }, /** * Commit A Forecast * * @fires forecasts:worksheet:committed * @param {Object} user * @param {String} worksheet_type * @param {Object} forecast_totals */ commitForecast: function(user, worksheet_type, forecast_totals) { var forecast = new this.collection.model(), forecastType = app.utils.getForecastType(user.is_manager, user.showOpps), forecastData = {}; // we need a commit_type so we know what to do on the back end. forecastData.commit_type = worksheet_type; forecastData.timeperiod_id = forecast_totals.timeperiod_id || this.context.get('selectedTimePeriod'); forecastData.forecast_type = forecastType; // For Forecast-level editable fields, include their values in the data let forecastFields = ['likely_case', 'best_case', 'worst_case']; let nextCommitModel = this.context.get('nextCommitModel'); if (nextCommitModel instanceof Backbone.Model) { _.each(forecastFields, function(forecastField) { let forecastValue = parseFloat(nextCommitModel.get(forecastField)); if (!_.isNaN(forecastValue)) { forecastData[forecastField] = forecastValue; } }, this); } app.alert.show('commit_alert', { level: 'process', title: app.lang.get('LBL_SAVING'), autoClose: false }); forecast.save(forecastData, { success: _.bind(function(model, response) { app.alert.dismiss('commit_alert'); // we need to make sure we are not disposed, this handles any errors that could come from the router and window // alert events if (!this.disposed) { // Call sync again so commitLog has the full collection // method gets overridden and options just needs an this.collection.fetch(); this.context.trigger("forecasts:worksheet:committed", worksheet_type, response); var msg, managerName; if (worksheet_type === 'sales_rep') { if (user.is_manager) { // as manager, use own name managerName = user.full_name; } else { // as sales rep, use manager name managerName = user.reports_to_name; } } else { if (user.reports_to_id) { // if manager has a manager, use reports to name managerName = user.reports_to_name; } } if (managerName) { msg = Handlebars.compile(app.lang.get('LBL_FORECASTS_WORKSHEET_COMMIT_SUCCESS_TO', 'Forecasts'))( { manager: managerName } ); } else { // user does not report to anyone, don't use any name msg = Handlebars.compile(app.lang.get('LBL_FORECASTS_WORKSHEET_COMMIT_SUCCESS', 'Forecasts'))(); } app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get('LBL_FORECASTS_WIZARD_SUCCESS_TITLE', 'Forecasts') + ':', messages: [msg] }); } }, this), error: _.bind(function(model, error) { //if the metadata error comes back, we saved successfully, so we need to clear the is_dirty flag so the //page can reload if (error.status === 412) { this.context.trigger('forecasts:worksheet:is_dirty', worksheet_type, false); } }, this), silent: true, alerts: { 'success': false }}); } }) }, "preview-activitystream": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.PreviewActivityStreamLayout * @alias SUGAR.App.view.layouts.BasePreviewActivityStreamLayout * @extends View.Layouts.Base.ActivitystreamLayout */ ({ // Preview-activitystream Layout (base) extendsFrom: 'ActivitystreamLayout', _previewOpened: false, //is the preview pane open? /** * Fetch and render activities when 'preview:render' event has been fired. */ initialize: function(options) { this._super('initialize', [options]); app.events.on('preview:render', this.fetchActivities, this); app.events.on('preview:open', function() { this._previewOpened = true; }, this); app.events.on('preview:close', function() { this._previewOpened = false; this.disposeAllActivities(); }, this); }, /** * Fetch and render activities. * * @param model * @param collection * @param fetch * @param previewId * @param {boolean} showActivities */ fetchActivities: function(model, collection, fetch, previewId, showActivities) { if (app.metadata.getModule(model.module).isBwcEnabled) { // don't fetch activities for BWC modules return; } this.disposeAllActivities(); this.collection.dataFetched = false; this.$el.hide(); showActivities = _.isUndefined(showActivities) ? true : showActivities; if (showActivities) { this.collection.reset(); this.collection.resetPagination(); this.collection.setOption('endpoint', function(method, collection, options, callbacks) { var url = app.api.buildURL( model.module, null, {id: model.get('id'), link: 'activities'}, options.params ); return app.api.call('read', url, null, callbacks); }); this.collection.fetch({ /* * Render activity stream */ success: _.bind(this.renderActivities, this) }); } }, /** * Render activity stream once the preview pane opens. Hide it when there are no activities. * @param collection */ renderActivities: function(collection) { var self = this; if (this.disposed) { return; } if (this._previewOpened) { if (collection.length === 0) { this.$el.hide(); } else { this.$el.show(); collection.each(function(activity) { self.renderPost(activity, true); }); } } else { //FIXME: MAR-2798 prevent the possibility of an infinite loop _.delay(function() { self.renderActivities(collection); }, 500); } }, /** * No need to set collectionOptions. */ setCollectionOptions: function() {}, /** * No need to expose data transfer object since this activity stream is readonly. */ exposeDataTransfer: function() {}, /** * Don't load activity stream until 'preview:render' event has been fired. */ loadData: function() {}, /** * No need to bind events here because this activity stream is readonly. */ bindDataChange: function() { this.collection.on('add', function(activity) { if (!this.disposed) { this.renderPost(activity, true); } }, this); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.PreviewLayout * @alias SUGAR.App.view.layouts.BasePreviewLayout * @extends View.Layout */ ({ // Preview Layout (base) events: { 'click .closeSubdetail': 'hidePreviewPanel' }, initialize: function(opts) { app.view.Layout.prototype.initialize.call(this, opts); app.events.on('preview:open', this.showPreviewPanel, this); app.events.on('preview:close', this.hidePreviewPanel, this); app.events.on('preview:pagination:hide', this.hidePagination, this); }, /** * Show the preview panel, if it is part of the active drawer * @param event (optional) DOM event */ showPreviewPanel: function(event) { if (_.isUndefined(app.drawer) || app.drawer.isActive(this.$el)) { var layout = this.$el.parents('.sidebar-content'); layout.find('.side-pane').removeClass('active'); layout.find('.dashboard-pane').hide(); layout.find('.preview-pane').addClass('active'); var defaultLayout = this.closestComponent('sidebar'); if (defaultLayout) { defaultLayout.trigger('sidebar:toggle', true); } } }, /** * Hide the preview panel, if it is part of the active drawer * @param event (optional) DOM event */ hidePreviewPanel: function(event) { if (_.isUndefined(app.drawer) || app.drawer.isActive(this.$el)) { var layout = this.$el.parents('.sidebar-content'); layout.find('.side-pane').addClass('active'); layout.find('.dashboard-pane').show(); layout.find('.preview-pane').removeClass('active'); app.events.trigger('list:preview:decorate', false); } }, hidePagination: function() { if (_.isUndefined(app.drawer) || app.drawer.isActive(this.$el)) { this.hideNextPrevious = true; this.trigger('preview:pagination:update'); } } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ForecastsConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseForecastsConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'ConfigDrawerContentLayout', timeperiodsTitle: undefined, timeperiodsText: undefined, scenariosTitle: undefined, scenariosText: undefined, rangesTitle: undefined, rangesText: undefined, forecastByTitle: undefined, forecastByText: undefined, wkstColumnsTitle: undefined, wkstColumnsText: undefined, /** * @inheritdoc */ _initHowTo: function() { var appLang = app.lang, forecastBy = app.metadata.getModule('Forecasts', 'config').forecast_by, forecastByLabels = { forecastByModule: appLang.getAppListStrings('moduleList')[forecastBy], forecastByModuleSingular: appLang.getAppListStrings('moduleListSingular')[forecastBy] }; this.timeperiodsTitle = appLang.get('LBL_FORECASTS_CONFIG_TITLE_TIMEPERIODS', 'Forecasts'); this.timeperiodsText = appLang.get('LBL_FORECASTS_CONFIG_HELP_TIMEPERIODS', 'Forecasts'); this.scenariosTitle = appLang.get('LBL_FORECASTS_CONFIG_TITLE_SCENARIOS', 'Forecasts'); this.scenariosText = appLang.get('LBL_FORECASTS_CONFIG_HELP_SCENARIOS', 'Forecasts', forecastByLabels); this.rangesTitle = appLang.get('LBL_FORECASTS_CONFIG_TITLE_RANGES', 'Forecasts'); this.rangesText = appLang.get('LBL_FORECASTS_CONFIG_HELP_RANGES', 'Forecasts', forecastByLabels); this.forecastByTitle = appLang.get('LBL_FORECASTS_CONFIG_HOWTO_TITLE_FORECAST_BY', 'Forecasts'); this.forecastByText = appLang.get('LBL_FORECASTS_CONFIG_HELP_FORECAST_BY', 'Forecasts'); this.wkstColumnsTitle = appLang.get('LBL_FORECASTS_CONFIG_TITLE_WORKSHEET_COLUMNS', 'Forecasts'); this.wkstColumnsText = appLang.get('LBL_FORECASTS_CONFIG_HELP_WORKSHEET_COLUMNS', 'Forecasts'); }, /** * @inheritdoc */ _switchHowToData: function(helpId) { switch(helpId) { case 'config-timeperiods': this.currentHowToData.title = this.timeperiodsTitle; this.currentHowToData.text = this.timeperiodsText; break; case 'config-ranges': this.currentHowToData.title = this.rangesTitle; this.currentHowToData.text = this.rangesText; break; case 'config-scenarios': this.currentHowToData.title = this.scenariosTitle; this.currentHowToData.text = this.scenariosText; break; case 'config-forecast-by': this.currentHowToData.title = this.forecastByTitle; this.currentHowToData.text = this.forecastByText; break; case 'config-worksheet-columns': this.currentHowToData.title = this.wkstColumnsTitle; this.currentHowToData.text = this.wkstColumnsText; break; } } }) }, "metrics-help": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The layout for the metrics-help component. * * @class View.Layouts.Base.ForecastsMetricsHelpLayout * @alias SUGAR.App.view.layouts.BaseForecastsMetricsHelpLayout * @extends View.Layouts.Base.HelpLayout */ ({ // Metrics-help Layout (base) extendsFrom: 'HelpLayout', /** * URL to be used on the help button, this will navigate to SugarCRM Documention of Forecasts list view */ helpUrl: '', /** * Initializes the popover plugin for the button given. * * @param {jQuery} button The jQuery button. * @override */ _initPopover: function(button) { button.popover({ title: this._getTitle('LBL_FILTER_GUIDE_TITLE'), content: _.bind(function() { return this.$el; }, this), container: '.metrics-help-button', html: true, template: '<div class="helpmodal metrics-help-modal overflow-hidden z-40 border border-solid ' + 'border-[--border-base] rounded-md shadow-xl" data-modal="metrics-help">' + '<h3 class="popover-title popover-header"></h3>' + '<div class="popover-content !p-0 popover-body"></div>' + '</div>' }); // reposition the popover when the window is resized $(window).on(`resize.${this.cid}`, _.debounce(_.bind(function() { if (this.button) { this.button.popover('show'); } }, this),100)); }, /** * Collects server version, language, module, and route and returns an HTML * link to be used in the template. * * @private * @return {string} The anchor tag for the 'More Help' link. * @override */ _createMoreHelpLink: function() { var serverInfo = app.metadata.getServerInfo(); var lang = app.lang.getLanguage(); var module = app.controller.context.get('module'); var route = app.controller.context.get('layout'); var products = app.user.getProductCodes().join(','); var params = { edition: serverInfo.flavor, version: serverInfo.version, lang: lang, module: module, route: route, products: products }; if (params.route === 'records') { params.route = 'list'; } if (params.route === 'bwc') { // Parse `action` URL param. var action = window.location.hash.match(/#bwc.*action=(\w*)/i); if (action && !_.isUndefined(action[1])) { params.action = action[1]; } } return 'https://www.sugarcrm.com/crm/product_doc.php?' + $.param(params); }, /** * Creates the helpObject if it has not yet been created for this. * * @override */ _initHelpObject: function() { if (!this._helpObjectCreated) { this.helpUrl = this._createMoreHelpLink(); this._helpObjectCreated = true; } }, /** * Closes the Help modal if event target is outside of the Help modal. * * param {Object} evt jQuery event. * @override */ closeOnOutsideClick: function(evt) { let target = $(evt.target); if (target.closest('.metrics-help-button').length === 0) { this.toggle(false); } }, /** * @inheritdoc */ _dispose: function() { $(window).off('resize'); this._super('_dispose'); } }) } }} , "datas": {} }, "ForecastWorksheets":{"fieldTemplates": { "base": { "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsWorksheets.CurrencyField * @alias SUGAR.App.view.fields.BaseForecastsWorksheetsCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) extendsFrom: 'CurrencyField', initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents options.highlightChangedValues = true; this.plugins = _.clone(this.plugins) || []; this.plugins.push('ClickToEdit'); this._super("initialize", [options]); } }) }, "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsWorksheets.EnumField * @alias SUGAR.App.view.fields.BaseForecastsWorksheetsEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents options.highlightChangedValues = true; this.plugins = _.clone(this.plugins) || []; this.plugins.push('ClickToEdit'); this._super("initialize", [options]); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if(this.name === 'sales_stage') { this.model.on('change:sales_stage', function(model, newValue) { var salesStageWon = app.metadata.getModule('Forecasts', 'config').sales_stage_won; if(_.contains(salesStageWon, newValue)) { this.context.trigger('forecasts:cteRemove:' + model.id) } }, this); } if(this.name === 'commit_stage') { this.context.on('forecasts:cteRemove:' + this.model.id, function() { this.$el.removeClass('isEditable'); var $divEl = this.$('div.clickToEdit'); if($divEl.length) { $divEl.removeClass('clickToEdit'); } }, this); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); // make sure commit_stage enum maintains 'list' class for style reasons if(this.name === 'commit_stage' && this.$el.hasClass('disabled')) { this.$el.addClass('list'); } } }) }, "parent": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsWorksheets.ParentField * @alias SUGAR.App.view.fields.BaseForecastsWorksheetsParentField * @extends View.Fields.Base.ParentField */ ({ // Parent FieldTemplate (base) extendsFrom: 'ParentField', _render: function () { if(this.model.get('parent_deleted') == 1) { // set the value for use in the template this.deleted_value = this.model.get('name'); // override the template to use the delete one this.options.viewName = 'deleted'; } this._super("_render"); } }) }, "int": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsWorksheets.IntField * @alias SUGAR.App.view.fields.BaseForecastsWorksheetsIntField * @extends View.Fields.Base.IntField */ ({ // Int FieldTemplate (base) extendsFrom: 'IntField', initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents options.highlightChangedValues = true; this.plugins = _.clone(this.plugins) || []; this.plugins.push('ClickToEdit'); this._super("initialize", [options]); } }) }, "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsWorksheets.DateField * @alias SUGAR.App.view.fields.BaseForecastsWorksheetsDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * @inheritdoc */ initialize: function(options) { options.highlightChangedValues = true; this._super('initialize', [options]); }, /** * @inheritdoc * * Add `ClickToEdit` plugin to the list of required plugins. */ _initPlugins: function() { this._super('_initPlugins'); this.plugins = _.union(this.plugins, [ 'ClickToEdit' ]); return this; } }) } }} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Forecast Sales Rep Worksheet Record List. * * @class View.Views.Base.ForecastsWorksheets.RecordlistView * @alias SUGAR.App.view.views.BaseForecastsWorksheetsRecordlistView * @extends View.Views.Base.RecordlistView */ /** * Events * * forecasts:worksheet:is_dirty * on: this.context.parent || this.context * by: this.dirtyModels 'add' Event * when: a model is added to the dirtModels collection * * forecasts:worksheet:needs_commit * on: this.context.parent || this.context * by: checkForDraftRows * when: this.collection has a row newer than the last commit date * * forecasts:worksheet:totals * on: this.context.parent || this.context * by: calculateTotals * when: after it's done calculating totals from a collection change or reset event * * forecasts:worksheet:saved * on: this.context.parent || this.context * by: saveWorksheet * when: after it's done saving the worksheets to the db for a save draft * * forecasts:worksheet:commit * on: this.context.parent || this.context * by: forecasts:worksheet:saved event * when: only when the commit button is pressed * * forecasts:sync:start * on: this.context.parent * by: data:sync:start handler * when: this.collection starts syncing * * forecasts:sync:complete * on: this.context.parent * by: data:sync:complete handler * when: this.collection completes syncing * */ ({ // Recordlist View (base) /** * Who is my parent */ extendsFrom: 'RecordlistView', /** * Type of worksheet */ worksheetType: 'sales_rep', /** * Totals Storage */ totals: {}, /** * Before W/L/B Columns Colspan */ before_colspan: 0, /** * After W/L/B Columns Colspan */ after_colspan: 0, /** * Selected User Storage */ selectedUser: {}, /** * Can we edit this worksheet? * * defaults to true as it's always the current user that loads first */ canEdit: true, /** * Active Filters */ filters: [], /** * Filtered Collection */ filteredCollection: new Backbone.Collection(), /** * Selected Timeperiod Storage */ selectedTimeperiod: '', /** * Navigation Message To Display */ navigationMessage: '', /** * Special Navigation for the Window Refresh */ routeNavigationMessage: '', /** * Do we actually need to display a navigation message */ displayNavigationMessage: false, /** * Only check for draft records once */ hasCheckedForDraftRecords: false, /** * Holds the model currently being displayed in the preview panel */ previewModel: undefined, /** * Tracks if the preview panel is visible or not */ previewVisible: false, /** * is the collection syncing * @param boolean */ isCollectionSyncing: false, /** * is the commit history being loading * @param boolean */ isLoadingCommits: false, /** * The template for when we don't have access to a data point */ noAccessDataErrorTemplate: undefined, /** * Target URL of the nav action */ targetURL: '', /** * Current URL of the module */ currentURL: '', /** * Takes the values calculated in `this.totals` and condenses them for the totals.hbs subtemplate */ totalsTemplateObj: undefined, initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents this.plugins = _.without(this.plugins, 'ReorderableColumns', 'MassCollection'); this.plugins.push('ClickToEdit', 'DirtyCollection'); this._super('initialize', [options]); // we need to get the flex-list template from the ForecastWorksheets module so it can use the filteredCollection // for display this.template = app.template.getView('flex-list', this.module); this.selectedUser = this.context.get('selectedUser') || this.context.parent.get('selectedUser') || app.user.toJSON(); this.selectedTimeperiod = this.context.get('selectedTimePeriod') || this.context.parent.get('selectedTimePeriod') || ''; this.context.set('skipFetch', !(this.selectedUser.showOpps || !this.selectedUser.is_manager)); // if user is a manager, skip the initial fetch this.filters = this.context.get('selectedRanges') || this.context.parent.get('selectedRanges'); this.collection.sync = _.bind(this.sync, this); this.noAccessDataErrorTemplate = app.template.getField('base', 'noaccess')(this); this.currentURL = Backbone.history.getFragment(); }, bindDataChange: function() { // these are handlers that we only want to run when the parent module is forecasts if (!_.isUndefined(this.context.parent) && !_.isUndefined(this.context.parent.get('model'))) { if (this.context.parent.get('model').module == 'Forecasts') { this.context.parent.on('button:export_button:click', function() { if (!this._isUserManager()) { this.exportCallback(); } }, this); this.on('render', function() { this.renderCallback(); if (this.previewVisible) { this.decorateRow(this.previewModel); } }, this); this.on('list:toggle:column', function(column, isVisible, columnMeta) { // if we hide or show a column, recalculate totals this.calculateTotals(); }, this); this.context.parent.on('forecasts:worksheet:totals', function(totals, type) { if (type == this.worksheetType && !this._isUserManager()) { this.totalsTemplateObj = { orderedFields: [] }; var tpl = app.template.getView('recordlist.totals', this.module), filteredKey, totalValues; // loop through visible fields in metadata order _.each(this._fields.visible, function(field) { if(_.contains(['likely_case', 'best_case', 'worst_case'], field.name)) { totalValues = {}; totalValues.fieldName = field.name; switch(field.name) { case 'worst_case': case 'best_case': filteredKey = field.name.split('_')[0]; break; case 'likely_case': filteredKey = 'amount'; break; } totalValues.display = this.totals[field.name + '_display']; totalValues.access = this.totals[field.name + '_access']; totalValues.filtered = this.totals['filtered_' + filteredKey]; totalValues.overall = this.totals['overall_' + filteredKey]; this.totalsTemplateObj.orderedFields.push(totalValues); } }, this); this.$('tfoot').remove(); this.$('tbody').after(tpl(this)); } }, this); /** * trigger an event if dirty */ this.dirtyModels.on('add change reset', function(){ if (!this._isUserManager()) { this.context.parent.trigger('forecasts:worksheet:dirty', this.worksheetType, this.dirtyModels.length > 0); } }, this); this.context.parent.on('change:selectedTimePeriod', function(model, changed) { this.updateSelectedTimeperiod(changed); }, this); this.context.parent.on('change:selectedUser', function(model, changed) { this.updateSelectedUser(changed) }, this); this.context.parent.on('button:save_draft_button:click', function() { if (!this._isUserManager()) { // after we save, trigger the needs_commit event this.context.parent.once('forecasts:worksheet:saved', function() { // clear out the current navigation message this.setNavigationMessage(false, '', ''); this.cleanUpDirtyModels(); this.refreshData(); this.collection.once('reset', function(){ this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); }, this); }, this); this.saveWorksheet(true); } }, this); this.context.parent.on('button:commit_button:click', function() { if (!this._isUserManager()) { this.context.parent.once('forecasts:worksheet:saved', function() { this.context.parent.trigger('forecasts:worksheet:commit', this.selectedUser, this.worksheetType, this.getCommitTotals()) }, this); this.saveWorksheet(false); } }, this); // On cancel click, revert any unsaved changes to Opps/RLIs and reset the filtered collection this.listenTo(this.context.parent, 'button:cancel_button:click', () => { if (!this._isUserManager()) { this.collection.models.forEach(model => model.revertAttributes()); this.cleanUpDirtyModels(); this.setNavigationMessage(false, '', ''); this.filterCollection(); if (!this.disposed) { this.render(); } } }); this.context.parent.on('change:currentForecastCommitDate', function(context, changed) { if (!this._isUserManager()) { this.checkForDraftRows(changed); } }, this); if (this.context.parent.has('collection')) { var parentCollection = this.context.parent.get('collection'); parentCollection.on('data:sync:start', function() { this.isLoadingCommits = true; }, this); parentCollection.on('data:sync:complete', function() { this.isLoadingCommits = false; }, this); } this.collection.on('data:sync:start', function() { this.isCollectionSyncing = true; // Begin sync start for buttons this.context.parent.trigger('forecasts:sync:start'); }, this); this.collection.on('data:sync:complete', function() { this.isCollectionSyncing = false; // End sync start for buttons this.context.parent.trigger('forecasts:sync:complete'); }, this); this.collection.on('reset', function() { this.setNavigationMessage(false, '', ''); this.cleanUpDirtyModels(); var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, false); if (this.isLoadingCommits === false) { this.checkForDraftRows(ctx.get('currentForecastCommitDate')); } this.filterCollection(); }, this); this.collection.on('change:commit_stage', function(model) { if (!_.isEmpty(this.filters) // we have filters && _.indexOf(this.filters, model.get('commit_stage')) === -1 // and the commit_stage is not shown ) { this.filterCollection(); _.defer(_.bind(function() { if (!this.disposed) { this.render(); } }, this)); } else { var commitStage = model.get('commit_stage'), includedCommitStages = app.metadata.getModule('Forecasts', 'config').commit_stages_included, el = this.$('tr[name=' + model.module + '_' + model.id + ']'), isIncluded = _.include(includedCommitStages, commitStage); if (el) { // we need to update the data-forecast attribute on the row // and the new commit stage is visible el.attr('data-forecast', commitStage); if (isIncluded && !el.hasClass('included')) { // if the commitStage is included, and it doesnt have the included class, add it el.addClass('included'); model.set({ includedInForecast: true }, {silent: true}); } else if (!isIncluded && el.hasClass('included')) { // if the commitStage isn't included, and it still has the class, remove it el.removeClass('included'); model.unset('includedInForecast'); } } } }, this); this.context.parent.on('change:selectedRanges', function(model, changed) { this.filters = changed; this.once('render', function() { app.alert.dismiss('worksheet_filtering'); }); this.filterCollection(); this.calculateTotals(); if (!this.disposed) this.render(); }, this); this.context.parent.on('forecasts:worksheet:committed', function() { if (!this._isUserManager()) { this.setNavigationMessage(false, '', ''); this.cleanUpDirtyModels(); var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, false); this.refreshData(); } }, this); this.context.parent.on('forecasts:worksheet:is_dirty', function(worksheetType, is_dirty) { if (this.worksheetType == worksheetType) { if (is_dirty) { this.setNavigationMessage(true, 'LBL_WARN_UNSAVED_CHANGES', 'LBL_WARN_UNSAVED_CHANGES'); } else { this.setNavigationMessage(false, '', ''); } } }, this); app.routing.before('route', this.beforeRouteHandler, this); $(window).bind('beforeunload.' + this.worksheetType, _.bind(function() { var ret = this.showNavigationMessage('window'); if (_.isString(ret)) { return ret; } }, this)); } } // listen for the before list:orderby to handle if the worksheet is dirty or notW this.before('list:orderby', function(options) { if (this.isDirty()) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WARN_UNSAVED_CHANGES_CONFIRM_SORT', 'Forecasts'), onConfirm: _.bind(function() { this._setOrderBy(options); }, this) }); return false; } return true; }, this); // When the collection resets, recalculate the list totals and notify // the context that the totals are being initialized this.listenTo(this.collection, 'reset', function() { this.calculateTotals(true); this.context.parent.trigger('forecasts:worksheet:totals:initialized', this.totals); }); // When a value in the collection changes, recalculate the list totals this.listenTo(this.collection, 'change', function() { this.calculateTotals(); }); if (!_.isUndefined(this.dirtyModels)) { this.dirtyModels.on('add', function() { if (this.canEdit) { var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, true); } }, this); } this.layout.on('hide', function() { this.totals = {}; }, this); // call the parent this._super('bindDataChange'); }, /** * Determines if the forecast user is a manager type * * @return {boolean} true if the forecast user is a manager; false otherwise * @private */ _isUserManager: function() { let user = this.selectedUser || this.context.parent.get('selectedUser') || app.user.toJSON(); return app.utils.getForecastType(user.is_manager, user.showOpps) === 'Rollup'; }, beforeRouteHandler: function() { return this.showNavigationMessage('router'); }, /** * default navigation callback for alert message */ defaultNavCallback: function(){ this.displayNavigationMessage = false; app.router.navigate(this.targetURL, {trigger: true}); }, /** * @inheritdoc */ unbindData: function() { app.events.off(null, null, this); this._super('unbindData'); }, /** * Handle Showing of the Navigation messages if any are applicable * * @param type * @returns {*} */ showNavigationMessage: function(type, callback) { if (!_.isFunction(callback)) { callback = this.defaultNavCallback; } if (!this._isUserManager()) { var canEdit = this.dirtyCanEdit || this.canEdit; if (canEdit && this.displayNavigationMessage) { if (type == 'window') { if (!_.isEmpty(this.routeNavigationMessage)) { return app.lang.get(this.routeNavigationMessage, 'Forecasts'); } return false; } this.targetURL = Backbone.history.getFragment(); //Replace the url hash back to the current staying page app.router.navigate(this._currentUrl, {trigger: false, replace: true}); app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get(this.navigationMessage, 'Forecasts').split('<br>'), onConfirm: _.bind(function() { callback.call(this); }, this) }); return false; } } return true; }, /** * Utility to set the Navigation Message and Flag * * @param display * @param reload_label * @param route_label */ setNavigationMessage: function(display, reload_label, route_label) { this.displayNavigationMessage = display; this.navigationMessage = reload_label; this.routeNavigationMessage = route_label; this.context.parent.trigger('forecasts:worksheet:navigationMessage', this.navigationMessage); }, /** * Handle the export callback */ exportCallback: function() { if (this.canEdit && this.isDirty()) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WORKSHEET_EXPORT_CONFIRM', 'Forecasts'), onConfirm: _.bind(function() { this.doExport(); }, this) }); } else { this.doExport(); } }, /** * Actually run the export */ doExport: function() { app.alert.show('massexport_loading', {level: 'process', title: app.lang.get('LBL_LOADING')}); var params = { timeperiod_id: this.selectedTimeperiod, user_id: this.selectedUser.id, filters: this.filters, platform: app.config.platform }; var url = app.api.buildURL(this.module, 'export', null, params); app.api.fileDownload(url, { complete: function(data) { app.alert.dismiss('massexport_loading'); } }, { iframe: this.$el }); }, /** * @inheritdoc * * Calculates worksheet totals if the selected forecast user is not a manager type */ _render: function() { // Empty out the left columns this.leftColumns = []; if (!this._isUserManager()) { this.calculateTotals(); } this._super('_render'); }, /** * Callback for the on('render') event */ renderCallback: function() { if (this.layout.isVisible()) { this.layout.hide(); } var user = this.selectedUser || this.context.parent.get('selectedUser') || app.user.toJSON() if (user.showOpps || !user.is_manager) { if (this.filteredCollection.length == 0) { var tpl = app.template.getView('recordlist.noresults', this.module); this.$('tbody').html(tpl(this)); } // insert the footer if (!_.isEmpty(this.totals) && !this._isUserManager()) { var tpl = app.template.getView('recordlist.totals', this.module); this.$('tbody').after(tpl(this)); } //adjust width of sales stage column to longest value so cells don't shift when using CTE var sales_stage_width = this.$('td[data-field-name="sales_stage"] span.isEditable').width(); var sales_stage_outerwidth = this.$('td[data-field-name="sales_stage"] span.isEditable').outerWidth(); this.$('td[data-field-name="sales_stage"] span.isEditable').width(sales_stage_width + 20); this.$('td[data-field-name="sales_stage"] span.isEditable').parent('td').css('min-width', sales_stage_outerwidth + 26 + 'px'); // figure out if any of the row actions need to be disabled this.setRowActionButtonStates(); } }, /** * Code to handle if the selected user changes * * @param changed */ updateSelectedUser: function(changed) { var doFetch = false; if (this.selectedUser.id != changed.id) { // user changed. make sure it's not a manager view before we say fetch or not doFetch = (changed.showOpps || !changed.is_manager); } // if we are already not going to fetch, check to see if the new user is showingOpps or is not // a manager, then we want to fetch if (!doFetch && (changed.showOpps || !changed.is_manager)) { doFetch = true; } if (this.displayNavigationMessage) { // save the user just in case this.dirtyUser = this.selectedUser; this.dirtyCanEdit = this.canEdit; } this.cleanUpDirtyModels(); this.selectedUser = changed; // Set the flag for use in other places around this controller to suppress stuff if we can't edit this.canEdit = (this.selectedUser.id == app.user.get('id')); this.hasCheckedForDraftRecords = false; if (doFetch) { this.refreshData(); } else { if ((!this.selectedUser.showOpps && this.selectedUser.is_manager) && !this._isUserManager()) { // we need to hide this.layout.hide(); } } }, updateSelectedTimeperiod: function(changed) { if (this.displayNavigationMessage) { // save the time period just in case this.dirtyTimeperiod = this.selectedTimeperiod; } this.selectedTimeperiod = changed; this.hasCheckedForDraftRecords = false; if (!this._isUserManager()) { this.refreshData(); } }, /** * Check to make sure that if there are dirty rows, then trigger the needs_commit event to enable * the buttons * * @fires forecasts:worksheet:needs_commit * @param lastCommitDate */ checkForDraftRows: function(lastCommitDate) { if (!this._isUserManager() && this.canEdit && this.hasCheckedForDraftRecords === false && !_.isEmpty(this.collection.models) && this.isCollectionSyncing === false) { this.hasCheckedForDraftRecords = true; if (_.isUndefined(lastCommitDate)) { // we have rows but no commit, enable the commit button this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); } else { // check to see if anything in the collection is a draft, if it is, then send an event // to notify the commit button to enable this.collection.find(function(item) { if (item.get('date_modified') > lastCommitDate) { this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); return true; } return false; }, this); } } else if (this._isUserManager() && this.canEdit && this.hasCheckedForDraftRecords === false) { // since the layout is not visible, lets wait for it to become visible this.layout.once('show', function() { this.checkForDraftRows(lastCommitDate); }, this); } else if (this.isCollectionSyncing === true) { this.collection.once('data:sync:complete', function() { this.checkForDraftRows(lastCommitDate); }, this); } }, /** * Handles setting the proper state for the Preview in the row-actions */ setRowActionButtonStates: function() { _.each(this.fields, function(field) { if (field.def.event === 'list:preview:fire') { // we have a field that needs to be disabled, so disable it! field.setDisabled((field.model.get('parent_deleted') == '1')); field.render(); } }); }, /** * Filter the Collection so we only show what the filter says we should show */ filterCollection: function() { this.filteredCollection.reset(); if (_.isEmpty(this.filters)) { this.filteredCollection.add(this.collection.models); } else { this.collection.each(function(model) { if (_.indexOf(this.filters, model.get('commit_stage')) !== -1) { this.filteredCollection.add(model); } }, this); } }, /** * Save the worksheet to the database * * @fires forecasts:worksheet:saved * @return {Number} */ saveWorksheet: function(isDraft) { // only run the save when the worksheet is visible and it has dirty records var totalToSave = 0; if (!this._isUserManager()) { var saveCount = 0, ctx = this.context.parent || this.context; if (this.isDirty()) { totalToSave = this.dirtyModels.length; this.dirtyModels.each(function(model) { //set properties on model to aid in save model.set({ draft: (isDraft && isDraft == true) ? 1 : 0, timeperiod_id: this.dirtyTimeperiod || this.selectedTimeperiod, current_user: this.dirtyUser.id || this.selectedUser.id }, {silent: true}); // set the correct module on the model since sidecar doesn't support sub-beans yet model.save({}, {success: _.bind(function() { saveCount++; // Make sure the preview panel gets updated model info if (this.previewVisible) { var previewId = this.previewModel.get('parent_id') || this.previewModel.get('id'); if (model.get('parent_id') == previewId) { var previewCollection = new Backbone.Collection(); this.filteredCollection.each(function(model) { if (model.get('parent_deleted') !== '1') { previewCollection.add(model); } }, this); app.events.trigger('preview:render', model, previewCollection, true, model.get('id'), true); } } //if this is the last save, go ahead and trigger the callback; if (totalToSave === saveCount) { // we only want to show this when the draft is being saved if (isDraft) { app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get('LBL_FORECASTS_WIZARD_SUCCESS_TITLE', 'Forecasts') + ':', messages: [app.lang.get('LBL_FORECASTS_WORKSHEET_SAVE_DRAFT_SUCCESS', 'Forecasts')] }); } ctx.trigger('forecasts:worksheet:saved', totalToSave, this.worksheetType, isDraft); } }, this), silent: true, alerts: { 'success': false }}); }, this); this.cleanUpDirtyModels(); } else { // we only want to show this when the draft is being saved if (isDraft) { app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get('LBL_FORECASTS_WIZARD_SUCCESS_TITLE', 'Forecasts') + ':', messages: [app.lang.get('LBL_FORECASTS_WORKSHEET_SAVE_DRAFT_SUCCESS', 'Forecasts')] }); } ctx.trigger('forecasts:worksheet:saved', totalToSave, this.worksheetType, isDraft); } } return totalToSave }, /** * Calculate the totals for the visible fields * * @param {boolean} reset true when being called on a reset */ calculateTotals: function(reset = false) { // fire an event on the parent context if (!this._isUserManager() || reset) { this.totals = this.getCommitTotals(); var calcFields = ['worst_case', 'best_case', 'likely_case'], fields = _.filter(this._fields.visible, function(field) { if (_.contains(calcFields, field.name)) { this.totals[field.name + '_access'] = app.acl.hasAccess('read', this.module, app.user.get('id'), field.name); this.totals[field.name + '_display'] = true; return true; } return false; }, this); // loop though all the fields and find where the worst/likely/best start at for(var x = 0; x < this._fields.visible.length; x++) { var f = this._fields.visible[x]; if (_.contains(calcFields, f.name)) { break; } } this.before_colspan = x; this.after_colspan = (this._fields.visible.length - (x + fields.length)); //TODO: SS-2847 We need to do a large rework to remove this view // completely and not break forecasts. } }, /** * Set the loading message and have a way to hide it */ displayLoadingMessage: function() { app.alert.show('worksheet_loading', {level: 'process', title: app.lang.get('LBL_LOADING')} ); this.collection.once('reset', function() { app.alert.dismiss('worksheet_loading'); }, this); }, /** * Custom Method to handle the refreshing of the worksheet Data */ refreshData: function() { this.displayLoadingMessage(); this.collection.fetch(); }, /** * Custom Sync Method * * @param method * @param model * @param options */ sync: function(method, model, options) { var callbacks, url; options = options || {}; options.params = options.params || {}; if (!_.isUndefined(this.selectedUser.id)) { options.params.user_id = this.selectedUser.id; } if (!_.isEmpty(this.selectedTimeperiod)) { options.params.timeperiod_id = this.selectedTimeperiod; } options.limit = 1000; options = app.data.parseOptionsForSync(method, model, options); // Since parent_name breaks the XHR call in the order by, just use the name field instead // they are the same anyways. if (!_.isUndefined(options.params.order_by) && options.params.order_by.indexOf('parent_name') === 0) { options.params.order_by = options.params.order_by.replace('parent_', ''); } // custom success handler options.success = _.bind(function(data) { if (!this.disposed) { this.collection.reset(data); } }, this); callbacks = app.data.getSyncCallbacks(method, model, options); this.collection.trigger('data:sync:start', method, model, options); url = app.api.buildURL('ForecastWorksheets', null, null, options.params); app.api.call('read', url, null, callbacks); }, /** * Get the totals that need to be committed * * @returns {{amount: number, best_case: number, worst_case: number, overall_amount: number, overall_best: number, overall_worst: number, timeperiod_id: (*|bindDataChange.selectedTimeperiod), lost_count: number, lost_amount: number, won_count: number, won_amount: number, included_opp_count: number, total_opp_count: Number, closed_count: number, closed_amount: number}} */ getCommitTotals: function() { var includedAmount = 0, includedBest = 0, includedWorst = 0, filteredAmount = 0, filteredBest = 0, filteredWorst = 0, filteredCount = 0, overallAmount = 0, overallBest = 0, overallWorst = 0, includedCount = 0, lostCount = 0, lostAmount = 0, lostBest = 0, lostWorst = 0, wonCount = 0, wonAmount = 0, wonBest = 0, wonWorst = 0, includedClosedCount = 0, includedClosedAmount = 0, cfg = app.metadata.getModule('Forecasts', 'config'), startEndDates = this.context.get('selectedTimePeriodStartEnd') || this.context.parent.get('selectedTimePeriodStartEnd'), activeFilters = this.context.get('selectedRanges') || this.context.parent.get('selectedRanges') || []; //Get the excluded_sales_stage property. Default to empty array if not set var sales_stage_won_setting = cfg.sales_stage_won || [], sales_stage_lost_setting = cfg.sales_stage_lost || []; // set up commit_stages that should be processed in included total var commit_stages_in_included_total = ['include']; if (cfg.forecast_ranges == 'show_custom_buckets') { commit_stages_in_included_total = cfg.commit_stages_included; } this.collection.each(function(model) { // make sure that the selected date is between the start and end dates for the current timeperiod // if it's not, then don't include it in the totals if (app.date(model.get('date_closed')).isBetween(startEndDates['start'], startEndDates['end'])) { var won = _.include(sales_stage_won_setting, model.get('sales_stage')), lost = _.include(sales_stage_lost_setting, model.get('sales_stage')), commit_stage = model.get('commit_stage'), base_rate = model.get('base_rate'), // added || 0 in case these converted out to NaN so they dont make charts blow up worst_base = app.currency.convertWithRate(model.get('worst_case'), base_rate) || 0, amount_base = app.currency.convertWithRate(model.get('likely_case'), base_rate) || 0, best_base = app.currency.convertWithRate(model.get('best_case'), base_rate) || 0, includedInForecast = _.include(commit_stages_in_included_total, commit_stage), includedInFilter = _.include(activeFilters, commit_stage); if (won && includedInForecast) { wonAmount = app.math.add(wonAmount, amount_base); wonBest = app.math.add(wonBest, best_base); wonWorst = app.math.add(wonWorst, worst_base); wonCount++; includedClosedCount++; includedClosedAmount = app.math.add(amount_base, includedClosedAmount); } else if (lost) { lostAmount = app.math.add(lostAmount, amount_base); lostBest = app.math.add(lostBest, best_base); lostWorst = app.math.add(lostWorst, worst_base); lostCount++; } if (includedInFilter || _.isEmpty(activeFilters)) { filteredAmount = app.math.add(filteredAmount, amount_base); filteredBest = app.math.add(filteredBest, best_base); filteredWorst = app.math.add(filteredWorst, worst_base); filteredCount++; } if (includedInForecast) { includedAmount = app.math.add(includedAmount, amount_base); includedBest = app.math.add(includedBest, best_base); includedWorst = app.math.add(includedWorst, worst_base); includedCount++; // since we're already looping through the collection of models and we have // the included commit stages, set or unset the includedInForecast property here model.set({ includedInForecast: true }, {silent: true}); } else if (model.has('includedInForecast')) { model.unset('includedInForecast'); } overallAmount = app.math.add(overallAmount, amount_base); overallBest = app.math.add(overallBest, best_base); overallWorst = app.math.add(overallWorst, worst_base); } }, this); return { 'likely_case': includedAmount, 'best_case': includedBest, 'worst_case': includedWorst, 'overall_amount': overallAmount, 'overall_best': overallBest, 'overall_worst': overallWorst, 'filtered_amount': filteredAmount, 'filtered_best': filteredBest, 'filtered_worst': filteredWorst, 'timeperiod_id': this.dirtyTimeperiod || this.selectedTimeperiod, 'lost_count': lostCount, 'lost_amount': lostAmount, 'won_count': wonCount, 'won_amount': wonAmount, 'included_opp_count': includedCount, 'total_opp_count': this.collection.length, 'closed_count': includedClosedCount, 'closed_amount': includedClosedAmount }; }, /** * We need to overwrite so we pass in the filterd list */ addPreviewEvents: function() { //When clicking on eye icon, we need to trigger preview:render with model&collection this.context.on('list:preview:fire', function(model) { var previewCollection = new Backbone.Collection(); this.filteredCollection.each(function(model) { if (model.get('parent_deleted') !== '1') { previewCollection.add(model); } }, this); if (_.isUndefined(this.previewModel) || model.get('id') != this.previewModel.get('id')) { this.previewModel = model; app.events.trigger('preview:render', model, previewCollection, true); } else { // user already has the preview panel open and has clicked the preview icon again // remove row decoration this.decorateRow(); // close the preview panel app.events.trigger('preview:close'); } }, this); //When switching to next/previous record from the preview panel, we need to update the highlighted row app.events.on('list:preview:decorate', this.decorateRow, this); if (this.layout) { this.layout.on('list:sort:fire', function() { //When sorting the list view, we need to close the preview panel app.events.trigger('preview:close'); }, this); } app.events.on('preview:render', function(model) { if (this.disposed) { return; } this.previewModel = model; this.previewVisible = true; }, this); app.events.on('preview:close', function() { this.previewVisible = false; this.previewModel = undefined; }, this); }, /** * @inheritdoc */ _dispose: function() { if (!_.isUndefined(this.context.parent) && !_.isNull(this.context.parent)) { this.context.parent.off(null, null, this); if (this.context.parent.has('collection')) { this.context.parent.get('collection').off(null, null, this); } } // make sure this alert is hidden if the the view is disposed app.alert.dismiss('workshet_loading'); app.routing.offBefore('route', this.beforeRouteHandler, this); $(window).off('beforeunload.' + this.worksheetType); this.stopListening(); this._super('_dispose'); } }) }, "filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ForecastsWorksheets.FilterView * @alias SUGAR.App.view.views.BaseForecastsWorksheetsFilterView * @extends View.View */ ({ // Filter View (base) /** * Front End Javascript Events */ events: { 'keydown .select2-input': 'disableSelect2KeyPress' }, /** * Since we don't what the user to be able to type in the filter input * just disable all key press events for the .select2-input boxes * * @param event */ disableSelect2KeyPress: function(event) { event.preventDefault(); }, /** * Key for saving the users last selected filters */ userLastWorksheetFilterKey: undefined, /** * Initialize because we need to set the selectedUser variable * @param {Object} options */ initialize: function(options) { this._super('initialize', [options]); this.userLastWorksheetFilterKey = app.user.lastState.key('worksheet-filter', this); this.selectedUser = { id: app.user.get('id'), is_manager: app.user.get('is_manager'), showOpps: false }; }, // prevent excessive renders when things change. bindDomChange: function() {}, /** * Override the render to have call the group by toggle * * @private */ _render:function () { app.view.View.prototype._render.call(this); this.node = this.$el.find("#" + this.cid); // set up the filters this._setUpFilters(); return this; }, /** * Set up select2 for driving the filter UI * @param node the element to use as the basis for select2 * @private */ _setUpFilters: function() { var ctx = this.context.parent || this.context, user_ranges = app.user.lastState.get(this.userLastWorksheetFilterKey), selectedRanges = user_ranges || ctx.get('selectedRanges') || app.defaultSelections.ranges; this.node.select2({ data:this._getRangeFilters(), initSelection: function(element, callback) { callback(_.filter( this.data, function(obj) { return _.contains(this, obj.id); }, $(element.val().split(",")) )); }, multiple:true, placeholder: app.lang.get("LBL_MODULE_FILTER"), dropdownCss: {width:"auto"}, containerCssClass: "select2-choices-pills-close", containerCss: "border: none", formatSelection: this.formatCustomSelection, formatResultCssClass: this.formatCustomResultCssClass, dropdownCssClass: "search-filter-dropdown", escapeMarkup: function(m) { return m; }, width: '100%' }); // set the default selections this.node.select2("val", selectedRanges); // add a change handler that updates the forecasts context appropriately with the user's selection this.node.change( { context: ctx }, _.bind(function(event) { app.alert.show('worksheet_filtering', {level: 'process', title: app.lang.get('LBL_LOADING')} ); app.user.lastState.set(this.userLastWorksheetFilterKey, event.val); _.delay(function() { event.data.context.set('selectedRanges', event.val); }, 25); }, this) ); }, /** * Formats pill selections * * @param {Object} item The selected item * @param {jQuery} container The jQuery container element */ formatCustomSelection: function(item, container) { $(container.prevObject).addClass(item.id + '-select-choice'); return '<span class="select2-choice-type" disabled="disabled">' + app.lang.get('LBL_FILTER') + '</span><a class="select2-choice-filter" rel="' + _.escape(item.id) + '" href="javascript:void(0)">' + _.escape(item.text) + '</a>'; }, /** * Adds custom css class for result items * * @param {Object} object The selected item */ formatCustomResultCssClass: function(object) { return object.id + '-select-result'; }, /** * Gets the list of filters that correspond to the forecasts range settings that were selected by the admin during * configuration of the forecasts module. * * @return {Array} array of the selected ranges */ _getRangeFilters: function() { var options = app.metadata.getModule('Forecasts', 'config').buckets_dom || 'commit_stage_binary_dom'; return _.map(app.lang.getAppListStrings(options), function(value, key) { return {id: key, text: value}; }); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ForecastManagerWorksheets":{"fieldTemplates": { "base": { "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsManagerWorksheets.CurrencyField * @alias SUGAR.App.view.fields.BaseForecastsManagerWorksheetsCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) extendsFrom: 'CurrencyField', initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents this.plugins = _.clone(this.plugins) || []; this.plugins.push('ClickToEdit'); this._super("initialize", [options]); } }) }, "userLink": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsManagerWorksheets.UserLinkField * @alias SUGAR.App.view.fields.BaseForecastsManagerWorksheetsUserLinkField * @extends View.Fields.Base.BaseField */ ({ // UserLink FieldTemplate (base) /** * Attach a click event to <a class="worksheetManagerLink"> field */ events: { 'click a.worksheetManagerLink': 'linkClicked' }, /** * Holds the user_id for passing into userTemplate */ uid: '', initialize: function(options) { this.uid = this.model.get('user_id'); app.view.Field.prototype.initialize.call(this, options); return this; }, format: function(value) { var su = this.context.get('selectedUser') || this.context.parent.get('selectedUser') || app.user.toJSON(); if (value == su.full_name && su.id == app.user.get('id')) { var hb = Handlebars.compile("{{str key module context}}"); value = hb({'key': 'LBL_MY_MANAGER_LINE', 'module': this.module, 'context': su}); } return value; }, /** * Handle a user link being clicked * @param event */ linkClicked: function(event) { var uid = $(event.target).data('uid'); var selectedUser = { id: '', user_name: '', full_name: '', first_name: '', last_name: '', is_manager: false, showOpps: false, reportees: [] }; var options = { dataType: 'json', success: _.bind(function(data) { selectedUser.id = data.id; selectedUser.user_name = data.user_name; selectedUser.full_name = data.full_name; selectedUser.first_name = data.first_name; selectedUser.last_name = data.last_name; selectedUser.is_manager = data.is_manager; selectedUser.reports_to_id = data.reports_to_id; selectedUser.reports_to_name = data.reports_to_name; selectedUser.is_top_level_manager = data.is_top_level_manager || (data.is_manager && _.isEmpty(data.reports_to_id)); var su = this.context.get('selectedUser') || this.context.parent.get('selectedUser') || app.user.toJSON(); // get the current selected user, if the id's match up set the showOpps to be true) selectedUser.showOpps = (su.id == data.id); this.context.parent.trigger("forecasts:user:changed", selectedUser, this.context.parent); }, this) }; myURL = app.api.buildURL('Forecasts', 'user/' + uid); app.api.call('read', myURL, null, options); } }) }, "commithistory": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ForecastsManagerWorksheets.CommithistoryField * @alias SUGAR.App.view.fields.BaseForecastsManagerWorksheetsCommithistoryField * @extends View.Fields.Base.BaseField */ ({ // Commithistory FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.on('render', function() { this.loadData(); }, this); }, /** * @inheritdoc */ loadData: function() { var ctx = this.context.parent || this.context, su = ctx.get('selectedUser') || app.user.toJSON(), isManager = this.model.get('is_manager'), showOpps = (su.id == this.model.get('user_id')) ? 1 : 0, forecastType = app.utils.getForecastType(isManager, showOpps), args_filter = [], options = {}, url; args_filter.push( {"user_id": this.model.get('user_id')}, {"forecast_type": forecastType}, {"timeperiod_id": this.view.selectedTimeperiod} ); url = {"url": app.api.buildURL('Forecasts', 'filter'), "filters": {"filter": args_filter}}; options.success = _.bind(function(data) { this.buildLog(data); }, this); app.api.call('create', url.url, url.filters, options, { context: this }); }, /** * Build out the History Log * @param data */ buildLog: function(data) { data = data.records; var ctx = this.context.parent || this.context, forecastCommitDate = ctx.get('currentForecastCommitDate'), commitDate = app.date(forecastCommitDate), newestModel = new Backbone.Model(_.first(data)), // get everything that is left but the first item. otherModels = _.last(data, data.length - 1), oldestModel = {}, displayCommitDate = newestModel.get('date_modified'); // using for because you can't break out of _.each for(var i = 0; i < otherModels.length; i++) { // check for the first model equal to or past the forecast commit date // we want the last commit just before the whole forecast was committed if (app.date(otherModels[i].date_modified) <= commitDate) { oldestModel = new Backbone.Model(otherModels[i]); break; } } // create the history log var tpl = app.template.getField(this.type, 'log', this.module); this.$el.html(tpl({ commit: app.utils.createHistoryLog(oldestModel, newestModel).text, commit_date: displayCommitDate })); }, /** * Override the _render so we can tell it where to render at in the list view * @private */ _render: function() { // set the $el equal to the place holder so it renders in the correct spot this.$el = this.view.$('span[sfuuid="' + this.sfId + '"]'); this._super('_render'); } }) } }} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Forecast Manager Worksheet Record List. * * Events * * forecasts:worksheet:is_dirty * on: this.context.parent || this.context * by: this.dirtyModels 'add' Event * when: a model is added to the dirtModels collection * * forecasts:worksheet:needs_commit * on: this.context.parent || this.context * by: checkForDraftRows * when: this.collection has a row newer than the last commit date * * forecasts:worksheet:totals * on: this.context.parent || this.context * by: calculateTotals * when: after it's done calculating totals from a collection change or reset event * * forecasts:worksheet:saved * on: this.context.parent || this.context * by: saveWorksheet and _worksheetSaveHelper * when: after it's done saving the worksheets to the db for a save draft * * forecasts:worksheet:commit * on: this.context.parent || this.context * by: forecasts:worksheet:saved event * when: only when the commit button is pressed * * forecasts:assign_quota * on: this.context.parent || this.context * by: forecasts:worksheet:saved event * when: only when the Assign Quota button is pressed * * forecasts:sync:start * on: this.context.parent * by: data:sync:start handler * when: this.collection starts syncing * * forecasts:sync:complete * on: this.context.parent * by: data:sync:complete handler * when: this.collection completes syncing * * @class View.Views.Base.ForecastsManagerWorksheets.RecordListView * @alias SUGAR.App.view.views.BaseForecastsManagerWorksheetsRecordListView * @extends View.Views.Base.RecordListView */ ({ // Recordlist View (base) /** * Who are parent is */ extendsFrom: 'RecordlistView', /** * what type of worksheet are we? */ worksheetType: 'manager', /** * Selected User Storage */ selectedUser: {}, /** * Can we edit this worksheet? */ canEdit: true, /** * Selected Timeperiod Storage */ selectedTimeperiod: {}, /** * Totals Storage */ totals: {}, /** * Default values for the blank rows */ defaultValues: { id: '', // set id to empty so it fails the isNew() check as we don't want this to override the currency quota: '0', best_case: '0', best_case_adjusted: '0', likely_case: '0', likely_case_adjusted: '0', worst_case: '0', worst_case_adjusted: '0', show_history_log: 0 }, /** * Navigation Message To Display */ navigationMessage: '', /** * Special Navigation for the Window Refresh */ routeNavigationMessage: '', /** * Do we actually need to display a navigation message */ displayNavigationMessage: false, /** * Only check for draft records once */ hasCheckedForDraftRecords: false, /** * Draft Save Type */ draftSaveType: undefined, /** * is the collection syncing * @param boolean */ isCollectionSyncing: false, /** * is the commit history being loading * @param boolean */ isLoadingCommits: false, /** * Target URL of the nav action */ targetURL: '', /** * Current URL of the module */ currentURL: '', /** * @inheritdoc */ initialize: function(options) { // we need to make a clone of the plugins and then push to the new object. this prevents double plugin // registration across ExtendedComponents this.plugins = _.without(this.plugins, 'ReorderableColumns', 'MassCollection'); this.plugins.push('ClickToEdit'); this.plugins.push('DirtyCollection'); this._super("initialize", [options]); this.template = app.template.getView('flex-list', this.module); this.selectedUser = this.context.get('selectedUser') || this.context.parent.get('selectedUser') || app.user.toJSON(); this.selectedTimeperiod = this.context.get('selectedTimePeriod') || this.context.parent.get('selectedTimePeriod') || ''; this.context.set('skipFetch', (this.selectedUser.is_manager && this.selectedUser.showOpps)); // skip the initial fetch, this will be handled by the changing of the selectedUser this.collection.sync = _.bind(this.sync, this); this.currentURL = Backbone.history.getFragment(); }, /** * @inheritdoc */ bindDataChange: function() { // these are handlers that we only want to run when the parent module is forecasts if (!_.isUndefined(this.context.parent) && !_.isUndefined(this.context.parent.get('model'))) { if (this.context.parent.get('model').module == 'Forecasts') { this.context.parent.on('button:export_button:click', function() { if (this.layout.isVisible()) { this.exportCallback(); } }, this); // before render has happened, potentially stopping the render from happening this.before('render', this.beforeRenderCallback, this); // after render has completed this.on('render', this.renderCallback, this); this.on('list:toggle:column', function(column, isVisible, columnMeta) { // if we hide or show a column, recalculate totals this.calculateTotals(); }, this); // trigger the worksheet save draft code this.context.parent.on('button:save_draft_button:click', function() { if (this.layout.isVisible()) { // after we save, trigger the needs_commit event this.context.parent.once('forecasts:worksheet:saved', function() { // clear out the current navigation message this.setNavigationMessage(false, '', ''); this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); }, this); this.draftSaveType = 'draft'; this.saveWorksheet(true); } }, this); // trigger the worksheet save draft code and then commit the worksheet this.context.parent.on('button:commit_button:click', function() { if (this.layout.isVisible()) { // we just need to listen to it once, then we don't want to listen to it any more this.context.parent.once('forecasts:worksheet:saved', function() { this.context.parent.trigger('forecasts:worksheet:commit', this.selectedUser, this.worksheetType, this.getCommitTotals()) }, this); this.draftSaveType = 'commit'; this.saveWorksheet(false); } }, this); // On cancel click, revert any unsaved changes to rep rollup lines this.listenTo(this.context.parent, 'button:cancel_button:click', () => { if (this.layout.isVisible()) { this.collection.models.forEach(model => model.revertAttributes()); this.cleanUpDirtyModels(); this.setNavigationMessage(false, '', ''); } }); /** * trigger an event if dirty */ this.dirtyModels.on("add change reset", function(){ if(this.layout.isVisible()){ this.context.parent.trigger("forecasts:worksheet:dirty", this.worksheetType, this.dirtyModels.length > 0); } }, this); /** * Watch for a change to the selectedTimePeriod */ this.context.parent.on('change:selectedTimePeriod', function(model, changed) { this.updateSelectedTimeperiod(changed); }, this); /** * Watch for a change int he worksheet totals */ this.context.parent.on('forecasts:worksheet:totals', function(totals, type) { if (type == this.worksheetType) { var tpl = app.template.getView('recordlist.totals', this.module); this.$el.find('tfoot').remove(); this.$el.find('tbody').after(tpl(this)); } }, this); /** * Watch for a change in the selectedUser */ this.context.parent.on('change:selectedUser', function(model, changed) { this.updateSelectedUser(changed); }, this); /** * Watch for the currentForecastCommitDate to be updated */ this.context.parent.on('change:currentForecastCommitDate', function(context, changed) { if (this.layout.isVisible()) { this.checkForDraftRows(changed); } }, this); if (this.context.parent.has('collection')) { var parentCollection = this.context.parent.get('collection'); parentCollection.on('data:sync:start', function() { this.isLoadingCommits = true; }, this); parentCollection.on('data:sync:complete', function() { this.isLoadingCommits = false; }, this); } this.collection.on('data:sync:start', function() { this.isCollectionSyncing = true; // Begin sync start for buttons this.context.parent.trigger('forecasts:sync:start'); }, this); this.collection.on('data:sync:complete', function() { this.isCollectionSyncing = false; // End sync start for buttons this.context.parent.trigger('forecasts:sync:complete'); }, this); /** * When the collection is reset, we need checkForDraftRows */ this.collection.on('reset', function() { var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, false); if (this.isLoadingCommits === false) { this.checkForDraftRows(ctx.get('currentForecastCommitDate')); } }, this); this.collection.on('change:quota', function(model, changed) { // a quota has changed, trigger an event to toggle the assign quota button var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:quota_changed', this.worksheetType); }, this); this.context.parent.on('forecasts:worksheet:committed', function() { if (this.layout.isVisible()) { var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, false); this.refreshData(); // after a commit, we don't need to check for draft records again this.hasCheckedForDraftRecords = true; } }, this); this.context.parent.on('forecasts:worksheet:is_dirty', function(worksheetType, is_dirty) { if (this.worksheetType == worksheetType) { if (is_dirty) { this.setNavigationMessage(true, 'LBL_WARN_UNSAVED_CHANGES', 'LBL_WARN_UNSAVED_CHANGES'); } else { // worksheet is not dirty, this.cleanUpDirtyModels(); this.setNavigationMessage(false, '', ''); } } }, this); this.context.parent.on('button:assign_quota:click', function() { this.context.parent.once('forecasts:worksheet:saved', function() { // clear out the current navigation message this.setNavigationMessage(false, '', ''); this.context.parent.trigger('forecasts:assign_quota', this.worksheetType, this.selectedUser, this.selectedTimeperiod); }, this); app.alert.show('saving_quota', { level: 'process', title: app.lang.get('LBL_ASSIGNING_QUOTA', 'Forecasts') }); this.draftSaveType = 'assign_quota'; this.saveWorksheet(true, true); }, this); this.context.parent.on('forecasts:quota_assigned', function() { // after the quote has been re-assigned, lets refresh the data just in case. this.refreshData(); }, this); app.routing.before('route', this.beforeRouteHandler, this); $(window).bind("beforeunload." + this.worksheetType, _.bind(function() { if (!this.disposed) { var ret = this.showNavigationMessage('window'); if (_.isString(ret)) { return ret; } } }, this)); this.layout.on('hide', function() { this.hasCheckedForDraftRecords = false; }, this); } } // make sure that the dirtyModels plugin is there if (!_.isUndefined(this.dirtyModels)) { // when something gets added, the save_draft and commit buttons need to be enabled this.dirtyModels.on('add', function() { var ctx = this.context.parent || this.context; ctx.trigger('forecasts:worksheet:is_dirty', this.worksheetType, true); }, this); } /** * Listener for the list:history_log:fire event, this triggers the inline history log to display or hide */ this.context.on('list:history_log:fire', function(model, e) { // parent row var row_name = model.module + '_' + model.id; // check if the row is open, if it is, just destroy it var log_row = this.$el.find('tr[name="' + row_name + '_commit_history"]'); var field; // if we have a row, just close it and destroy the field if (log_row.length == 1) { // remove it and dispose the field log_row.remove(); // find the field field = _.find(this.fields, function(field, idx) { return (field.name == row_name + '_commit_history'); }, this); field.dispose(); } else { var rowTpl = app.template.getView('recordlist.commithistory', this.module); field = app.view.createField({ def: { 'type': 'commithistory', 'name': row_name + '_commit_history' }, view: this, model: model }); this.$el.find('tr[name="' + row_name + '"]').after(rowTpl({ module: this.module, id: model.id, placeholder: field.getPlaceholder(), colspan: this._fields.visible.length + this.leftColumns.length + this.rightColumns.length // do the +1 to account for right side Row Actions })); field.render(); } }, this); // listen for the before list:orderby to handle if the worksheet is dirty or not this.before('list:orderby', function(options) { if (this.isDirty()) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WARN_UNSAVED_CHANGES_CONFIRM_SORT', 'Forecasts'), onConfirm: _.bind(function() { this._setOrderBy(options); }, this) }); return false; } return true; }, this); // When the collection resets, recalculate the list totals and notify // the context that the totals are being initialized this.listenTo(this.collection, 'reset', function() { this.calculateTotals(true); this.context.parent.trigger('forecasts:worksheet:totals:initialized', this.totals); }); // When a value in the collection changes, recalculate the list totals this.listenTo(this.collection, 'change', function() { this.calculateTotals(); }); this.layout.on('hide', function() { this.totals = {}; }, this); // call the parent this._super("bindDataChange"); }, /** * Handles the before route event so we can show nav messages * @returns {*} */ beforeRouteHandler: function() { return this.showNavigationMessage('router'); }, /** * default navigation callback for alert message */ defaultNavCallback: function(){ this.displayNavigationMessage = false; app.router.navigate(this.targetURL, {trigger: true}); }, /** * Handle Showing of the Navigation messages if any are applicable * * @param type * @returns {*} */ showNavigationMessage: function(type, callback) { if (!_.isFunction(callback)) { callback = this.defaultNavCallback; } if (this.layout.isVisible()) { var canEdit = this.dirtyCanEdit || this.canEdit; if (canEdit && this.displayNavigationMessage) { if (type == 'window') { if (!_.isEmpty(this.routeNavigationMessage)) { return app.lang.get(this.routeNavigationMessage, 'Forecasts'); } return false; } this.targetURL = Backbone.history.getFragment(); //Replace the url hash back to the current staying page app.router.navigate(this._currentUrl, {trigger: false, replace: true}); app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get(this.navigationMessage, 'Forecasts').split('<br>'), onConfirm: _.bind(function() { callback.call(this); }, this) }); return false; } } return true; }, /** * Utility to set the Navigation Message and Flag * * @param display * @param reload_label * @param route_label */ setNavigationMessage: function(display, reload_label, route_label) { this.displayNavigationMessage = display; this.navigationMessage = reload_label; this.routeNavigationMessage = route_label; this.context.parent.trigger("forecasts:worksheet:navigationMessage", this.navigationMessage); }, /** * Custom Method to handle the refreshing of the worksheet Data */ refreshData: function() { this.displayLoadingMessage(); this.collection.fetch(); }, /** * Set the loading message and have a way to hide it */ displayLoadingMessage: function() { app.alert.show('worksheet_loading', {level: 'process', title: app.lang.get('LBL_LOADING')} ); this.collection.once('reset', function() { app.alert.dismiss('worksheet_loading'); }, this); }, /** * Handle the export callback */ exportCallback: function() { if (this.canEdit && this.isDirty()) { app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WORKSHEET_EXPORT_CONFIRM', 'Forecasts'), onConfirm: _.bind(function() { this.doExport(); }, this) }); } else { this.doExport(); } }, /** * Actually run the export */ doExport: function() { app.alert.show('massexport_loading', {level: 'process', title: app.lang.get('LBL_LOADING')}); var params = { timeperiod_id: this.selectedTimeperiod, user_id: this.selectedUser.id, platform: app.config.platform }; var url = app.api.buildURL(this.module, 'export/', null, params); app.api.fileDownload(url, { complete: function(data) { app.alert.dismiss('massexport_loading'); } }, { iframe: this.$el }); }, /** * Method for the before('render') event */ beforeRenderCallback: function() { // if manager is not set or manager == false var ret = true; if (_.isUndefined(this.selectedUser.is_manager) || this.selectedUser.is_manager == false) { ret = false; } // only render if this.selectedUser.showOpps == false which means // we want to display the manager worksheet view if (ret) { ret = !(this.selectedUser.showOpps); } // if we are going to stop render but the layout is visible if (ret === false && this.layout.isVisible()) { // hide the layout this.layout.hide(); } // Adjust the label on the quota field if the user doesn't report to any one var quotaLabel = _.isEmpty(this.selectedUser.reports_to_id) ? 'LBL_QUOTA' : 'LBL_QUOTA_ADJUSTED'; _.each(this._fields, function(fields) { _.each(fields, function(field) { if(field.name == 'quota') { field.label = quotaLabel; } }); }); // empty out the left columns this.leftColumns = []; return ret; }, /** * Method for the on('render') event */ renderCallback: function() { var user = this.selectedUser || this.context.parent.get('selectedUser') || app.user.toJSON(); if (user.is_manager && user.showOpps == false) { if (!this.layout.isVisible()) { this.layout.once('show', this.calculateTotals, this); this.layout.show(); } if (!_.isEmpty(this.totals) && this.layout.isVisible()) { var tpl = app.template.getView('recordlist.totals', this.module); this.$el.find('tfoot').remove(); this.$el.find('tbody').after(tpl(this)); } // set the commit button states to match the models this.setCommitLogButtonStates(); } else { if (this.layout.isVisible()) { this.layout.hide(); } } }, /** * Update the selected timeperiod, and run a fetch if the worksheet is visible * @param changed */ updateSelectedTimeperiod: function(changed) { if (this.displayNavigationMessage) { // save the time period just in case this.dirtyTimeperiod = this.selectedTimeperiod; } this.selectedTimeperiod = changed; if (this.layout.isVisible()) { this.refreshData(); } }, /** * Update the selected user and do a fetch if the criteria is met * @param changed */ updateSelectedUser: function(changed) { // selected user changed var doFetch = false; if (this.selectedUser.id != changed.id) { doFetch = true; } if (!doFetch && this.selectedUser.is_manager != changed.is_manager) { doFetch = true; } if (!doFetch && this.selectedUser.showOpps != changed.showOpps) { doFetch = !(changed.showOpps); } if (this.displayNavigationMessage) { // save the user just in case this.dirtyUser = this.selectedUser; this.dirtyCanEdit = this.canEdit; } this.selectedUser = changed; // Set the flag for use in other places around this controller to suppress stuff if we can't edit this.canEdit = (this.selectedUser.id == app.user.get('id')); this.cleanUpDirtyModels(); if (doFetch) { this.refreshData(); } else { if (this.selectedUser.is_manager && this.selectedUser.showOpps && this.layout.isVisible()) { // viewing managers opp worksheet so hide the manager worksheet this.layout.hide(); } } }, /** * Check the collection for any rows that may have been saved as a draft or rolled up from a reportee commit and * trigger the commit button to be enabled * * @fires forecasts:worksheet:needs_commit * @param lastCommitDate */ checkForDraftRows: function(lastCommitDate) { var isVisible = this.layout.isVisible(); if (isVisible && this.canEdit && !_.isUndefined(lastCommitDate) && this.collection.length !== 0 && this.hasCheckedForDraftRecords === false && this.isCollectionSyncing === false) { this.hasCheckedForDraftRecords = true; this.collection.find(function(item) { if (item.get('date_modified') > lastCommitDate) { this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); return true; } return false; }, this); } else if (isVisible && this.canEdit &&_.isUndefined(lastCommitDate) && !this.collection.isEmpty) { // if there is no commit date, e.g. new manager with no commits yet // but there IS data, then the commit button should be enabled this.context.parent.trigger('forecasts:worksheet:needs_commit', this.worksheetType); } else if (isVisible === false && this.canEdit && this.hasCheckedForDraftRecords === false) { // since the layout is not visible, lets wait for it to become visible this.layout.once('show', function() { this.checkForDraftRows(lastCommitDate); }, this); } else if (this.isCollectionSyncing === true) { this.collection.once('data:sync:complete', function() { this.checkForDraftRows(lastCommitDate); }, this); } }, /** * Handles setting the proper state for the CommitLog Buttons in the row-actions */ setCommitLogButtonStates: function() { _.each(this.fields, function(field) { if (field.def.event === 'list:history_log:fire') { // we have a field that needs to be disabled, so disable it! field.setDisabled((field.model.get('show_history_log') == "0")); if ((field.model.get('show_history_log') == "0")) { field.$el.find("a.rowaction").attr( "data-original-title", app.lang.get("LBL_NO_COMMIT", "ForecastManagerWorksheets") ); } } }); }, /** * Override the sync method so we can put out custom logic in it * * @param method * @param model * @param options */ sync: function(method, model, options) { if (!_.isUndefined(this.context.parent) && !_.isUndefined(this.context.parent.get('selectedUser'))) { var sl = this.context.parent.get('selectedUser'); if (sl.is_manager == false) { // they are not a manager, we should always hide this if it's not already hidden if (this.layout.isVisible()) { this.layout.hide(); } return; } } var callbacks, url; options = options || {}; options.params = options.params || {}; if (!_.isUndefined(this.selectedUser.id)) { options.params.user_id = this.selectedUser.id; } if (!_.isEmpty(this.selectedTimeperiod)) { options.params.timeperiod_id = this.selectedTimeperiod; } options.limit = 1000; options = app.data.parseOptionsForSync(method, model, options); // custom success handler options.success = _.bind(function(data) { this.collectionSuccess(data); }, this); callbacks = app.data.getSyncCallbacks(method, model, options); this.collection.trigger("data:sync:start", method, model, options); url = app.api.buildURL("ForecastManagerWorksheets", null, null, options.params); app.api.call("read", url, null, callbacks); }, /** * Method to handle the success of a collection call to make sure that all reportee's show up in the table * even if they don't have data for the user that is asking for it. * * @param data */ collectionSuccess: function(data) { var records = [], users = $.map(this.selectedUser.reportees, function(obj) { return $.extend(true, {}, obj); }); // put the selected user on top users.unshift({id: this.selectedUser.id, name: this.selectedUser.full_name}); // get the base currency var currency_id = app.currency.getBaseCurrencyId(), currency_base_rate = app.metadata.getCurrency(app.currency.getBaseCurrencyId()).conversion_rate; _.each(users, function(user) { var row = _.find(data, function(rec) { return (rec.user_id == this.id) }, user); if (!_.isUndefined(row)) { // update the name on the row as this will have the correct formatting for the locale row.name = user.name; } else { row = _.clone(this.defaultValues); row.currency_id = currency_id; row.base_rate = currency_base_rate; row.user_id = user.id; row.assigned_user_id = this.selectedUser.id; row.draft = (this.selectedUser.id == app.user.id) ? 1 : 0; row.name = user.name; } if (_.isEmpty(row.id)) { row.id = app.utils.generateUUID(); row.fakeId = true; } records.push(row); }, this); if (!_.isUndefined(this.orderBy)) { // lets sort the collection if (this.orderBy.field !== 'name') { records = _.sortBy(records, function(item) { // typecast values to Number since it's not the 'name' // column (the only string value in the manager worksheet) var val = +item[this.orderBy.field]; if (this.orderBy.direction == "desc") { return -val; } else { return val; } }, this); } else { // we have the name records.sort(_.bind(function(a, b) { if (this.orderBy.direction == 'asc') { if (a.name.toString() < b.name.toString()) return 1; if (a.name.toString() > b.name.toString()) return -1; } else { if (a.name.toString() < b.name.toString()) return -1; if (a.name.toString() > b.name.toString()) return 1; } return 0; }, this)); } } this.collection.isEmpty = (_.isEmpty(data)); this.collection.reset(records); }, /** * Calculates the display totals for the worksheet * * @param {boolean} reset true when being called on a reset * @fires forecasts:worksheet:totals */ calculateTotals: function(reset = false) { if (this.layout.isVisible() || reset) { this.totals = this.getCommitTotals(); this.totals['display_total_label_in'] = _.first(this._fields.visible).name; _.each(this._fields.visible, function(field) { this.totals[field.name + '_display'] = true; }, this); var ctx = this.context.parent || this.context; // fire an event on the parent context ctx.trigger('forecasts:worksheet:totals', this.totals, this.worksheetType); } }, /** * Gets the numbers needed for a commit * * @returns {{quota: number, best_case: number, best_adjusted: number, likely_case: number, likely_adjusted: number, worst_case: number, worst_adjusted: number, included_opp_count: number, pipeline_opp_count: number, pipeline_amount: number, closed_amount: number, closed_count: number}} */ getCommitTotals: function() { var quota = 0, best_case = 0, best_case_adjusted = 0, likely_case = 0, likely_case_adjusted = 0, worst_case_adjusted = 0, worst_case = 0, included_opp_count = 0, pipeline_opp_count = 0, pipeline_amount = 0, closed_amount = 0; this.collection.forEach(function(model) { var base_rate = parseFloat(model.get('base_rate')), mPipeline_opp_count = model.get("pipeline_opp_count"), mPipeline_amount = model.get("pipeline_amount"), mClosed_amount = model.get("closed_amount"), mOpp_count = model.get("opp_count"); quota = app.math.add(app.currency.convertWithRate(model.get('quota'), base_rate), quota); best_case = app.math.add(app.currency.convertWithRate(model.get('best_case'), base_rate), best_case); best_case_adjusted = app.math.add(app.currency.convertWithRate(model.get('best_case_adjusted'), base_rate), best_case_adjusted); likely_case = app.math.add(app.currency.convertWithRate(model.get('likely_case'), base_rate), likely_case); likely_case_adjusted = app.math.add(app.currency.convertWithRate(model.get('likely_case_adjusted'), base_rate), likely_case_adjusted); worst_case = app.math.add(app.currency.convertWithRate(model.get('worst_case'), base_rate), worst_case); worst_case_adjusted = app.math.add(app.currency.convertWithRate(model.get('worst_case_adjusted'), base_rate), worst_case_adjusted); included_opp_count += (_.isUndefined(mOpp_count)) ? 0 : parseInt(mOpp_count); pipeline_opp_count += (_.isUndefined(mPipeline_opp_count)) ? 0 : parseInt(mPipeline_opp_count); if (!_.isUndefined(mPipeline_amount)) { pipeline_amount = app.math.add(pipeline_amount, mPipeline_amount); } if (!_.isUndefined(mClosed_amount)) { closed_amount = app.math.add(closed_amount, mClosed_amount); } }); return { 'quota': quota, 'best_case': best_case, 'best_adjusted': best_case_adjusted, 'likely_case': likely_case, 'likely_adjusted': likely_case_adjusted, 'worst_case': worst_case, 'worst_adjusted': worst_case_adjusted, 'included_opp_count': included_opp_count, 'pipeline_opp_count': pipeline_opp_count, 'pipeline_amount': pipeline_amount, 'closed_amount': closed_amount, 'closed_count': (included_opp_count - pipeline_opp_count) }; }, /** * We have to overwrite this method completely, since there is currently no way to completely disable * a field from being displayed * * @returns {{default: Array, available: Array, visible: Array, options: Array}} */ parseFields: function() { var catalog = this._super("parseFields"); _.each(catalog, function(group, i) { if (_.isArray(group)) { catalog[i] = _.filter(group, function(fieldMeta) { return app.utils.getColumnVisFromKeyMap(fieldMeta.name, 'forecastsWorksheetManager'); }); } else { // _byId is an Object and _.filter returns data in Array form // so just go through _byId this way _.each(group, function(fieldMeta) { if (!app.utils.getColumnVisFromKeyMap(fieldMeta.name, 'forecastsWorksheetManager')) { delete group[fieldMeta.name]; } }); } }); return catalog; }, /** * Call the worksheet save event * * @fires forecasts:worksheet:saved * @param {bool} isDraft * @param {bool} [suppressMessage] * @returns {number} */ saveWorksheet: function(isDraft, suppressMessage) { // only run the save when the worksheet is visible and it has dirty records var saveObj = { totalToSave: 0, saveCount: 0, model: undefined, isDraft: isDraft, suppressMessage: suppressMessage || false, timeperiod: this.dirtyTimeperiod, userId: this.dirtyUser }, ctx = this.context.parent || this.context; if (this.layout.isVisible()) { if (_.isUndefined(saveObj.userId)) { saveObj.userId = this.selectedUser; } saveObj.userId = saveObj.userId.id; /** * If the sheet is dirty, save the dirty rows. Else, if the save is for a commit, and we have * draft models (things saved as draft), we need to resave those as committed (version 1). If neither * of these conditions are true, then we need to fall through and signal that the save is complete so other * actions listening for this can continue. */ if (this.isDirty()) { saveObj.totalToSave = this.dirtyModels.length; if (saveObj.suppressMessage === false) { app.alert.show('save_worksheet_alert', { level: 'process', title: app.lang.get('LBL_SAVING'), autoClose: false }); } this.dirtyModels.each(function(model) { saveObj.model = model; this._worksheetSaveHelper(saveObj, ctx); }, this); this.cleanUpDirtyModels(); } else { if (isDraft && saveObj.suppressMessage === false) { app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get("LBL_FORECASTS_WIZARD_SUCCESS_TITLE", "Forecasts") + ":", messages: [app.lang.get("LBL_FORECASTS_WORKSHEET_SAVE_DRAFT_SUCCESS", "Forecasts")] }); } ctx.trigger('forecasts:worksheet:saved', saveObj.totalToSave, this.worksheetType, isDraft); } } this.draftSaveType = undefined; return saveObj.totalToSave }, /** * Helper function for worksheet save * * @fires forecasts:worksheet:saved */ _worksheetSaveHelper: function(saveObj, ctx) { var id = (saveObj.model.get('fakeId')) ? null : saveObj.model.get('id'); saveObj.model.set({ id: id, // we have to set the id back to null if ID is not set // so when the xhr runs it knows it's a new model and will use // POST vs PUT current_user: saveObj.userId || this.selectedUser.id, timeperiod_id: saveObj.timeperiod || this.selectedTimeperiod, draft_save_type: this.draftSaveType }, {silent: true}); saveObj.model.save({}, { complete: () => { saveObj.saveCount++; if (saveObj.totalToSave === saveObj.saveCount) { if (saveObj.isDraft && saveObj.suppressMessage === false) { app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: `${app.lang.get('LBL_FORECASTS_WIZARD_SUCCESS_TITLE', 'Forecasts')}`, messages: [app.lang.get('LBL_FORECASTS_WORKSHEET_SAVE_DRAFT_SUCCESS', 'Forecasts')] }); } ctx.trigger('forecasts:worksheet:saved', saveObj.totalToSave, this.worksheetType, saveObj.isDraft); app.alert.dismiss('save_worksheet_alert'); } }, silent: true, alerts: { success: false } }); }, /** * @inheritdoc */ _dispose: function() { if (!_.isUndefined(this.context.parent) && !_.isNull(this.context.parent)) { this.context.parent.off(null, null, this); if (this.context.parent.has('collection')) { this.context.parent.get('collection').off(null, null, this); } } app.routing.offBefore('route', this.beforeRouteHandler, this); $(window).off(`beforeunload.${this.worksheetType}`); this.stopListening(); this._super('_dispose'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "VisualPipeline":{"fieldTemplates": { "base": { "header-values": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.VisualPipeline.HeaderValuesField * @alias SUGAR.App.view.fields.BaseVisualPipelineHeaderValuesField * @extends View.Fields.Base.BaseField */ ({ // Header-values FieldTemplate (base) extendsFrom: 'BaseField', /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:table_header', this.render, this); }, /** * @inheritdoc */ _render: function() { if (!_.isEmpty(this.context)) { this.context.set('selectedValues', {}); } this.populateHeaderValues(); this._super('_render'); this.handleDraggableActions(); }, /** * Populates the whitelist and blacklist sections based on the hidden_values config */ populateHeaderValues: function() { var tableHeader = this.model.get('table_header'); var module = this.model.get('enabled_module'); var fields = app.metadata.getModule(module, 'fields'); var translated = app.lang.getAppListStrings((fields[tableHeader] || {}).options); translated = _.pick(translated, _.identity); // remove empty values if (!_.isEmpty(tableHeader) && _.isEmpty(translated)) { // call enum api app.api.enumOptions(module, tableHeader, { success: _.bind(function(data) { if (!this.disposed) { this._createHeaderValueLists(tableHeader, data); this._super('_render'); this.handleDraggableActions(); } }, this) }); } this._createHeaderValueLists(tableHeader, translated); }, /** * Creates whitelist and blacklist of header values. * * @param {string} tableHeader Header name * @param {Array} translated List of options */ _createHeaderValueLists: function(tableHeader, translated) { var whiteListedKeys = []; var blackListedKeys = []; var whiteListed = []; var blackListed = []; var availableItems = { [tableHeader]: {} }; if (!_.isEmpty(tableHeader) && !_.isEmpty(translated)) { let availableSortValues = this._getAvailableColumnNames(tableHeader); let allAvailableValues = availableSortValues || this.model.get('available_columns') || {}; let availableValues = allAvailableValues[tableHeader] || {}; if (_.isArray(availableValues)) { /* there are some data stored in the DB as an array with simple numeric indices, so we have to work with values and not keys */ whiteListedKeys = _.intersection(_.values(availableValues), _.values(translated)); blackListedKeys = _.difference(_.values(translated), _.values(availableValues)); } else { /* due to the variety in the spelling of some number-like keys in DB ('0', 0, '00'), the comparison functions may not work correctly, so unification is needed */ let keysAvailabled = _.keys(availableValues).map(key => isNaN(key) ? key : Number(key)); let keysTranslated = _.keys(translated).map(key => isNaN(key) ? key : Number(key)); whiteListedKeys = _.intersection(keysAvailabled, keysTranslated); blackListedKeys = _.difference(keysTranslated, keysAvailabled); } _.each(whiteListedKeys, key => { let val = _.isArray(availableValues) ? key : translated[key]; whiteListed.push({ key: key, translatedLabel: val }); availableItems[tableHeader][key] = val; }); _.each(blackListedKeys, key => { let val = _.isArray(availableValues) ? key : translated[key]; blackListed.push({ key: key, translatedLabel: val }); }); } this.model.set('available_columns_edited', availableItems); this.model.set('hidden_values', blackListedKeys); this.model.set({ 'white_listed_header_vals': whiteListed, 'black_listed_header_vals': blackListed }); }, /** * Handles the dragging of the items from the white list to the black list section */ handleDraggableActions: function() { this.$('#pipeline-sortable-1, #pipeline-sortable-2').sortable({ connectWith: '.connectedSortable', update: _.bind(function(evt, ui) { let whiteListed = this._getWhiteListedArray(); let $item = $(ui.item); let moduleName = $item.closest('.header-values-wrapper').data('modulename'); let model = _.find(this.collection.models, function(item) { if (item.get('enabled_module') === moduleName) { return item; } }); if (_.isArray(whiteListed)) { model.set('available_values', whiteListed); this._getAvailableColumnNames(model.get('table_header')); } }, this), receive: _.bind(function(event, ui) { var $item = $(ui.item); var movedItem = $item.data('headervalue'); var movedInColumn = $item.parent().data('columnname'); var moduleName = $item.closest('.header-values-wrapper').data('modulename'); var model = _.find(this.collection.models, function(item) { if (item.get('enabled_module') === moduleName) { return item; } }); var blackListed = this.getBlackListedArray(); let whiteListed = this._getWhiteListedArray(); if (movedInColumn === 'black_list') { blackListed.push(movedItem); whiteListed = whiteListed.filter(item => item !== movedItem); } if (movedInColumn === 'white_list') { whiteListed.push(movedItem); var index = _.indexOf(blackListed, movedItem); if (index > -1) { blackListed.splice(index, 1); } } if (blackListed instanceof Array) { model.set('hidden_values', blackListed); } if (_.isArray(whiteListed)) { model.set('available_values', whiteListed); this._getAvailableColumnNames(model.get('table_header')); } }, this) }); }, /** * Return the list of fields that are black listed based on the hidden_value config * @return {Array} The black listed fields */ getBlackListedArray: function() { var blackListed = this.model.get('hidden_values'); if (_.isEmpty(blackListed)) { blackListed = []; } if (!(blackListed instanceof Array)) { blackListed = JSON.parse(blackListed); } return blackListed; }, /** * Return the list of fields that are white listed * @return {Array} The white listed fields * @private */ _getWhiteListedArray: function() { let whiteListed = []; let $elemList = this.$('#pipeline-sortable-1 li'); _.each($elemList, itemElem => { let key = $(itemElem).data('headervalue'); whiteListed[key] = itemElem.innerText.trim(); }); return whiteListed; }, /** * Gets the list of all the available columns in the exact order * * @param {string} tableHeader Header name * @return {Object|null} List of available whitelisted column names * @private */ _getAvailableColumnNames: function(tableHeader) { let availableColumns = this.model.get('available_values'); if (!availableColumns) { return null; } let availableColumnsEdited = this._setAvailableColumnsEdited(tableHeader, availableColumns); return availableColumnsEdited; }, /** * Sets in the model the initial whitelist for columns * * @param {string} tableHeader Header name * @param {Array} availableColumns Available columns * @return {Object|null} List of available whitelisted column names * @private */ _setAvailableColumnsEdited: function(tableHeader, availableColumns) { let availableColumnsEdited = { [tableHeader]: {} }; for (let key in availableColumns) { availableColumnsEdited[tableHeader][key] = availableColumns[key]; } this.model.set('available_columns_edited', availableColumnsEdited); return availableColumnsEdited; }, /** * @inheritdoc */ _dispose: function() { this.model.off('change:table_header', null, this); this._super('_dispose'); } }) }, "table-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.VisualPipeline.TableHeaderField * @alias SUGAR.App.view.fields.BaseVisualPipelineTableHeaderField * @extends View.Fields.Base.EnumField */ ({ // Table-header FieldTemplate (base) extendsFrom: 'EnumField', /** * The name of the fields that should be excluded from the * Tile View header options. */ excludedTileHeaderOptions: ['commentlog'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); var items = {}; var tabContent = this.model.get('tabContent'); if (_.isEmpty(tabContent)) { tabContent = this.getTabContent(this.model.get('enabled_module')); } if (options.def.name === 'table_header') { items = tabContent.dropdownFields; } if (options.def.name === 'tile_body_fields' || options.def.name === 'tile_header') { items = _.omit(tabContent.fields, this.excludedTileHeaderOptions); } if (options.def.name === 'total_field') { items = tabContent.allTotalableFields; } this.items = items; var optionsBody = this.model.get('tile_body_fields'); if (!_.isEmpty(optionsBody)) { //Transform the tile_body back to array if it isn't already. if (false === optionsBody instanceof Array) { var parsedOptions = JSON.parse(optionsBody); this.model.set('tile_body_fields', parsedOptions); } } }, /** * @inheritdoc */ _render: function() { if (this.def.name === 'total_field' && _.isEmpty(this.model.get('tabContent').allTotalableFields)) { return; } this._super('_render'); }, /** * Retrieves the content of the tab for the module * @param {string} changes Object containing the changes for the fields of an update activity message * @return {Object} The tab content */ getTabContent: function(module) { var content = {}; var dropdownFields = {}; var allFields = {}; var fields = app.metadata.getModule(module, 'fields'); _.each(fields, function(field) { if (_.isObject(field)) { var label = field.vname || field.label; if (!_.isEmpty(app.lang.getModString(label, module))) { allFields[field.name] = app.lang.getModString(label, module); if (field.type === 'enum') { dropdownFields[field.name] = app.lang.getModString(label, module); } } } }); content.dropdownFields = dropdownFields; content.fields = allFields; return content; } }) }, "modules-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.VisualPipeline.ModulesListField * @alias SUGAR.App.view.fields.BaseVisualPipelineModulesListField * @extends View.Fields.Base.EnumField */ ({ // Modules-list FieldTemplate (base) extendsFrom: 'EnumField', plugins: ['EllipsisInline'], /** * HTML tag of the append tag checkbox. * * @property {string} */ appendTagInput: 'input[name=append_tag]', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); var items = {}; if (options.def.name === 'enabled_modules') { items = this.context.get('allowedModules'); } if (options.def.name === 'tile_body_fields') { var tabContent = this.model.get('tabContent'); items = tabContent.fields; } this.items = items; }, /** * @inheritdoc */ getSelect2Options: function(optionsKeys) { optionsKeys = this._super('getSelect2Options', [optionsKeys]); if (this.name === 'enabled_modules') { optionsKeys.formatSelection = this.formatSelect2Selection; optionsKeys.formatResult = this.formatSelect2Result; } return optionsKeys; }, /** * Formats a dropdown selection * * @param {Object} state The id and text object * @return {string} HTML to use for the item */ formatSelect2Selection: function(state) { return '<span class="enabled-module-item" data-module="' + _.escape(state.id) + '">' + _.escape(state.text) + '</span>'; }, /** * Formats a dropdown result * * @param {Object} state The id and text object * @return {string} HTML to use for the item */ formatSelect2Result: function(state) { return '<span class="enabled-module-result-item" data-module="' + _.escape(state.id) + '">' + _.escape(state.text) + '</span>'; }, /** * @inheritdoc */ _render: function() { this.items = this._sortModuleNamesAlphabetical(this.items); this._super('_render'); if (this.name === 'enabled_modules') { this.attachEvents(); } if (this.name === 'enabled_modules') { // add the data module to the li _.each(this.$('.select2-search-choice'), function(el) { var $el = $(el); $el.attr('data-module', $el.find('span').data('module')); $el.addClass('enabled-module-item'); }, this); } }, /** * Set up events for the field to add and remove items from the collection. */ attachEvents: function() { this.handleRemoveItemHandler = _.bind(this._handleRemoveItemFromCollection, this); this.handleAddItemHandler = _.bind(this._handleAddItemToCollection, this); this.$el.on('select2-removed', this.handleRemoveItemHandler); this.$el.on('select2-selecting', this.handleAddItemHandler); }, /** * Handles triggering the removal of the model from the collection */ _handleRemoveItemFromCollection: function(e) { if (!_.isEmpty(e.val)) { this.context.trigger('pipeline:config:model:remove', e.val); } }, /** * Handles triggering for adding a model from the collection */ _handleAddItemToCollection: function(e) { if (!_.isEmpty(e.val)) { this.context.trigger('pipeline:config:model:add', e.val); } }, /** * @inheritdoc */ _dispose: function() { this.$el.off('select2-removed', this.handleRemoveItemHandler); this.$el.off('select2-selecting', this.handleAddItemHandler); this._super('_dispose'); }, /** * Sorts module names object by property value as name of module * * @param {Object} obj * @return {Object} * @private */ _sortModuleNamesAlphabetical: function(obj) { let sortedKeys = Object.keys(obj).sort((a, b) => obj[a].localeCompare(obj[b])); let sortedObj = {}; for (let i = 0; i < sortedKeys.length; i++) { sortedObj[sortedKeys[i]] = obj[sortedKeys[i]]; } return sortedObj; } }) } }} , "views": { "base": { "config-visual-pipeline": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.VisualPipeline.ConfigPanelView * @alias SUGAR.App.view.views.BaseVisualConfigPanelView * @extends View.Fields.Base.BaseField */ ({ // Config-visual-pipeline View (base) extendsFrom: 'BaseConfigPanelView', selectedModules: [], activeTabIndex: 0, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.customizeMetaFields(); }, /** * @inheritdoc */ bindDataChange: function() { this.collection.on('add remove reset', this.render, this); this.collection.on('change:show_column_total', this._toggleDepedent, this); this.listenTo(this.context, 'pipeline:config:set-active-module', this._setupActiveModule); }, /** * @inheritdoc */ render: function() { this._super('render'); //event used in tile preview this.context.trigger('pipeline:config:tabs-initialized'); }, /** * @inheritdoc * When rendering fields, handle the state of the axis labels */ _renderField: function(field) { let noTotalableFields = _.isEmpty(_.first(this.activeModule).get('tabContent').allTotalableFields); if (field.name === 'show_column_total_options' && noTotalableFields) { return; } this._super('_renderField', [field]); // manage display state of fieldsets with toggle this._toggleDepedent(); }, /** * Adds the fields to the module into a two column layout */ customizeMetaFields: function() { var twoColumns = []; var customizedFields = []; // To use as row in the UI _.each(this.meta.panels, function(panel) { _.each(panel.fields, function(field) { if (field.twoColumns) { twoColumns.push(field); if (twoColumns.length === 2) { customizedFields.push(twoColumns); twoColumns = []; } } else { customizedFields.push([field]); } }, this); }, this); this.meta.customizedFields = customizedFields; }, /** * Set active module and render * * @param {string} activeModule */ _setupActiveModule: function(activeModule) { this.activeModule = _.filter(this.collection.models, function(model) { model.set('selectedModule', model.get('enabled_module') === activeModule); return model.get('enabled_module') === activeModule; }); this.render(); }, /** * Handle the conditional display of settings input field based on checkbox toggle state */ _toggleDepedent: function() { const checkboxField = this.getField('show_column_total').$('input'); this.getField('total_field').setDisabled(!checkboxField.prop('checked')); }, }) }, "pipeline-modules": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.VisualPipeline.PipelineModulesView * @alias SUGAR.App.view.views.BaseVisualPipelinePipelineModulesView * @extends View.Fields.Base.ConfigPanelView */ ({ // Pipeline-modules View (base) extendsFrom: 'BaseConfigPanelView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.collection = this.layout.collection || app.data.createBeanCollection(this.module); this.allowedModules = this.context.get('allowedModules'); } }) }, "config-preview-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.VisualPipeline.ConfigPreviewContentView * @alias SUGAR.App.view.views.BaseVisualConfigPreviewContentView * @extends View.Fields.Base.BaseField */ ({ // Config-preview-content View (base) /** * @inheritdoc */ bindDataChange: function() { // events based on which we have to re-render the preview this.model.on('change', this.render, this); this.context.on('pipeline:config:tabs-initialized', this.setupTabChange, this); this.collection.on('change', this.render, this); }, /** * Sets a listener on the module settings tab in the main content for * render the view when a tab is changed */ setupTabChange: function() { var content = this.closestComponent('side-pane').layout; let tabControls = content.$('#tabs select.module-selection'); _.each(tabControls, function(el) { $(el).on('change', _.bind(this.render, this)); }, this); }, /** * Removes listeners on the module settings tab in the main content * on dispose */ removeTabChangeEvents: function() { var content = this.closestComponent('side-pane').layout; var tabControls = content.$('#tabs li.tab'); _.each(tabControls, function(el) { $(el).off('click'); }, this); }, /** * @inheritdoc */ render: function() { //get the currently active tab var content = this.closestComponent('side-pane').layout; let currentTab = content.$('#tabs select.module-selection').val(); //get the model shown in the current tab var currentModel = _.find(this.collection.models, function(model) { if (model.get('enabled_module') === currentTab) { return model; } }, this); //we will use this object in the preview this.previewModel = {}; this.currentModel = currentModel; //if we have a currently selected model extract the information we want to show in the preview if (!_.isUndefined(currentModel)) { this.previewModel.moduleName = currentModel.get('enabled_module'); this.previewModel.tile_header = this.getFieldLabel(currentModel.get('tile_header')); this.previewModel.tile_body_fields = []; _.each(currentModel.get('tile_body_fields'), function(fieldName) { this.previewModel.tile_body_fields.push(this.getFieldLabel(fieldName)); }, this); this._super('render'); } }, /** * Returns the label value of a field based on the currently selected module * @return {string} The label of a field */ getFieldLabel: function(fieldName) { var fields = app.metadata.getModule(this.previewModel.moduleName, 'fields'); var label = ''; _.each(fields, function(field) { if (_.isObject(field) && field.name === fieldName) { label = field.vname || field.label; return label; } }); return label; }, /** * Remove the tab events * @inheritdoc */ _dispose: function() { this.removeTabChangeEvents(); this._super('_dispose'); } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.VisualPipeline.ConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseVisualPipelineConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', plugins: ['ErrorDecoration'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._viewAlerts = []; this.moduleLangObj = { // using "Tile View" for config title module: app.lang.get('LBL_PIPELINE_VIEW_NAME', this.module) }; }, /** * Displays alert message for invalid models */ showInvalidModel: function() { var self = this; if (!this instanceof app.view.View) { app.logger.error('This method should be invoked by Function.prototype.call(), passing in as ' + 'argument an instance of this view.'); return; } var errorMsg = app.lang.get('LBL_PIPELINE_ERR_VALIDATION_FAILED', self.module); _.each(this.validatedModels, function(model) { if (!model.isValid) { errorMsg += '<li>' + model.moduleName + '</li>'; } }); var name = 'invalid-data'; self._viewAlerts.push(name); app.alert.show(name, { level: 'error', messages: errorMsg }); }, /** * @inheritdoc */ cancelConfig: function() { if (this.triggerBefore('cancel')) { if (app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } app.router.navigate('#Administration', {trigger: true}); } }, /** * Process all the models of the collection and prepares the context * bean for save action */ _setupSaveConfig: function() { var ctxModel = this.context.get('model'); var enabledModules = ctxModel.get('enabled_modules'); var tableHeader = {}; var tileHeader = {}; var tileBodyFields = {}; var recordsPerColumn = {}; var hiddenValues = {}; var availableColumns = {}; let showColumnCount = {}; let showColumnTotal = {}; let totalField = {}; _.each(this.collection.models, function(model) { var moduleName = model.get('enabled_module'); tableHeader[moduleName] = model.get('table_header'); tileHeader[moduleName] = model.get('tile_header'); tileBodyFields[moduleName] = model.get('tile_body_fields'); recordsPerColumn[moduleName] = model.get('records_per_column'); hiddenValues[moduleName] = model.get('hidden_values'); showColumnCount[moduleName] = model.get('show_column_count'); showColumnTotal[moduleName] = model.get('show_column_total'); totalField[moduleName] = model.get('total_field'); availableColumns[moduleName] = model.get('available_columns_edited') || model.get('available_columns'); }, this); ctxModel.set({ is_setup: true, enabled_modules: enabledModules, table_header: tableHeader, tile_header: tileHeader, tile_body_fields: tileBodyFields, records_per_column: recordsPerColumn, hidden_values: hiddenValues, available_columns: availableColumns, show_column_count: showColumnCount, show_column_total: showColumnTotal, total_field: totalField }, {silent: true}); }, /** * Calls the context model save and saves the config model in case * the default model save needs to be overwritten * * @protected */ _saveConfig: function() { this.validatedModels = []; this.getField('save_button').setDisabled(true); if (this.collection.models.length === 0) { this._setupSaveConfig(); this._super('_saveConfig'); } else { async.waterfall([ _.bind(this.validateCollection, this) ], _.bind(function(result) { this.validatedModels.push(result); // doValidate() has finished on all models. if (this.collection.models.length === this.validatedModels.length) { var found = _.find(this.validatedModels, function(details) { return details.isValid === false; }); if (found) { this.showInvalidModel(); this.getField('save_button').setDisabled(false); } else { this._setupSaveConfig(); this._super('_saveConfig'); } } }, this)); } }, /** * Validates all the models in the collection using the validation tasks */ validateCollection: function(callback) { var fieldsToValidate = {}; var allFields = this.getFields(this.module, this.model); for (var fieldKey in allFields) { if (app.acl.hasAccessToModel('edit', this.model, fieldKey)) { _.extend(fieldsToValidate, _.pick(allFields, fieldKey)); } } // Clear errors from any previous validation runs first this.clearValidationErrors(this.getFieldNames()); _.each(this.collection.models, function(model) { model.doValidate(fieldsToValidate, function(isValid) { var moduleName = app.lang.getModuleName(model.get('enabled_module'), {plural: true}); callback({modelId: model.id, isValid: isValid, moduleName: moduleName}); }); }, this); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.VisualPipelineConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseVisualPipelineConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'BaseConfigDrawerLayout', plugins: ['ErrorDecoration'], fieldsAllowedInTileBody: 5, // Use a number <= than 0 to disable the 'Number of fields allowed in a tile' check. /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setAllowedModules(); this.moduleLangObj = { // using "Tile View" for error messages module: app.lang.get('LBL_PIPELINE_VIEW_NAME', this.module) }; }, /** * @inheritdoc */ bindDataChange: function() { this.context.on('pipeline:config:model:add', this.addModelToCollection, this); this.context.on('pipeline:config:model:remove', this.removeModelFromCollection, this); }, /** * Returns the list of modules the user has access to * and are supported. * * @return {Array} The list of module names. */ getAvailableModules: function() { var selectedModules = this.model.get('enabled_modules'); return _.filter(selectedModules, function(module) { return !_.isEmpty(app.metadata.getModule(module)); }); }, /** * Sets the list of modules the user has no access to on the model. */ setNotAvailableModules: function() { let modules = app.metadata.getModuleNames({ filter: 'display_tab', access: 'read' }); let notAvailableModules = _.reject(modules, function(module) { let moduleDetail = app.metadata.getModule(module); return !_.isEmpty(moduleDetail) && !moduleDetail.isPipelineExcluded && this._checkDropdownField(moduleDetail.fields); }, this); this.model.set('notAvailableModules', notAvailableModules); }, /** * Sets up the models for each of the enabled modules from the configs */ loadData: function(options) { if (!this.checkAccess()) { this.blockModule(); return; } var availableModules = this.getAvailableModules(); var tableHeaders = this.model.get('table_header'); var tileHeaders = this.model.get('tile_header'); var tileBodyFields = this.model.get('tile_body_fields'); var recordsPerColumn = this.model.get('records_per_column'); var hiddenValues = this.model.get('hidden_values'); var availableColumns = this.model.get('available_columns'); let showColumnCount = this.model.get('show_column_count') || {}; let showColumnTotal = this.model.get('show_column_total') || {}; let totalField = this.model.get('total_field') || {}; if (!(recordsPerColumn instanceof Object)) { recordsPerColumn = JSON.parse(recordsPerColumn); } _.each(availableModules, function(moduleName) { var data = { enabled: true, enabled_module: moduleName, table_header: tableHeaders[moduleName], tile_header: tileHeaders[moduleName], tile_body_fields: tileBodyFields[moduleName], records_per_column: recordsPerColumn[moduleName], hidden_values: hiddenValues[moduleName], available_columns: availableColumns[moduleName], show_column_count: showColumnCount[moduleName], show_column_total: showColumnTotal[moduleName], total_field: totalField[moduleName], }; this.addModelToCollection(moduleName, data); }, this); this.setNotAvailableModules(); this.setActiveTabIndex(0); }, /** * Checks VisualPipeline ACLs to see if the User is a system admin * or if the user has a developer role for the VisualPipeline module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().VisualPipeline; var isSysAdmin = (app.user.get('type') == 'admin'); var isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isDev); }, /** * Sets the allowed modules that the admin are allowed to configure */ setAllowedModules: function() { var moduleDetails = {}; let allowedModules = app.metadata.getModuleNames({ filter: 'display_tab', access: 'read' }); var modules = {}; _.each(allowedModules, function(module) { moduleDetails = app.metadata.getModule(module); if (moduleDetails && !moduleDetails.isBwcEnabled && !_.isEmpty(moduleDetails.fields) && !moduleDetails.isPipelineExcluded && this._checkDropdownField(moduleDetails.fields)) { modules[module] = app.lang.getAppListStrings('moduleList')[module]; } }, this); this.context.set('allowedModules', modules); }, /** * Function to check if the module has any dropdown type fields * * @param {Object} fields * @return {boolean} */ _checkDropdownField: function(fields) { for (const property in fields) { if (fields[property].type === 'enum') { return true; } } return false; }, /** * Sets the active tab */ setActiveTabIndex: function(index) { if (this.collection.length >= 1 || !_.isUndefined(index)) { var activeIndex = !_.isUndefined(index) ? index : this.collection.length - 1; this.context.set('activeTabIndex', activeIndex); } }, /** * Removes a model from the collection and triggers events * to re-render the components * @param {string} module Module Name */ removeModelFromCollection: function(module) { var modelToDelete = _.find(this.collection.models, function(model) { return model.get('enabled_module') === module; }); if (!_.isEmpty(modelToDelete)) { this.collection.remove(modelToDelete); this.setActiveTabIndex(); } }, /** * Adds a model from the collection and triggers events * to re-render the components * @param {string} module Module Name * @param {Object} data Model data to add to the collection */ addModelToCollection: function(module, data) { var data = data || {}; var existingBean = _.find(this.collection.models, function(model) { if (_.contains(_.keys(this.context.get('allowedModules')), module)) { return model.get('enabled_module') === module; } }, this); if (_.isEmpty(existingBean)) { var bean = app.data.createBean(this.module, { enabled: data.enabled || true, enabled_module: data.module || module, table_header: data.table_header || '', tile_header: data.tile_header || '', tile_body_fields: data.tile_body_fields || '', records_per_column: data.records_per_column || '', hidden_values: data.hidden_values || '', available_columns: data.available_columns || '', show_column_count: data.show_column_count || '', show_column_total: data.show_column_total || '', total_field: data.show_column_total ? (data.total_field || '') : '', }); this.getModuleFields(bean); this.addValidationTasks(bean); this.collection.add(bean); } this.setActiveTabIndex(); this.context.trigger('pipeline:config:set-active-module', module); }, /** * Set the fields for the module on the bean * * @param {Object} bean ta Model data to add to the collection */ getModuleFields: function(bean) { var module = bean.get('enabled_module'); var content = {}; var dropdownFields = {}; var allFields = {}; let allTotalableFields = {}; var studioFields = []; var metaFields = app.metadata.getModule(module) ? app.metadata.getModule(module).fields : {}; const totalableFields = ['int', 'float', 'decimal', 'currency']; _.each(metaFields, function(metaField) { if (this.isValidStudioField(metaField)) { studioFields.push(metaField); } }, this); _.each(metaFields, function(field) { if (field.type === 'enum' && field.source !== 'non-db' && app.acl.hasAccess('read', module, null, field.name)) { dropdownFields[field.name] = app.lang.get(field.label || field.vname, module); } }, this); _.each(studioFields, function(field) { if (_.isObject(field) && app.acl.hasAccess('read', module, null, field.name)) { var label = app.lang.get(field.label || field.vname, module); if (!_.isEmpty(label)) { allFields[field.name] = label; if (totalableFields.includes(field.type)) { allTotalableFields[field.name] = label; } } } }, this); content.dropdownFields = dropdownFields; content.fields = allFields; content.allTotalableFields = allTotalableFields; bean.set('tabContent', content); }, /** * Checks if a metadata field is valid to be shown in Studio layout editors * * @param {Object} metaField metadata field to be checked * @return {boolean} true if the field can be shown in studio layout */ isValidStudioField: function(metaField) { if (!_.isUndefined(metaField.studio)) { if (_.isObject(metaField.studio)) { if (!_.isUndefined(metaField.studio.recordview)) { return (metaField.studio.recordview !== 'hidden' && metaField.studio.recordview !== false); } if (!_.isUndefined(metaField.studio.visible)) { return metaField.studio.visible; } } else { return (metaField.studio !== 'false' && metaField.studio !== false && metaField.studio !== 'hidden'); } } // JSON fields are not supposed to be modified in studio if (!_.isUndefined(metaField.type) && metaField.type === 'json') { return false; } // remove id type fields except those with names as *_name or email1 return ((!_.isUndefined(metaField.name) && (metaField.name === 'email1' || metaField.name.slice(-5) === '_name')) || ( (!_.isUndefined(metaField.type) && metaField.type !== 'id') && metaField.type !== 'parent_type' && (_.isEmpty(metaField.dbType) || metaField.dbType !== 'id') && (_.isEmpty(metaField.source) || metaField.source === 'db' || metaField.source === 'custom_fields') && (!_.isUndefined(metaField.name) && metaField.name !== 'deleted') )); }, /** * Adds validation tasks to the fields in the layout for the enabled modules */ addValidationTasks: function(bean) { if (bean !== undefined) { bean.addValidationTask('check_table_header', _.bind(this._validateTableHeader, bean)); bean.addValidationTask('check_tile_header', _.bind(this._validateTileOptionsHeader, bean)); bean.addValidationTask('check_tile_body_fields', _.bind(this._validateTileOptionsBody, bean)); bean.addValidationTask('check_records_displayed', _.bind(this._validateRecordsDisplayed, bean)); bean.addValidationTask('check_total_field', _.bind(this._validateTileOptionsTotal, bean)); if (this.fieldsAllowedInTileBody > 0) { bean.addValidationTask( 'check_nb_fields_in_tile_body_fields', _.bind(this._validateNbFieldsInTileOptions, { model: bean, nbFieldsAllowed: this.fieldsAllowedInTileBody }) ); } } else { _.each(this.collection.models, function(model) { model.addValidationTask('check_table_header', _.bind(this._validateTableHeader, model)); model.addValidationTask('check_tile_header', _.bind(this._validateTileOptionsHeader, model)); model.addValidationTask('check_tile_body_fields', _.bind(this._validateTileOptionsBody, model)); model.addValidationTask('check_records_displayed', _.bind(this._validateRecordsDisplayed, model)); model.addValidationTask('check_total_field', _.bind(this._validateTileOptionsTotal, model)); if (this.fieldsAllowedInTileBody > 0) { model.addValidationTask( 'check_nb_fields_in_tile_body_fields', _.bind(this._validateNbFieldsInTileOptions, { model: model, nbFieldsAllowed: this.fieldsAllowedInTileBody }) ); } }, this); } }, /** * Validates table header values for the enabled module * * @protected */ _validateTableHeader: function(fields, errors, callback) { if (_.isEmpty(this.get('table_header'))) { errors.table_header = errors.table_header || {}; errors.table_header.required = true; } callback(null, fields, errors); }, /** * Validates Tile Options header values for the enabled module * * @protected */ _validateTileOptionsHeader: function(fields, errors, callback) { if (_.isEmpty(this.get('tile_header'))) { errors.tile_header = errors.tile_header || {}; errors.tile_header.required = true; } callback(null, fields, errors); }, /** * Validates Tile Options body values for the enabled module * * @protected */ _validateTileOptionsBody: function(fields, errors, callback) { if (_.isEmpty(this.get('tile_body_fields'))) { errors.tile_body_fields = errors.tile_body_fields || {}; errors.tile_body_fields.required = true; } callback(null, fields, errors); }, /** * Validates number of fields in the tile options for the enabled module * * @protected */ _validateNbFieldsInTileOptions: function(fields, errors, callback) { var nbFields = this.model.get('tile_body_fields').length; if (nbFields > this.nbFieldsAllowed) { errors.tile_body_fields = errors.tile_body_fields || {}; errors.tile_body_fields.tooManyFields = true; } callback(null, fields, errors); }, /** * Validates records per column values for the enabled module * * @protected */ _validateRecordsDisplayed: function(fields, errors, callback) { if (_.isEmpty(this.get('records_per_column'))) { errors.records_per_column = errors.records_per_column || {}; errors.records_per_column.required = true; } callback(null, fields, errors); }, /** * Validates Tile Options total value for the enabled module * @param {Object} fields * @param {Object} errors * @param {Function} callback * * @protected */ _validateTileOptionsTotal: function(fields, errors, callback) { if (this.get('show_column_total') && !this.get('total_field')) { errors.total_field = errors.total_field || {}; errors.total_field.required = true; } callback(null, fields, errors); }, /** * @inheritdoc */ _dispose: function() { this.context.off('pipeline:config:model:add', null, this); this.context.off('pipeline:config:model:remove', null, this); this._super('_dispose'); } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.VisualPipelineConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseVisualPipelineConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'BaseConfigDrawerContentLayout', events: { 'change select.module-selection': '_changeModule', }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$el.addClass('record-panel'); this._changeModule(); }, /** * Trigger module selection change * @private */ _changeModule: function() { let selectedModule = this.$el.find('select.module-selection').val(); this.context.trigger('pipeline:config:set-active-module', selectedModule); }, }) } }} , "datas": {} }, "ConsoleConfiguration":{"fieldTemplates": { "base": { "multi-field-label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.MultiFieldColumnLinkField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationMultiFieldColumnLinkField * @extends View.Fields.Base.BaseField */ ({ // Multi-field-label FieldTemplate (base) extendsFrom: 'ConsoleConfigurationFieldListField', events: { 'click .multi-field-label': 'multiFieldColumnLinkClicked' }, /** * Create a new empty block and append it to the field list * @param e */ multiFieldColumnLinkClicked: function(e) { var multiRow = app.lang.get('LBL_CONSOLE_MULTI_ROW', this.module); var multiRowHint = app.lang.get('LBL_CONSOLE_MULTI_ROW_HINT', this.module); var newMultiField = '<li class="pill outer multi-field-block">' + '<ul class="multi-field-sortable multi-field connectedSortable">' + '<li class="list-header" rel="tooltip" data-original-title="' + multiRow + '">' + multiRow + '<i class="sicon sicon-remove multi-field-column-remove"></i></li><div class="multi-field-hint">' + multiRowHint + '</div></ul></li>'; var columnBox = $(e.currentTarget).closest('div.column').find('ul.field-list:first'); columnBox.append(newMultiField); var newUl = columnBox.find('.multi-field-sortable.multi-field.connectedSortable:last'); this.initMultiFieldDragAndDrop(newUl); } }) }, "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.EnumField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', orderByFieldNames: ['order_by_primary', 'order_by_secondary'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // If this is an enum for fields to order console multi-line lists by, // populate those field options if (this.orderByFieldNames.indexOf(options.def.name) > -1) { this.populateOrderByValues(); if (this.model) { this.model.on('change:tabContent', function() { this.populateOrderByValues(); }, this); } } }, /** * Populates an "Order By" enum with the proper order by field options */ populateOrderByValues: function() { // Allow a blank field option this.items = { '': '' }; // Get the fields to populate the order-by list with var tabContent = this.model.get('tabContent'); if (_.isEmpty(tabContent)) { tabContent = this.getTabContent(this.model.get('enabled_module')); } if (!_.isEmpty(tabContent)) { this.items = _.extend(this.items, tabContent.sortFields); } }, /** * Retrieves the content of the tab for the module * @param {string} changes Object containing the changes for the fields of an update activity message * @return {Object} The tab content */ getTabContent: function(module) { var content = {}; var allFields = {}; var fields = app.metadata.getModule(module, 'fields'); _.each(fields, function(field) { if (_.isObject(field)) { var label = field.vname || field.label; if (!_.isEmpty(app.lang.getModString(label, module))) { allFields[field.name] = app.lang.getModString(label, module); } } }); content.fields = allFields; return content; } }) }, "directions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.DirectionsField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationDirectionsField * @extends View.Fields.Base.BaseField */ ({ // Directions FieldTemplate (base) events: { 'click .restore-defaults-btn': 'restoreClicked' }, /** * Stores the default attributes for the model */ defaults: {}, /** * Stores a mapping of {value} => {label} used for sort direction fields */ sortDirectionLabels: { desc: 'LBL_CONSOLE_DIRECTIONS_DESCENDING', asc: 'LBL_CONSOLE_DIRECTIONS_ASCENDING' }, /** * These store the template strings representing the default field values */ primarySortName: '', primarySortDirection: '', secondarySortName: '', secondarySortDirection: '', filterString: '', /** * Link to detailed instructions */ detailedInstructionsLink: '', /** * @inheritdoc * * @param options */ initialize: function(options) { this._super('initialize', [options]); this._initDefaults(); }, /** * Initializes the template strings that represent the tab model's default * values for the fields on the console configuration view * * @private */ _initDefaults: function() { this.defaults = this.model.get('defaults') || {}; // Build detailedInstructionsLink var serverInfo = app.metadata.getServerInfo(); this.detailedInstructionsLink = 'https://www.sugarcrm.com/crm/product_doc.php?edition=' + serverInfo.flavor + '&version=' + serverInfo.version + '&lang=' + app.lang.getLanguage() + '&module=ConsoleManagement'; let products = app.user.getProductCodes(); this.detailedInstructionsLink += products ? '&products=' + encodeURIComponent(products.join(',')) : ''; // Get the tabContent attribute, which includes a mapping of // {sort field value} => {sort field label} var tabContent = this.model.get('tabContent'); var sortFields = tabContent.sortFields || {}; // Initialize the primary sort default template strings this.primarySortName = sortFields[this.defaults.order_by_primary] || ''; var sortDirection = this.defaults.order_by_primary_direction || 'asc'; this.primarySortDirection = app.lang.get(this.sortDirectionLabels[sortDirection], this.module); // Initialize the secondary sort default template strings this.secondarySortName = sortFields[this.defaults.order_by_secondary]; sortDirection = this.defaults.order_by_secondary_direction || 'asc'; this.secondarySortDirection = app.lang.get(this.sortDirectionLabels[sortDirection], this.module); // Initialize the filter definition default template string this._buildFilterString(this.defaults.filter_def); }, /** * Builds a readable string representing a filter definition * * @param {Array} filterDef the filter definition to convert into a string * @private */ _buildFilterString: function(filterDef) { filterDef = filterDef || []; // Make use of the existing filter-field code to help with getting // field and operator labels var tempField = app.view.createField({ def: { name: 'temp_field', type: 'filter-field' }, view: this.view, nested: true, viewName: 'edit', model: this.model }); // Add the rows/rules of the filter definition to the filter string one // at a time this.filterString = ''; _.each(filterDef, function(filter) { _.each(filter, function(conditions, field) { // If this is not the first filter rule, add an "and" to separate them if (!_.isEmpty(this.filterString)) { this.filterString += app.lang.get('LBL_CONSOLE_DIRECTIONS_FILTER_AND', this.module); } this.filterString += this._buildFilterRowString(field, conditions, tempField); }, this); }, this); tempField.dispose(); }, /** * Helper function to build a string representing a single row/rule of a * filter definition * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} conditions the conditions of the filter rule, typically a map * of operator => value(s) * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the filter row * @private */ _buildFilterRowString: function(field, conditions, filterField) { var rowString = ''; rowString += this._getFilterFieldString(field, filterField); // If this is not a predefined filter, also add the operator if (filterField.fieldList[field] && !filterField.fieldList[field].predefined_filter) { rowString += this._getFilterOperatorAndValueString(field, conditions, filterField); } return rowString; }, /** * Helper function to get the field of a filter row as a readable string * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the field label * @private */ _getFilterFieldString: function(field, filterField) { if (filterField.filterFields && filterField.filterFields[field]) { return filterField.filterFields[field] + ' '; } return ''; }, /** * Helper function to get the operator(s) and value(s) of a filter row as a * readable string * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} conditions the conditions of the filter rule, typically a map * of operator => value(s) * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the operator and value(s) labels * @private */ _getFilterOperatorAndValueString: function(field, conditions, filterField) { var operatorAndValueString = ''; _.each(conditions, function(value, operator) { // If there are multiple conditions on the field, separate them with commas if (!_.isEmpty(operatorAndValueString)) { operatorAndValueString += ', '; } // Add the operator label based on the field type var fieldData = app.metadata.getField({ module: this.model.get('enabled_module'), name: field }); var hasOperatorLabel = filterField.filterOperatorMap && filterField.filterOperatorMap[fieldData.type]; if (hasOperatorLabel) { var operatorMap = filterField.filterOperatorMap[fieldData.type]; if (operatorMap[operator]) { operatorAndValueString += app.lang.get(operatorMap[operator], 'Filters') + ': '; } } // If the operator requires a value, add the value to the string if (!_.contains(filterField._operatorsWithNoValues, operator)) { operatorAndValueString += this._getFilterValueString(value) + ' '; } }, this); return operatorAndValueString; }, /** * Helper function to get the value(s) of a filter row value as a readable * string * * @param value the value(s) of the filter row * @return {Array|string} the string representing the filter row's value(s) * @private */ _getFilterValueString: function(value) { if (_.isArray(value)) { var valueString = '('; for (var i = 0; i < value.length; i++) { if (i > 0) { if (i === value.length - 1) { valueString += ' ' + app.lang.get('LBL_CONSOLE_DIRECTIONS_FILTER_OR', this.module); } else { valueString += ', '; } } valueString += value[i]; } valueString += ')'; return valueString; } return value; }, /** * Set defaultViewMeta to context and trigger defaultmetaready. * * @param data */ setViewMetaData: function(data) { this.context.set('defaultViewMeta', data); this.context.trigger('consoleconfig:reset:defaultmetaready'); }, /** * Sets the default values for fields on the model when the reset button is * clicked. Triggers an event to signal to the filter field to re-render properly */ restoreClicked: function() { this.model.set(this.defaults); this.model.trigger('consoleconfig:reset:default'); var params = {modules: this.model.get('enabled_module'), type: 'view', name: 'multi-line-list'}; var url = app.api.buildURL('ConsoleConfiguration', 'default-metadata', {}, params); app.api.call('GET', url, null, { success: _.bind(this.setViewMetaData, this) }); } }) }, "filter-field": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.FilterFieldField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationFilterFieldField * @extends View.Fields.Base.BaseField */ ({ // Filter-field FieldTemplate (base) events: { 'click [data-action=add]': 'addRow', 'click [data-action=remove]': 'removeRow', 'change [data-filter=field] input[type=hidden]': 'handleFieldSelected', 'change [data-filter=operator] input[type=hidden]': 'handleOperatorSelected', }, /** * Stores the list of filter field options. Defaults for all filter lists * can be specified here */ fieldList: { $owner: { 'predefined_filter': true, 'label': 'LBL_CURRENT_USER_FILTER' }, $favorite: { 'predefined_filter': true, 'label': 'LBL_FAVORITES_FILTER' } }, filterFields: {}, /** * Stores the mapping of filter operator options */ filterOperators: {}, _operatorsWithNoValues: [], /** * Stores the filter definition */ filterDef: [], /** * Stores the template to render a row of the filter list */ rowTemplate: null, /** * Map of fields types. * * Specifies correspondence between field types and field operator types. */ fieldTypeMap: { 'datetime': 'date', 'datetimecombo': 'date' }, /** * Stores the name of the module this filter refers to */ moduleName: null, /** * @override * @param {Object} opts */ initialize: function(opts) { this._super('initialize', [opts]); this.moduleName = this.model.get('enabled_module'); // Store partial template this.rowTemplate = app.template.getField('filter-field', 'edit-filter-row', 'ConsoleConfiguration'); // Store the filter field and operator information for the module this.loadFilterOperators(this.model.get('enabled_module')); this.loadFilterFields(this.model.get('enabled_module')); this.filterDef = this.model.get('filter_def'); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset */ bindDataChange: function() { if (this.model) { this.model.on('consoleconfig:reset:default', function() { this.render(); }, this); } }, /** * Loads the list of filter fields for supplied module. * * @param {string} module The module to load the filter fields for. */ loadFilterFields: function(module) { // Get the set of filterableFields for the tab, and extend it with // the default fieldList var filterableFields = this.model.get('filterableFields') || {}; this.fieldList = _.extend({}, this.fieldList, filterableFields); // For each field, if it is filterable (or a pre-defined filter), add it // to the filterFields list this.filterFields = {}; _.each(this.fieldList, function(fieldDef, fieldName) { var label = app.lang.get(fieldDef.label || fieldDef.vname, module); var isPredefined = fieldDef.predefined_filter; var isFilterable = !_.isEmpty(label) && this.filterOperatorMap[fieldDef.type]; if (isPredefined || isFilterable) { this.filterFields[fieldName] = label; } }, this); }, /** * Loads the list of filter operators for supplied module. * * @param {string} [module] The module to load the filters for. */ loadFilterOperators: function(module) { this.filterOperatorMap = app.metadata.getFilterOperators(module); this._operatorsWithNoValues = ['$empty', '$not_empty']; }, /** * In edit mode, render filter input fields using the edit-filter-row template. * @inheritdoc * @private */ _render: function() { this._super('_render'); this.populateFilter(this.model.get('filter_def')); // If the filter definition is empty, add a fresh row if (this.$('[data-filter=row]').length === 0) { this.addRow(); } }, /** * Builds the initial elements of the filter for the given filter definition * @param array filterDef the filter definition */ populateFilter: function(filterDef) { filterDef = app.data.getBeanClass('Filters').prototype.populateFilterDefinition(filterDef, true); _.each(filterDef, function(row) { this.populateRow(row); }, this); }, /** * Populates row fields with the row filter definition. * * In case it is a template filter that gets populated by values passed in * the context/metadata, empty values will be replaced by populated * value(s). * * @param {Object} rowObj The filter definition of a row. */ populateRow: function(rowObj) { var moduleMeta = app.metadata.getModule(this.moduleName); var fieldMeta = moduleMeta.fields; _.each(rowObj, function(value, key) { var isPredefinedFilter = (this.fieldList[key] && this.fieldList[key].predefined_filter === true); if (key === '$or') { var keys = _.reduce(value, function(memo, obj) { return memo.concat(_.keys(obj)); }, []); key = _.find(_.keys(this.fieldList), function(key) { if (_.has(this.fieldList[key], 'dbFields')) { return _.isEqual(this.fieldList[key].dbFields.sort(), keys.sort()); } }, this); // Predicates are identical, so we just use the first. value = _.values(value[0])[0]; } else if (key === '$and') { var values = _.reduce(value, function(memo, obj) { return _.extend(memo, obj); }, {}); var def = _.find(this.fieldList, function(fieldDef) { return _.has(values, fieldDef.id_name || fieldDef.name); }, this); var operator = '$equals'; key = def ? def.name : key; // We want to get the operator from our values object only for currency fields if (def && !_.isString(values[def.name]) && def.type === 'currency') { operator = _.keys(values[def.name])[0]; values[key] = values[key][operator]; } value = {}; value[operator] = values; } else if (!fieldMeta[key] && !isPredefinedFilter) { return; } if (!this.fieldList[key]) { //Make sure we use name for relate fields var relate = _.find(this.fieldList, function(field) { return field.id_name === key; }); // field not found so don't create row for it. if (!relate) { return; } key = relate.name; // for relate fields in version < 7.7 we used `$equals` and `$not_equals` operator so for version // compatibility & as per TY-159 needed to fix this since 7.7 & onwards we will be using `$in` & // `$not_in` operators for relate fields if (_.isString(value) || _.isNumber(value)) { value = {$in: [value]}; } else if (_.keys(value)[0] === '$not_equals') { var val = _.values([value])[0]; value = {$not_in: val}; } } if (_.isString(value) || _.isNumber(value)) { value = {$equals: value}; } _.each(value, function(value, operator) { this.initRow(null, {name: key, operator: operator, value: value}); }, this); }, this); }, /** * Add a row * @param {Event} e * @return {jQuery} $row The added row element. */ addRow: function(e) { var $row; if (e) { // Triggered by clicking the plus sign. Add the row to that point. $row = this.$(e.currentTarget).closest('[data-filter=row]'); $row.after(this.rowTemplate()); $row = $row.next(); } return this.initRow($row); }, /** * Remove a row * @param {Event} e */ removeRow: function(e) { var $row = this.$(e.currentTarget).closest('[data-filter=row]'); $row.remove(); if (this.$('[data-filter=row]').length === 0) { this.addRow(); } this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Initializes a row either with the retrieved field values or the * default field values. * * @param {jQuery} [$row] The related filter row. * @param {Object} [data] The values to set in the fields. * @return {jQuery} $row The initialized row element. */ initRow: function($row, data) { $row = $row || $(this.rowTemplate()).appendTo(this.$el); data = data || {}; var model; var field; // Init the row with the data available. $row.attr('data-name', data.name); $row.attr('data-operator', data.operator); $row.attr('data-value', data.value); $row.data('name', data.name); $row.data('operator', data.operator); $row.data('value', data.value); // Create a blank model for the field selector enum, and set the // field value if we know it. model = app.data.createBean(this.model.get('enabled_module')); if (data.name) { model.set('filter_row_name', data.name); } // Create the field selector enum and add it to the dom field = this.createField(model, { name: 'filter_row_name', type: 'enum', options: this.filterFields }); field.render(); $row.find('[data-filter=field]').append(field.$el); // Store the field in the data attributes. $row.data('nameField', field); // If this selector has a value, init the operator field as well if (data.name) { this.initOperatorField($row); } return $row; }, /** * Initializes the operator field. * * @param {jQuery} $row The related filter row. */ initOperatorField: function($row) { var $fieldWrapper = $row.find('[data-filter=operator]'); var data = $row.data(); var fieldName = data.nameField.model.get('filter_row_name'); var previousOperator = data.operator; // Make sure the data attributes contain the right selected field. data.name = fieldName; if (!fieldName) { return; } // For relate fields data.id_name = this.fieldList[fieldName].id_name; // For flex-relate fields data.type_name = this.fieldList[fieldName].type_name; //Predefined filters don't need operators and value field if (this.fieldList[fieldName].predefined_filter === true) { data.isPredefinedFilter = true; return; } // Get operators for this filter type var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var payload = {}; var types = _.keys(this.filterOperatorMap[fieldType]); // For parent field with the operator '$equals', the operator field is // hidden and we need to display the value field directly. So here we // need to assign 'previousOperator' and 'data.operator variables' to let // the value field initialize. //FIXME: We shouldn't have a condition on the parent field. TY-352 will // fix it. if (fieldType === 'parent' && _.isEqual(types, ['$equals'])) { previousOperator = data.operator = types[0]; } fieldType === 'parent' ? $fieldWrapper.addClass('hide').empty() : $fieldWrapper.removeClass('hide').empty(); $row.find('[data-filter=value]').addClass('hide').empty(); _.each(types, function(operand) { payload[operand] = app.lang.get( this.filterOperatorMap[fieldType][operand], [this.moduleName, 'Filters'] ); }, this); // Render the operator field var model = app.data.createBean(this.moduleName); if (previousOperator) { model.set('filter_row_operator', data.operator === '$dateRange' ? data.value : data.operator); } var field = this.createField(model, { name: 'filter_row_operator', type: 'enum', // minimumResultsForSearch set to 9999 to hide the search field, // See: https://github.com/ivaynberg/select2/issues/414 searchBarThreshold: 9999, options: payload }); field.render(); $fieldWrapper.append(field.$el); data.operatorField = field; var hide = fieldType === 'parent'; this._hideOperator(hide, $row); // We want to go into 'initValueField' only if the field value is known. // We need to check 'previousOperator' instead of 'data.operator' // because even if the default operator has been set, the field would // have set 'data.operator' when it rendered anyway. if (previousOperator) { this.initValueField($row); } }, /** * Initializes the value field. * * @param {jQuery} $row The related filter row. */ initValueField: function($row) { var self = this; var data = $row.data(); var operation = data.operatorField.model.get('filter_row_operator'); // Make sure the data attributes contain the right operator selected. data.operator = operation; if (!operation) { return; } if (_.contains(this._operatorsWithNoValues, operation)) { return; } // Patching fields metadata var moduleName = this.moduleName; var module = app.metadata.getModule(moduleName); var fields = app.metadata._patchFields(moduleName, module, app.utils.deepCopy(this.fieldList)); // More patch for some field types var fieldName = $row.find('[data-filter=field] input[type=hidden]').select2('val'); var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var fieldDef = fields[fieldName]; switch (fieldType) { case 'enum': fieldDef.isMultiSelect = this.isCollectiveValue($row); // Set minimumResultsForSearch to a negative value to hide the search field, // See: https://github.com/ivaynberg/select2/issues/489#issuecomment-13535459 fieldDef.searchBarThreshold = -1; break; case 'bool': fieldDef.type = 'enum'; fieldDef.options = fieldDef.options || 'filter_checkbox_dom'; break; case 'int': fieldDef.auto_increment = false; //For $in operator, we need to convert `['1','20','35']` to `1,20,35` to make it work in a varchar field if (operation === '$in') { fieldDef.type = 'varchar'; fieldDef.len = 200; if (_.isArray($row.data('value'))) { $row.attr('data-value', $row.data('value').join(',')); } } break; case 'teamset': fieldDef.type = 'relate'; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'datetimecombo': case 'date': fieldDef.type = 'date'; //Flag to indicate the value needs to be formatted correctly data.isDate = true; if (operation.charAt(0) !== '$') { //Flag to indicate we need to build the date filter definition based on the date operator data.isDateRange = true; return; } break; case 'relate': fieldDef.auto_populate = true; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'parent': data.isFlexRelate = true; break; } fieldDef.required = false; fieldDef.readonly = false; // Create new model with the value set var model = app.data.createBean(moduleName); var $fieldValue = $row.find('[data-filter=value]'); $fieldValue.removeClass('hide').empty(); // Add the field type as an attribute on the HTML element so that it // can be used as a CSS selector. $fieldValue.attr('data-type', fieldType); //fire the change event as soon as the user start typing var _keyUpCallback = function(e) { if ($(e.currentTarget).is('.select2-input')) { return; //Skip select2. Select2 triggers other events. } this.value = $(e.currentTarget).val(); // We use "silent" update because we don't need re-render the field. model.set(this.name, this.unformat($(e.currentTarget).val()), {silent: true}); model.trigger('change'); }; //If the operation is $between we need to set two inputs. if (operation === '$between' || operation === '$dateBetween') { var minmax = []; var value = $row.data('value') || []; if (fieldType === 'currency' && $row.data('value')) { value = $row.data('value') || {}; model.set(value); value = value[fieldName] || []; // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). model.set('id', 'not_new'); } model.set(fieldName + '_min', value[0] || ''); model.set(fieldName + '_max', value[1] || ''); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_min'}))); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_max'}))); if (operation === '$dateBetween') { minmax[0].label = app.lang.get('LBL_FILTER_DATEBETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_DATEBETWEEN_TO'); } else { minmax[0].label = app.lang.get('LBL_FILTER_BETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_BETWEEN_TO'); } data.valueField = minmax; _.each(minmax, function(field) { $fieldValue.append(field.$el); this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); field.$('.input-append').prepend('<span class="add-on">' + field.label + '</span>') .addClass('input-prepend') .removeClass('date'); // .date makes .inherit-width on input have no effect field.$('input, textarea').on('keyup', _.debounce(_.bind(_keyUpCallback, field), 400)); }); field.render(); }, this); } else if (data.isFlexRelate) { var values = {}; _.each($row.data('value'), function(value, key) { values[key] = value; }, this); model.set(values); var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); findRelatedName = app.data.createBeanCollection(model.get('parent_type')); data.valueField = field; $fieldValue.append(field.$el); if (model.get('parent_id')) { findRelatedName.fetch({ params: {filter: [{'id': model.get('parent_id')}]}, complete: _.bind(function() { if (!this.disposed) { if (findRelatedName.first()) { model.set(fieldName, findRelatedName.first().get(field.getRelatedModuleField()), {silent: true}); } if (!field.disposed) { field.render(); } } }, this) }); } else { field.render(); } } else { // value is either an empty object OR an object containing `currency_id` and currency amount if (fieldType === 'currency' && $row.data('value')) { // for stickiness & to retrieve correct saved values, we need to set the model with data.value object model.set($row.data('value')); // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). // Mark this one as not_new so that model isn't treated as new model.set('id', 'not_new'); } else { model.set(fieldDef.id_name || fieldName, $row.data('value')); } // Render the value field var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); $fieldValue.append(field.$el); data.valueField = field; this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); // .date makes .inherit-width on input have no effect so we need to remove it. field.$('.input-append').removeClass('date'); field.$('input, textarea').on('keyup',_.debounce(_.bind(_keyUpCallback, field), 400)); }); if ((fieldDef.type === 'relate' || fieldDef.type === 'nestedset') && !_.isEmpty($row.data('value')) ) { var findRelatedName = app.data.createBeanCollection(fieldDef.module); var relateOperator = this.isCollectiveValue($row) ? '$in' : '$equals'; var relateFilter = [{id: {}}]; relateFilter[0].id[relateOperator] = $row.data('value'); findRelatedName.fetch({fields: [fieldDef.rname], params: {filter: relateFilter}, complete: function() { if (!self.disposed) { if (findRelatedName.length > 0) { model.set(fieldDef.id_name, findRelatedName.pluck('id'), {silent: true}); model.set(fieldName, findRelatedName.pluck(fieldDef.rname), {silent: true}); } if (!field.disposed) { field.render(); } } } }); } else { field.render(); } } // When the value changes, update the filter value var updateFilter = function() { self._updateFilterData($row); self.model.set('filter_def', self.buildFilterDef(true), {silent: true}); }; this.listenTo(model, 'change', updateFilter); this.listenTo(model, 'change:' + fieldName, updateFilter); // Manually trigger the filter request if a value has been selected lately // This is the case for checkbox fields or enum fields that don't have empty values. var modelValue = model.get(fieldDef.id_name || fieldName); // To handle case: value is an object with 'currency_id' = 'xyz' and 'likely_case' = '' // For currency fields, when value becomes an object, trigger change if (!_.isEmpty(modelValue) && modelValue !== $row.data('value')) { model.trigger('change'); } }, /** * Check if the selected filter operator is a collective type. * * @param {jQuery} $row The related filter row. */ isCollectiveValue: function($row) { return $row.data('operator') === '$in' || $row.data('operator') === '$not_in'; }, /** * Update filter data for this row * @param $row Row to update * @private */ _updateFilterData: function($row) { var data = $row.data(); var field = data.valueField; var name = data.name; var valueForFilter; //Make sure we use ID for relate fields if (this.fieldList[name] && this.fieldList[name].id_name) { name = this.fieldList[name].id_name; } //If we have multiple fields we have to build an array of values if (_.isArray(field)) { valueForFilter = []; _.each(field, function(field) { var value = !field.disposed && field.model.has(field.name) ? field.model.get(field.name) : ''; value = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; valueForFilter.push(value); }); } else { var value = !field.disposed && field.model.has(name) ? field.model.get(name) : ''; valueForFilter = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; } // Update filter value once we've calculated final value $row.data('value', valueForFilter); $row.attr('data-value', valueForFilter); }, /** * Shows or hides the operator field of the filter row specified. * * Automatically populates the operator field to have value `$equals` if it * is not in midst of populating the row. * * @param {boolean} hide Set to `true` to hide the operator field. * @param {jQuery} $row The filter row of interest. * @private */ _hideOperator: function(hide, $row) { $row.find('[data-filter=value]') .toggleClass('span4', !hide) .toggleClass('span8', hide); }, /** * Utility function that instantiates a field for this form. * * The field action is manually set to `detail` because we want to render * the `edit` template but the action remains `detail` (filtering). * * @param {Data.Bean} model A bean necessary to the field for storing the * value(s). * @param {Object} def The field definition. * @return {View.Field} The field component. */ createField: function(model, def) { var obj = { def: def, view: this.view, nested: true, viewName: 'edit', model: model }; var field = app.view.createField(obj); return field; }, /** * Fired when a user selects a field to filter by * @param {Event} e */ handleFieldSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {field: 'operatorField', value: 'operator'}, {field: 'valueField', value: 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initOperatorField($row); // Update the attributes of the row $row.attr('data-name', $el.val()); $row.attr('data-operator', ''); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Fired when a user selects an operator to filter by * @param {Event} e */ handleOperatorSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {'field': 'valueField', 'value': 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initValueField($row); // Update the attributes of the row $row.attr('data-operator', $el.val()); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Disposes fields stored in the data attributes of the row element. * * @example of an `opts` object param: * [ * {field: 'nameField', value: 'name'}, * {field: 'operatorField', value: 'operator'}, * {field: 'valueField', value: 'value'} * ] * * @param {jQuery} $row The row which fields are to be disposed. * @param {Array} opts An array of objects containing the field object and * value to the data attributes of the row. */ _disposeRowFields: function($row, opts) { var data = $row.data(); var model; if (_.isObject(data) && _.isArray(opts)) { _.each(opts, function(val) { if (data[val.field]) { //For in between filter we have an array of fields so we need to cover all cases var fields = _.isArray(data[val.field]) ? data[val.field] : [data[val.field]]; data[val.value] = ''; _.each(fields, function(field) { model = field.model; if (val.field === 'valueField' && model) { model.clear({silent: true}); this.stopListening(model); } field.dispose(); field = null; }, this); return; } if (data.isDateRange && val.value === 'value') { data.value = ''; } }, this); } //Reset flags data.isDate = false; data.isDateRange = false; data.isPredefinedFilter = false; data.isFlexRelate = false; $row.data(data); }, /** * Build filter definition for all rows. * * @param {boolean} onlyValidRows Set `true` to retrieve only filter * definition of valid rows, `false` to retrieve the entire field * template. * @return {Array} Filter definition. */ buildFilterDef: function(onlyValidRows) { var $rows = this.$('[data-filter=row]'); var filter = []; _.each($rows, function(row) { var rowFilter = this.buildRowFilterDef($(row), onlyValidRows); if (rowFilter) { filter.push(rowFilter); } }, this); return filter; }, /** * Build filter definition for this row. * * @param {jQuery} $row The related row. * @param {boolean} onlyIfValid Set `true` to validate the row and return * `undefined` if not valid, or `false` to build the definition anyway. * @return {Object} Filter definition for this row. */ buildRowFilterDef: function($row, onlyIfValid) { var data = $row.data(); if (onlyIfValid && !this.validateRow($row)) { return; } var operator = data.operator; var value = data.value || ''; var name = data.id_name || data.name; var filter = {}; if (_.isEmpty(name)) { return; } if (data.isPredefinedFilter || !this.fieldList) { filter[name] = ''; return filter; } else { if (!_.isEmpty(data.valueField) && _.isFunction(data.valueField.delegateBuildFilterDefinition)) { filter[name] = {}; filter[name][operator] = data.valueField.delegateBuildFilterDefinition(); } else if (this.fieldList[name] && _.has(this.fieldList[name], 'dbFields')) { var subfilters = []; _.each(this.fieldList[name].dbFields, function(dbField) { var filter = {}; filter[dbField] = {}; filter[dbField][operator] = value; subfilters.push(filter); }); filter.$or = subfilters; } else { if (data.isFlexRelate) { var valueField = data.valueField; var idFilter = {}; var typeFilter = {}; idFilter[data.id_name] = valueField.model.get(data.id_name); typeFilter[data.type_name] = valueField.model.get(data.type_name); filter.$and = [idFilter, typeFilter]; // Creating currency filter. For all but `$between` operators we use // type property from data.valueField. For `$between`, data.valueField // is an array and therefore we check for type==='currency' from // either of the elements. } else if (data.valueField && (data.valueField.type === 'currency' || (_.isArray(data.valueField) && data.valueField[0].type === 'currency')) ) { // initially value is an array which we later convert into an object for saving and retrieving // purposes (stickiness structure constraints) var amountValue; if (_.isObject(value) && !_.isUndefined(value[name])) { amountValue = value[name]; } else { amountValue = value; } var amountFilter = {}; amountFilter[name] = {}; amountFilter[name][operator] = amountValue; // for `$between`, we use first element to get dataField ('currency_id') since it is same // for both elements and also because data.valueField is an array var dataField; if (_.isArray(data.valueField)) { dataField = data.valueField[0]; } else { dataField = data.valueField; } var currencyId; currencyId = dataField.getCurrencyField().name; var currencyFilter = {}; currencyFilter[currencyId] = dataField.model.get(currencyId); filter.$and = [amountFilter, currencyFilter]; } else if (data.isDateRange) { //Once here the value is actually a key of date_range_selector_dom and we need to build a real //filter definition on it. filter[name] = {}; filter[name].$dateRange = operator; } else if (operator === '$in' || operator === '$not_in') { // IN/NOT IN require an array filter[name] = {}; //If value is not an array, we split the string by commas to make it an array of values if (_.isArray(value)) { filter[name][operator] = value; } else if (!_.isEmpty(value)) { filter[name][operator] = (value + '').split(','); } else { filter[name][operator] = []; } } else { filter[name] = {}; filter[name][operator] = value; } } return filter; } }, /** * Verify the value of the row is not empty. * * @param {Element} $row The row to validate. * @return {boolean} `true` if valid, `false` otherwise. */ validateRow: function(row) { var $row = $(row); var data = $row.data(); if (_.contains(this._operatorsWithNoValues, data.operator)) { return true; } // for empty value in currency we dont want to validate if (!_.isUndefined(data.valueField) && !_.isArray(data.valueField) && data.valueField.type === 'currency' && (_.isEmpty(data.value) || (_.isObject(data.value) && _.isEmpty(data.valueField.model.get(data.name))))) { return false; } //For date range and predefined filters there is no value if (data.isDateRange || data.isPredefinedFilter) { return true; } else if (data.isFlexRelate) { return data.value ? _.reduce(data.value, function(memo, val) { return memo && !_.isEmpty(val); }, true) : false; } //Special case for between operators where 2 values are needed if (_.contains(['$between', '$dateBetween'], data.operator)) { if (!_.isArray(data.value) || data.value.length !== 2) { return false; } switch (data.operator) { case '$between': // FIXME: the fields should set a true number (see SC-3138). return !(_.isNaN(parseFloat(data.value[0])) || _.isNaN(parseFloat(data.value[1]))); case '$dateBetween': return !_.isEmpty(data.value[0]) && !_.isEmpty(data.value[1]); default: return false; } } return _.isNumber(data.value) || !_.isEmpty(data.value); }, }) }, "sort-order-selector": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.SortOrderSelectorField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationSortOrderSelectorField * @extends View.Fields.Base.BaseField */ ({ // Sort-order-selector FieldTemplate (base) events: { 'click .sort-order-selector': 'setNewValue' }, /** * Stores the name of the field that this field is conditionally dependent on */ dependencyField: null, /** * @inheritdoc * * Grabs the name of the dependency field from the field options * * @param options */ initialize: function(options) { this._super('initialize', [options]); if (options.def && options.def.dependencyField) { this.dependencyField = options.def.dependencyField; } }, /** * @inheritdoc * * Extends the parent bindDataChange to include a check of the value of * the dependency field */ bindDataChange: function() { this._super('bindDataChange'); if (this.dependencyField) { this.model.on('change:' + this.dependencyField, function() { this._handleDependencyChange(); }, this); this.model.on('change:' + this.name, function() { this._setValue(this.model.get(this.name)); }, this); } }, /** * When this field first renders, check the dependency field to see if we * need to hide this * * @private */ _render: function() { this._super('_render'); this._handleDependencyChange(); }, /** * Checks the value of the dependency field. If it is empty, this field will * be set to 'ascending' and hidden. * * @private */ _handleDependencyChange: function() { if (this.model && this.$el) { if (_.isEmpty(this.model.get(this.dependencyField))) { this._setValue('asc'); this.$el.hide(); } else { this.$el.show(); } } }, /** * Simulates the user clicking on the field to set a value for this field * (both on the model and in the UI) * * @param value the value ('asc' or 'desc') to set the field to * @private */ _setValue: function(value) { this.$el.find('[name="' + value + '"]').click(); }, /** * Sets the value of the selected sort order on the model * * @param event the button click event */ setNewValue: function(event) { this.model.set(this.name, event.currentTarget.name); } }) }, "preview-table": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.PreviewTableField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationPreviewTableField * @extends View.Fields.Base.BaseField */ ({ // Preview-table FieldTemplate (base) /** * The name of the module the fields belong to. * @property {string} */ moduleName: '', /** * A mapping of fields to be rendered on the preview table. * @property {Object} */ fieldList: null, /** * The number of rows to be shown in the preview. * @property {number} */ previewRows: 5, /** * A mapping of class names that describe how the data rows should appear if there are no live data available. * This mapping is based on the fieldList and sent to the template. */ rowDesign: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.bindChangeEvent(options.model); }, /** * Will listen to an event that signals a change in the console configuration. * * @param {Data.Bean} model The model used for the preview. */ bindChangeEvent: function(model) { if (model && model.get('enabled_module')) { this.moduleName = model.get('enabled_module'); this.eventName = 'consoleconfig:preview:' + this.moduleName; this.context.on(this.eventName, this.renderPreview, this); } }, /** * It will create a mapping of css classes that corresponds to the list of columns to be displayed. * Odd and even rows while having a single sub-field should render alternating long and short placeholders, * while if there is a field with multiple sub-fields, 2 placeholders should be shown (1 long, 1 short). * * @param {Array} list A list of fields that have to appear as columns in the preview. */ setRowDesign: function(list) { var i; var oneRow; var singleClass; var longClass = 'cell-bar--long'; var shortClass = 'cell-bar--short'; this.rowDesign = []; for (i = 1; i <= this.previewRows; i++) { singleClass = i % 2 === 0 ? shortClass : longClass; oneRow = _.reduce(list, function(row, subFields) { row.push(subFields.length > 1 ? [longClass, shortClass] : [singleClass]); return row; }, []); this.rowDesign.push(oneRow); } }, /** * Triggers a render of the prevoew based on a list of field labels. The order of the columns * will be inherited from the the order of strings. * * @param {Array} list A list of fields that have to appear as columns in the preview. */ renderPreview: function(list) { this.fieldList = list; this.setRowDesign(list); this.render(); }, /** * @inheritdoc */ _dispose: function() { this.context.off(this.eventName, this.renderPreview, this); this._super('_dispose'); }, }) }, "available-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.AvailableFieldListField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationAvailableFieldListField * @extends View.Fields.Base.BaseField */ ({ // Available-field-list FieldTemplate (base) /** * Fields with these names should not be displayed in fields list. */ ignoredNames: ['deleted', 'mkto_id', 'googleplus', 'team_name'], /** * Fields with these types should not be displayed in fields list. */ ignoredTypes: ['id', 'link', 'tag'], /** * Here are stored all available fields for all available tabs. */ availableFieldLists: [], /** * List of fields that are displayed for a given module. */ currentAvailableFields: [], /** * @inheritdoc * * Collects all supported fields for all available modules and sets the module specific fields to be displayed. */ initialize: function(options) { this._super('initialize', [options]); var moduleName = this.model.get('enabled_module'); this.setAvailableFields(moduleName); this.currentAvailableFields = this.availableFieldLists; }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset */ bindDataChange: function() { if (this.model) { this.context.on('consoleconfig:reset:defaultmetaready', function() { // the default meta data is ready, use it to re-render var defaultViewMeta = this.context.get('defaultViewMeta'); var moduleName = this.model.get('enabled_module'); if (!_.isEmpty(defaultViewMeta) && !_.isEmpty(defaultViewMeta[moduleName])) { this.setAvailableFields(moduleName); this.currentAvailableFields = this.availableFieldLists; this.render(); this.context.trigger('consoleconfig:reset:defaultmetarelay'); } }, this); } }, /** * Return the proper view metadata. * * @param {string} moduleName The selected module name from the available modules. */ getViewMetaData: function(moduleName) { // If defaultViewMeta exists, it means we are restoring the default settings. var defaultViewMeta = this.context.get('defaultViewMeta'); if (!_.isEmpty(defaultViewMeta) && !_.isEmpty(defaultViewMeta[moduleName])) { return this.context.get('defaultViewMeta')[moduleName]; } // Not restoring defaults, use the regular view meta data return app.metadata.getView(moduleName, 'multi-line-list'); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.handleDragAndDrop(); }, /** * Sets the available fields for the requested module. * * @param {string} moduleName The selected module name from the available modules. */ setAvailableFields: function(moduleName) { var allFields = app.metadata.getModule(moduleName, 'fields'); var multiLineList = this.getViewMetaData(moduleName); var multiLineFields = this.getSelectedFields(_.first(multiLineList.panels).fields); this.availableFieldLists = []; _.each(allFields, function(field) { if (this.isFieldSupported(field, multiLineFields)) { this.availableFieldLists.push({ 'name': field.name, 'label': (field.label || field.vname), 'displayName': app.lang.get(field.label || field.vname, moduleName) }); } }, this); // Sort available fields alphabetically this.availableFieldLists = _.sortBy(this.availableFieldLists, 'displayName'); }, /** * Parse metadata and return array of fields that are already defined in the metadata. * * @param {Array} multiLineFields List of fields that appear on the multi-line list view. * @return {Array} True if the field is already in, false otherwise. */ getSelectedFields: function(multiLineFields) { var fields = []; _.each(multiLineFields, function(column) { _.each(column.subfields, function(subfield) { // if widget_name exists, it's a special field, use widget_name instead of name fields.push({'name': subfield.widget_name || subfield.name}); }, this); }, this); return fields; }, /** * Restricts specific fields to be shown in available fields list. * * @param {Object} field Field to be verified. * @param {Array} multiLineFields List of fields that appear on the multi-line list view. * @return {boolean} True if field is supported, false otherwise. */ isFieldSupported: function(field, multiLineFields) { // Specified fields names should be ignored. if (!field.name || _.contains(this.ignoredNames, field.name)) { return false; } // Specified field types should be ignored. if (_.contains(this.ignoredTypes, field.type) || field.dbType === 'id') { return false; } // Multi-line list view fields should not be displayed. if (_.findWhere(multiLineFields, {'name': field.name})) { return false; } return !this.hasNoStudioSupport(field); }, /** * Verify if fields do not have available studio support. * Studio fields have multiple value types (array, bool, string, undefined). * * @param {Object} field Field selected to get verified. * @return {boolean} True if there is no support, false otherwise. */ hasNoStudioSupport: function(field) { // if it's a special field, do not check studio attribute if (!_.isUndefined(field.type) && field.type === 'widget') { return false; } var studio = field.studio; if (!_.isUndefined(studio)) { if (studio === 'false' || studio === false) { return true; } if (!_.isUndefined(studio.listview)) { if (studio.listview === 'false' || studio.listview === false) { return true; } } } return false; }, /** * Handles the dragging of the items from available fields list to the columns list section * But not the way around */ handleDragAndDrop: function() { this.$('#fields-sortable').sortable({ connectWith: '.connectedSortable', update: _.bind(function(event, ui) { var multiRow = app.lang.get('LBL_CONSOLE_MULTI_ROW', this.module); var multiRowHint = app.lang.get('LBL_CONSOLE_MULTI_ROW_HINT', this.module); var hint = '<div class="multi-field-hint">' + multiRowHint + '</div>'; if ($(ui.sender).hasClass('multi-field') && ui.sender.children().length > 0) { var header = ''; var headerLabel = ''; var i = 0; _.each(ui.sender.children(), function(field) { if (i > 1) { header += '/'; headerLabel += '/'; } if (i++ > 0 && !_.isUndefined(field) && !_.isUndefined(field.textContent)) { if (field.textContent.trim() === multiRowHint) { // clean hint text, it will be added later $(field).remove(); } else { header += field.textContent.trim(); headerLabel += field.getAttribute('fieldlabel'); } } }, this); if (header.endsWith('/')) { header = header.slice(0, -1); headerLabel = headerLabel.slice(0, -1); } header = header ? header : multiRow; $(ui.sender.children()[0]).text(header) .append(this.removeColIcon); $(ui.sender.children()[0]).attr('data-original-title', header); $(ui.sender.children()[0]).attr('fieldname', header); $(ui.sender.children()[0]).attr('fieldlabel', headerLabel); } }, this), receive: _.bind(function(event, ui) { ui.sender.sortable('cancel'); }, this) }); } }) }, "field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.FieldListField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationFieldListField * @extends View.Fields.Base.BaseField */ ({ // Field-list FieldTemplate (base) removeFldIcon: '<i class="sicon sicon-remove console-field-remove"></i>', removeColIcon: '<i class="sicon sicon-remove multi-field-column-remove"></i>', events: { 'click .sicon.sicon-remove.console-field-remove': 'removePill', 'click .sicon.sicon-remove.multi-field-column-remove': 'removeMultiLineField', }, /** * Fields mapped to their subfields. */ mappedFields: {}, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.mappedFields = this.getMappedFields(); this.previewEvent = 'consoleconfig:preview:' + this.model.get('enabled_module'); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset. */ bindDataChange: function() { if (this.model) { this.context.on('consoleconfig:reset:defaultmetarelay', function() { var defaultViewMeta = this.context.get('defaultViewMeta'); var moduleName = this.model.get('enabled_module'); if (defaultViewMeta && defaultViewMeta[moduleName]) { this.mappedFields = this.getMappedFields(); this.context.set('defaultViewMeta', null); this.render(); this.handleColumnsChanging(); } }, this); } }, /** * Removes a pill from the selected fields list. * * @param {e} event Remove icon click event. */ removePill: function(event) { var pill = event.target.parentElement; var container = $(pill.parentElement); event.target.remove(); pill.setAttribute('class', 'pill outer'); this.getAvailableSortable().append(pill); if (container.hasClass('multi-field-sortable')) { this.updateMultiLineField(container); this.addMultiFieldHint(container); } this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Remove a multi line field column and fields inside. * * @param {e} event Remove icon click event. */ removeMultiLineField: function(event) { var multiLineField = event.target.parentElement.parentElement.parentElement; _.each($(multiLineField).find('.pill'), function(pill) { pill.children[0].remove(); pill.setAttribute('class', 'pill outer'); this.getAvailableSortable().append(pill); }, this); multiLineField.remove(); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.initSingleFieldDragAndDrop(); if (this.options.def.type == 'field-list') { this.triggerPreviewUpdate(); } }, /** * Initialize drag & drop for the selected field (main) list. */ initSingleFieldDragAndDrop: function() { var sortableEl = this.$('#columns-sortable'); sortableEl.sortable({ items: '.outer.pill', connectWith: '.connectedSortable', receive: _.bind(this.handleSingleFieldDrop, this), update: _.bind(this.handleSingleFieldStop, this), }); var multiFieldSortables = sortableEl.find('.multi-field-sortable.multi-field.connectedSortable'); _.each(multiFieldSortables, function(multiField) { this.initMultiFieldDragAndDrop($(multiField)); }, this); }, /** * Initialize drag & drop for a multi field container. * * @param {Object} element The multi-field container element. */ initMultiFieldDragAndDrop: function(element) { element.sortable({ items: '.pill', connectWith: '.connectedSortable', receive: _.bind(this.handleMultiLineFieldDrop, this), update: _.bind(this.handleMultiLineFieldStop, this), over: _.bind(this.handleMultiLineFieldOver, this), out: _.bind(this.handleMultiLineFieldOut, this), }); }, /** * Event handler for the single field drag & drop. The event is fired when an item is dropped to a list. * Several actions are performed: * - When moving a field from the right to the left we add the remove icon. * - When moving a field from a multi line field to the outside we change selector. * - The library can't handle the case when the last item from the list is a multi line field. * In such cases we manually insert the moved item after the group; * dropping into a multi-line field is handled in `handleMultiLineFieldDrop`. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleSingleFieldDrop: function(event, ui) { if ('fields-sortable' == ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } if (ui.sender.hasClass('multi-field-sortable')) { ui.item.addClass('outer'); this.addMultiFieldHint(ui.sender); } this.repositionItem(ui); }, /** * Event handler for the single field drag & drop. * The event is fired when drop has been finished and the DOM has been updated. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleSingleFieldStop: function(e, ui) { this.repositionItem(ui); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Event handler for the multi field drag & drop. The event is fired when an item is dropped to a list. * Several actions are performed here: * - If certain conditions are met the drag & drop is cancelled. * - If there is a hint text, it is removed. * - When moving a field from the right to the left we add the remove icon. * - When a field is being moved from the right to the left or from the ouside inside the selector is changed. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleMultiLineFieldDrop: function(event, ui) { var multiLineFields = $(event.target).find('.pill'); if (this.shouldRejectFieldDrop(ui, multiLineFields)) { ui.sender.sortable('cancel'); this.updateMultiLineField(ui.sender); } else { $(event.target).find('.multi-field-hint').remove(); if ('fields-sortable' == ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } if (ui.sender.hasClass('multi-field-sortable')) { this.addMultiFieldHint(ui.sender); } else { ui.item.removeClass('outer'); } this.triggerPreviewUpdate(); } }, /** * Event handler for the multi field drag & drop. * The event is fired when drop has been finished and the DOM has been updated. * * @param {e} event jQuery sortable event handler. */ handleMultiLineFieldStop: function(event) { this.updateMultiLineField($(event.target)); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Event handler for the multi field drag over * The event is fired when drag over with a draggable element has occurred * * @param {e} event jQuery sortable event handler * @param {Object} jQuery ui object selector */ handleMultiLineFieldOver: function(event, ui) { var eventTarget = $(event.target); var multiLineFields = eventTarget.find('.pill'); if (multiLineFields.length > 2 && !ui.item.parent().hasClass('multi-field-sortable')) { ui.item.css('cursor', 'no-drop'); ui.placeholder.addClass('multi-field-block-placeholder-none'); } else { eventTarget.parent().addClass('multi-field-block-highlight'); } }, /** * Event handler for the multi field drag out * The event is fired when drag out with a draggable element has occurred * * @param {e} event jQuery sortable event handler * @param {Object} jQuery ui object selector */ handleMultiLineFieldOut: function(event, ui) { ui.item.css('cursor', ''); ui.placeholder.removeClass('multi-field-block-placeholder-none'); $(event.target).parent().removeClass('multi-field-block-highlight'); }, /** * Update columns property of the model basing on the selected columns. */ handleColumnsChanging: function() { var fieldName; var columns = {}; var moduleName = this.model.get('enabled_module'); var columnsSortable = $('#' + moduleName + '-side') .find('#columns-sortable .pill:not(.multi-field-block)'); var fields = app.metadata.getModule(moduleName, 'fields'); _.each(columnsSortable, function(item) { fieldName = $(item).attr('fieldname'); columns[fieldName] = fields[fieldName]; }); this.model.set('columns', columns); }, /** * jQuery UI does not support drag & drop into nested containers. When the last item is a multi line field, * we have to check for the correct drop area and if the library targets the multi line field instead of the * main container as a drop zone, we move the dropped item to the outside container. * * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ repositionItem: function(ui) { var parentContainer = ui.item.parent(); if (parentContainer.hasClass('multi-field')) { var parentStartPos = parentContainer.offset().top; var parentEndPos = parentStartPos + parentContainer.height(); if (ui.offset.top <= parentStartPos || ui.offset.top >= parentEndPos) { parentContainer.parent().after(ui.item); } } }, /** * Checks 4 conditions in which drag & drop into a multi line field should not be allowed. * The 4 conditions are the following: * - When there are already 2 fields in the block. * - When a multi line field block is being dropped. * - When a field that is defined as a multi-line field is dropped into a block with at least 1 item already. * - When the block contains already a field defined as a multi-line field (such fields count as 2 simple fields). * * @param {Object} ui The jQuery UI library sortable action object. * @param {Array} multiLineFields The list of fields inside a multi field block. */ shouldRejectFieldDrop: function(ui, multiLineFields) { var moduleName = this.model.get('enabled_module'); var droppedFieldName = ui.item.attr('fieldname'); var fieldDefinitions = app.metadata.getModule(moduleName, 'fields'); var isDefinedAsMultiLine = this.isDefinedAsMultiLine(droppedFieldName, fieldDefinitions); // IMPORTANT NOTE: the placeholder is considered another field present in cases // when we perform operation other than a simple reordering of items. var subFieldLimit = 2; // Reject conditions. var hasAlready2Fields = multiLineFields.length > subFieldLimit; var isMultiLineIntoMultiLineDrop = ui.item.hasClass('multi-field-block'); var isMultiFieldDrop = isDefinedAsMultiLine && multiLineFields.length > (subFieldLimit - 1); var containsAlreadyAMultiLineFieldDef = multiLineFields.length == subFieldLimit && this.containsMultiLineFieldDef(multiLineFields, fieldDefinitions); return hasAlready2Fields || isMultiLineIntoMultiLineDrop || isMultiFieldDrop || containsAlreadyAMultiLineFieldDef; }, /** * Check whether a multi line field contains and fields that are defined as multi line fields. * * @param {jQuery} multiLineFields The pills from a multi line field. * @param {Object} fieldDefinitions The list of field definitions for the current module. * @return {boolean} True or false. */ containsMultiLineFieldDef: function(multiLineFields, fieldDefinitions) { return _.isObject(_.find(multiLineFields, function(field) { var fieldName = field.getAttribute('fieldname'); return fieldName && this.isDefinedAsMultiLine(fieldName, fieldDefinitions); }, this)); }, /** * Will add a text hint about possible drag & drop to a multi line field. * * @param {jQuery} multiLineField The multi field into which the hint text should be inserted. */ addMultiFieldHint: function(multiLineField) { var pills = multiLineField.children('.pill'); var hint = multiLineField.children('.multi-field-hint'); if (!hint.length && pills.length == 0) { multiLineField.append( '<div class="multi-field-hint">' + app.lang.get('LBL_CONSOLE_MULTI_ROW_HINT', 'ConsoleConfiguration') + '</div>' ); } }, /** * It will create a new aggregated header text and label for a multi line field. * In case there are no fields in a multi line field the default values will be set. * * @param {jQuery} fields The pills found inside a multi line field. * @return {Object} A header title text and custom label. */ getNewHeaderDetails: function(fields) { var lbl = ''; var name = ''; var text = ''; var delimiter = ''; _.each(fields, function(field) { lbl += delimiter + field.getAttribute('fieldlabel'); name += delimiter + field.getAttribute('fieldname'); text += delimiter + field.getAttribute('data-original-title'); delimiter = '/'; }); return { fieldName: name || '', label: lbl || '', text: text || app.lang.get('LBL_CONSOLE_MULTI_ROW', this.module) }; }, /** * It will update a multi line field depending on the number of pills it contains. * If there are no fields inside, a hint will be displayed. If a field has been added, * the hint text will be removed. Additionally the multi line field header text will be changed. * * @param {jQuery} multiLineField A multi line field to be updated. */ updateMultiLineField: function(multiLineField) { var fields = multiLineField.children('.pill'); var headerDetails = this.getNewHeaderDetails(fields); if (fields.length) { multiLineField.children('.multi-field-hint').remove(); } var header = multiLineField.children('.list-header'); header.text(headerDetails.text).append(this.removeColIcon) .attr('data-original-title', headerDetails.text) .attr('fieldname', headerDetails.fieldName) .attr('fieldlabel', headerDetails.label); }, /** * Checks if a given field is defined as a multi-line field. * * @param {string} fieldName The name of the field to check. * @return {boolean} True if it is a multi line field definition. */ isDefinedAsMultiLine: function(fieldName) { var moduleName = this.model.get('enabled_module'); var fieldDefinitions = app.metadata.getModule(moduleName, 'fields'); return _.isObject(_.find(fieldDefinitions, function(field) { return field.multiline && field.type === 'widget' && field.name === fieldName; })); }, /** * Return the proper view metadata. If there is a default metadata we restore it, * otherwise we return the view metadata. * * @param {string} moduleName The selected module name from the available modules. * @return {Object} The default view meta or the multi line list metadata. */ getViewMetaData: function(moduleName) { var defaultViewMeta = this.context.get('defaultViewMeta'); return defaultViewMeta && defaultViewMeta[moduleName] ? defaultViewMeta[moduleName] : app.metadata.getView(moduleName, 'multi-line-list'); }, /** * Will cache and return the sortable list with the available fields. * * @return {jQuery} The available fields sortable lost node. */ getAvailableSortable: function() { var parentSelector = '#' + this.model.get('enabled_module') + '-side'; return this.availableSortable || (this.availableSortable = $(parentSelector).find('#fields-sortable')); }, /** * Gets the module's multi-line list fields from the model with the parent field mapping * * @return {Object} the fields */ getMappedFields: function() { var tabContentFields = {}; var whitelistedProperties = [ 'name', 'label', 'widget_name', ]; var multiLineMeta = this.getViewMetaData(this.model.get('enabled_module')); _.each(multiLineMeta.panels, function(panel) { _.each(panel.fields, function(fieldDefs) { var subfields = []; _.each(fieldDefs.subfields, function(subfield) { var parsedSubfield = _.pick(subfield, whitelistedProperties); // if label does not exist, get it from the parent's vardef if (!_.has(parsedSubfield, 'label')) { parsedSubfield.label = this.model.fields[parsedSubfield.name].label || this.model.fields[parsedSubfield.name].vname; } parsedSubfield.parent_name = fieldDefs.name; parsedSubfield.parent_label = fieldDefs.label; if (_.has(parsedSubfield, 'widget_name')) { parsedSubfield.name = parsedSubfield.widget_name; } subfields = subfields.concat(parsedSubfield); }, this); tabContentFields[fieldDefs.name] = _.has(tabContentFields, fieldDefs.name) ? tabContentFields[fieldDefs.name].concat(subfields) : subfields; }, this); }, this); return tabContentFields; }, /** * It will trigger an update on the multi lint list preview. To trigger the preview it needs a * list of selected fields based on the sortable list. In case the preview is triggered from a * multi field, we have have to climb higher to find the sortable list. */ triggerPreviewUpdate: function() { var domFieldList = this.$el.find('#columns-sortable'); if (!domFieldList.length) { domFieldList = this.$el.parent().parent().parent().find('#columns-sortable'); } this.context.trigger(this.previewEvent, this.getSelectedFieldList(domFieldList)); }, /** * Taking the dom list of fields, creates an accurate mapping of fields for the preview. * * @param {jQuery} node The DOM representation of the selected fields. * @return {Array} The list of selected fields. */ getSelectedFieldList: function(node) { var subFields; var fieldList = []; node.children().each(function(index, field) { if ($(field).hasClass('multi-field-block')) { subFields = []; $(field).find('.pill').each(function(index, subField) { subFields.push({ name: $(subField).attr('fieldname'), label: $(subField).attr('fieldlabel') }); }); if (subFields.length) { fieldList.push(subFields); } } else { fieldList.push([{ name: $(field).attr('fieldname'), label: $(field).attr('fieldlabel') }]); } }); return fieldList; }, }) }, "freeze-first-column": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ConsoleConfiguration.FreezeFirstColumnField * @alias SUGAR.App.view.fields.BaseConsoleConfigurationFreezeFirstColumnField * @extends View.Fields.Base.BoolField */ ({ // Freeze-first-column FieldTemplate (base) extendsFrom: 'BoolField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setupField(); }, /** * Set the field value on load to be checked/unchecked based on the saved config */ setupField: function() { let moduleName = this.model.get('enabled_module'); let consoleId = this.context.get('consoleId'); let freezeFirstColumn = this.context.get('model') ? this.context.get('model').get('freeze_first_column') : {}; let setValue = !_.isEmpty(freezeFirstColumn) && !_.isUndefined(freezeFirstColumn[consoleId]) && !_.isUndefined(freezeFirstColumn[consoleId][moduleName]) ? freezeFirstColumn[consoleId][moduleName] : true; this.model.set(this.name, setValue); } }) } }} , "views": { "base": { "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ConsoleConfiguration.ConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseConsoleConfigurationConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * The labels to be created when saving console configuration */ labelList: {}, /** * The column definitions to be saved when saving console configuration */ selectedFieldList: {}, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._viewAlerts = []; this.moduleLangObj = { // using "Console Configuration" for config title module: app.lang.get('LBL_CONSOLE_CONFIG_TITLE', this.module) }; }, /** * Displays alert message for invalid models */ showInvalidModel: function() { if (!this instanceof app.view.View) { app.logger.error('This method should be invoked by Function.prototype.call(), passing in as ' + 'argument an instance of this view.'); return; } var name = 'invalid-data'; this._viewAlerts.push(name); app.alert.show(name, { level: 'error', messages: 'ERR_RESOLVE_ERRORS' }); }, /** * @inheritdoc */ cancelConfig: function() { if (this.triggerBefore('cancel')) { if (app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } } }, /** * Process all the models of the collection and prepares the context * beans "order_by_primary" & "order_by_secondary" for save action */ _setOrderByFields: function() { var consoleId = this.context.get('consoleId'); var ctxModel = this.context.get('model'); var orderByPrimary = ctxModel.get('order_by_primary') || {}; orderByPrimary[consoleId] = !_.isEmpty(orderByPrimary[consoleId]) ? orderByPrimary[consoleId] : {}; var orderBySecondary = ctxModel.get('order_by_secondary') || {}; orderBySecondary[consoleId] = !_.isEmpty(orderBySecondary[consoleId]) ? orderBySecondary[consoleId] : {}; _.each(this.collection.models, function(model) { var moduleName = model.get('enabled_module'); orderByPrimary[consoleId][moduleName] = this._buildOrderByValue(model, 'order_by_primary'); orderBySecondary[consoleId][moduleName] = this._buildOrderByValue(model, 'order_by_secondary'); }, this); ctxModel.set({ order_by_primary: orderByPrimary, order_by_secondary: orderBySecondary, }, {silent: true}); }, /** * Process all the models of the collection and prepares the context * bean for save action */ _beforeSaveConfig: function() { var consoleId = this.context.get('consoleId'); var ctxModel = this.context.get('model'); // Get the current settings for the given console ID. If there are no // settings for the given console ID, create them var enabledModules = ctxModel.get('enabled_modules') || {}; enabledModules[consoleId] = !_.isEmpty(enabledModules[consoleId]) ? enabledModules[consoleId] : []; var filterDef = ctxModel.get('filter_def') || {}; filterDef[consoleId] = !_.isEmpty(filterDef[consoleId]) ? filterDef[consoleId] : {}; let freezeFirstColumn = ctxModel.get('freeze_first_column') || {}; freezeFirstColumn[consoleId] = !_.isEmpty(freezeFirstColumn[consoleId]) ? freezeFirstColumn[consoleId] : {}; // Update the variables holding the field values for the given console ID _.each(this.collection.models, function(model) { var moduleName = model.get('enabled_module'); let isFreezeColumn = model.get('freeze_first_column'); // model.get returns a string value so we convert it to boolean first isFreezeColumn = _.isString(isFreezeColumn) ? JSON.parse(isFreezeColumn) : isFreezeColumn; filterDef[consoleId][moduleName] = model.get('filter_def'); freezeFirstColumn[consoleId][moduleName] = isFreezeColumn; }, this); // to build the definitions of selected fields and labels this.buildSelectedList(); ctxModel.set({ is_setup: true, enabled_modules: enabledModules, labels: this.labelList, viewdefs: this.selectedFieldList, filter_def: filterDef, freeze_first_column: freezeFirstColumn }, {silent: true}); return this._super('_beforeSaveConfig'); }, /** * This build a view meta object for a module * * @param module * @return An object of view metadata */ buildViewMetaObject: function(module) { return { base: { view: { 'multi-line-list': { panels: [ { label: 'LBL_LABEL_1', fields: [] } ], // use the original collectionOptions and filterDef collectionOptions: app.metadata.getView(module, 'multi-line-list').collectionOptions || {}, filterDef: app.metadata.getView(module, 'multi-line-list').filterDef || {} } } } }; }, /** * This builds both field list and label list. */ buildSelectedList: function() { var self = this; var selectedList = {}; var labelList = {}; // the main ul elements of the selected list, one ul for each module $('.columns ul.field-list').each(function(idx, ul) { var module = $(ul).attr('module_name'); // init selectedList for this module selectedList[module] = self.buildViewMetaObject(module); // init labelList for this module labelList[module] = []; $(ul).children('li').each(function(idx2, li) { if (_.isEmpty($(li).attr('fieldname'))) { // multi field column selectedList[module].base.view['multi-line-list'].panels[0].fields .push(self.buildMultiFieldObject(li, module, labelList[module])); } else { // single field column selectedList[module].base.view['multi-line-list'].panels[0].fields .push(self.buildSingleFieldObject(li, module)); } }); }); this.selectedFieldList = selectedList; this.labelList = labelList; }, /** * * @param li The <li> element that represents the multi field column * @param module Module name * @param labelList The label list * @return Object */ buildMultiFieldObject: function(li, module, labelList) { var subfields = []; var header = $(li).find('li.list-header'); var self = this; // We may need to add the label to the system if it's a multi field column this.addLabelToList(header, module, labelList); // construct the field level definitions in subfields $(li).find('li.pill').each(function(idx2, li) { var field = {default: true, enabled: true}; var fieldname = $(li).attr('fieldname'); if (self.isSpecialField(fieldname, module)) { self.buildSpecialField(fieldname, field, module); } else { self.buildRegularField(li, field, module); } subfields.push(field); }); return { // column level definitions name: $(header).attr('fieldname'), label: $(header).attr('fieldlabel'), subfields: subfields }; }, /** * * @param header The header element * @param module Module name * @param labelList The list to be added to */ addLabelToList: function(header, module, labelList) { var label = $(header).attr('fieldlabel'); var labelValue = $(header).attr('data-original-title'); if (label == app.lang.get(label, module) && !_.isEmpty(labelValue)) { // label not already in system, add it to the list to save to system labelList.push({label: label, labelValue: labelValue}); } }, /** * * @param li The <li> element * @param module * @return Object */ buildSingleFieldObject: function(li, module) { var subfields = []; var field = {default: true, enabled: true}; var fieldname = $(li).attr('fieldname'); // construct the field level definitions in subfields if (this.isSpecialField(fieldname, module)) { this.buildSpecialField(fieldname, field, module); } else { this.buildRegularField(li, field, module); } subfields.push(field); return { // column level definitions name: $(li).attr('fieldname'), label: $(li).attr('fieldlabel'), subfields: subfields }; }, /** * To check if this is a special field. * @param fieldname * @param module * @return {boolean} true if it's a special field, false otherwise */ isSpecialField: function(fieldname, module) { var type = app.metadata.getModule(module, 'fields')[fieldname].type; return type == 'widget'; }, /** * To build the special field definitions. * @param fieldname The field name * @param field The field object to be populated * @param module The module name */ buildSpecialField: function(fieldname, field, module) { var console = app.metadata.getModule(module, 'fields')[fieldname].console; // copy everything from console for (property in console) { field[property] = console[property]; } field.widget_name = fieldname; }, /** * To build the regular field definitions * @param li The <li> element of a regular field. * @param field The field object to be populated * @param module The module name */ buildRegularField: function(li, field, module) { field.name = $(li).attr('fieldname'); field.label = $(li).attr('fieldlabel'); var fieldDef = app.metadata.getModule(module, 'fields')[field.name]; var type = fieldDef.type; field.type = type; if (!_.isEmpty(fieldDef.related_fields)) { field.related_fields = fieldDef.related_fields; } if (type === 'relate') { // relate field, get the actual field type var actualType = this.getRelateFieldType(field.name, module); if (!_.isEmpty(actualType) && actualType === 'enum') { // if the actual type is enum, need to add enum and enum_module field.type = actualType; field.enum_module = fieldDef.module; } else { // not enum type, add module and related_fields field.module = fieldDef.module; field.related_fields = fieldDef.related_fields || [fieldDef.id_name]; } field.link = false; } else if (type === 'name') { field.link = false; } else if (type === 'text') { if (_.isEmpty(fieldDef.dbType)) { // if type is text and there is no dbType (such as description field) // make it not sortable field.sortable = false; } } }, /** * To get the actual field type of a relate field. * @param fieldname * @param module * @return {string|*} */ getRelateFieldType: function(fieldname, module) { var fieldDef = app.metadata.getModule(module, 'fields')[fieldname]; if (!_.isEmpty(fieldDef) && !_.isEmpty(fieldDef.rname) && !_.isEmpty(fieldDef.module)) { return app.metadata.getModule(fieldDef.module, 'fields')[fieldDef.rname].type; } return ''; }, /** * Parses the 'order by' components of the given model for the given field * and concatenates them into the proper ordering string. Example: if the * primary sort field is 'name', and primary sort direction is 'asc', * it will return 'name:asc' * * @param {Object} model the model being saved * @param {string} the base field name * @private */ _buildOrderByValue: function(model, fieldName) { var value = model.get(fieldName) || ''; if (!_.isEmpty(value)) { var direction = model.get(fieldName + '_direction') || 'asc'; value += ':' + direction; } return value; }, /** * Calls the context model save and saves the config model in case * the default model save needs to be overwritten * * @protected */ _saveConfig: function() { this.validatedModels = []; this.getField('save_button').setDisabled(true); if (this.collection.models.length === 0) { this._setOrderByFields(); this._super('_saveConfig'); } else { async.waterfall([ _.bind(this.validateCollection, this) ], _.bind(function(result) { this.validatedModels.push(result); // doValidate() has finished on all models. if (this.collection.models.length === this.validatedModels.length) { var found = _.find(this.validatedModels, function(details) { return details.isValid === false; }); if (found) { this.showInvalidModel(); this.getField('save_button').setDisabled(false); } else { this._setOrderByFields(); this._super('_saveConfig'); } } }, this)); } }, /** * Validates all the models in the collection using the validation tasks */ validateCollection: function(callback) { var fieldsToValidate = {}; var allFields = this.getFields(this.module, this.model); for (var fieldKey in allFields) { if (app.acl.hasAccessToModel('edit', this.model, fieldKey)) { _.extend(fieldsToValidate, _.pick(allFields, fieldKey)); } } _.each(this.collection.models, function(model) { model.doValidate(fieldsToValidate, function(isValid) { callback({modelId: model.id, isValid: isValid}); }); }, this); } }) }, "config-tab-settings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ConsoleConfiguration.ConfigPaneView * @alias SUGAR.App.view.views.BaseConsoleConfigurationConfigPanelView * @extends View.Fields.Base.BaseField */ ({ // Config-tab-settings View (base) extendsFrom: 'BaseConfigPanelView', selectedModules: [], activeTabIndex: 0, /** * @inheritdoc */ bindDataChange: function() { this.collection.on('add remove reset', this.render, this); }, /** * @inheritdoc */ render: function() { var self = this; this._super('render'); this.toggleFreezeColumn(); this.$('#tabs').tabs({ active: this.context.get('activeTabIndex'), classes: { 'ui-tabs-active': 'active', }, // when selecting another tab, show/hide the corresponding side [ane div accordingly activate: function(event, ui) { var index = self.$('#tabs').tabs('option', 'active'); var sidePanes = $('.config-side-pane-all .config-side-pane'); _.each(sidePanes, function(sidePane) { $(sidePane).css('display', 'none'); }); $(sidePanes[index]).css('display', 'flex'); } }); }, /** * Show/hide the Freeze first column config for the user based on the admin settings */ toggleFreezeColumn: function() { if (!app.config.allowFreezeFirstColumn) { let freezeElem = this.$('.freeze-config') || {}; let freezeCell = freezeElem.length > 0 && freezeElem.closest('.row-fluid') ? freezeElem.closest('.row-fluid') : {}; if (freezeCell.length > 0) { let freezeCellIndex = freezeCell.index(); let configParentElem = freezeCell.parent() || {}; // get the header label element for freeze option let fieldHeader = configParentElem.length > 0 && configParentElem.children() ? configParentElem.children().eq(freezeCellIndex - 1) : {}; fieldHeader.hide(); freezeCell.hide(); } } } }) }, "config-side-pane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ConsoleConfiguration.ConfigSidePaneView * @alias SUGAR.App.view.views.BaseConsoleConfigurationConfigSidePanelView * @extends View.Fields.Base.BaseView */ ({ // Config-side-pane View (base) extendsFrom: 'BaseConfigPanelView', /** * @inheritdoc */ render: function() { this._super('render'); // Based on the active tab, show the corresponding config-side-pane div var index = this.context.get('activeTabIndex'); var sidePanes = this.$('.config-side-pane-all .config-side-pane'); $(sidePanes[index]).css('display', 'flex'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ConsoleConfigurationConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseConsoleConfigurationConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'BaseConfigDrawerLayout', plugins: ['ErrorDecoration'], /** * Holds a list of all modules with multi-line list views that can be * configured using the Console Configurator */ supportedModules: ['Accounts', 'Cases', 'Opportunities'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setAllowedModules(); // Unless a console ID was passed in with the context, parse the console // information from the parent context it was opened from if (!this.context.get('consoleId')) { this._parseConsoleContext(); } }, /** * Extracts console information from the parent console context */ _parseConsoleContext: function() { var consoleContext = this.context.parent; if (consoleContext) { this.context.set({ consoleId: consoleContext.get('modelId'), consoleTabs: this._parseConsoleTabs(consoleContext.get('tabs')) }); } }, /** * Parses a list of console tabs from the console context to extract the * names of the modules used in the console * @param tabsArray * @return {Array} */ _parseConsoleTabs: function(tabsArray) { var modules = []; _.each(tabsArray, function(tab) { var tabComponents = tab.components || []; _.each(tabComponents, function(component) { if (component.view === 'multi-line-list' && component.context && component.context.module) { modules.push(component.context.module); } }); }); return modules; }, /** * @inheritdoc */ bindDataChange: function() { this.context.on('consoleconfiguration:config:model:add', this.addModelToCollection, this); this.context.on('consoleconfiguration:config:model:remove', this.removeModelFromCollection, this); }, /** * Returns the list of modules the user has access to * and are supported. * * @return {Array} The list of module names. */ getAvailableModules: function() { var moduleNames = app.metadata.getModuleNames(); // Get the configured list of currently enabled modules for the tab. // If there is no setting saved yet for this, use the list of modules // parsed from the parent console context var selectedModules = this.model.get('enabled_modules')[this.context.get('consoleId')] || this.context.get('consoleTabs'); return _.filter(selectedModules, function(module) { return _.contains(moduleNames, module); }); }, /** * Sets up the models for each of the enabled modules from the configs */ loadData: function(options) { if (!this.checkAccess()) { this.blockModule(); return; } // Get the modules that are currently enabled for the console var availableModules = this.getAvailableModules(); // Get the ID of the console in order to load the settings for the // correct console var consoleId = this.context.get('consoleId'); // Load the settings saved for this particular console ID. If no settings // are saved yet for this console, create them var orderByPrimary = this.model.get('order_by_primary')[consoleId] || {}; var orderBySecondary = this.model.get('order_by_secondary')[consoleId] || {}; var filterDef = this.model.get('filter_def')[consoleId] || {}; _.each(availableModules, function(moduleName) { // Parse the sort fields and directions from the 'order by' setting var orderByPrimaryComponents = this._parseOrderByComponents(orderByPrimary[moduleName] || ''); var orderBySecondaryComponents = this._parseOrderByComponents(orderBySecondary[moduleName] || ''); var data = { defaults: this._getModelDefaults(consoleId, moduleName), enabled: true, enabled_module: moduleName, order_by_primary: orderByPrimaryComponents[0] || '', order_by_primary_direction: orderByPrimaryComponents[1] || 'asc', order_by_secondary: orderBySecondaryComponents[0] || '', order_by_secondary_direction: orderBySecondaryComponents[1] || 'asc', filter_def: filterDef[moduleName] || [], }; this.addModelToCollection(moduleName, data); }, this); this.setActiveTabIndex(); }, /** * Takes a stored order_by value and splits it into field name and direction * * @param value * @return {Array} an array containing the order_by field and direction data * @private */ _parseOrderByComponents: function(value) { if (_.isString(value)) { return value.split(':'); } return []; }, /** * Utility function to get an object containing a mapping of * {field name} => {default value} for the given console and module tab * * @param {string} consoleId the ID of the console to grab default settings for * @param {string} moduleName the module tab name to grab default settings for * @return {Object} containing the mapping, which can be used directly by the * tab's model.set() function * @private */ _getModelDefaults: function(consoleId, moduleName) { var config = app.metadata.getModule('ConsoleConfiguration', 'config') || {}; var defaults = config.defaults || {}; var defaultAttributes = {}; _.each(defaults, function(value, key) { if (_.isObject(value) && _.isObject(value[consoleId]) && !_.isUndefined(value[consoleId][moduleName])) { if (key === 'order_by_primary' || key === 'order_by_secondary') { var orderByComponents = this._parseOrderByComponents(value[consoleId][moduleName]); defaultAttributes[key] = orderByComponents[0] || ''; defaultAttributes[key + '_direction'] = orderByComponents[1] || 'desc'; } else { defaultAttributes[key] = value[consoleId][moduleName]; } } }, this); return defaultAttributes; }, /** * Checks ConsoleConfiguration ACLs to see if the User is a system admin * or if the user has a developer role for the ConsoleConfiguration module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().ConsoleConfiguration; var isSysAdmin = (app.user.get('type') == 'admin'); var isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isDev); }, /** * Sets the allowed modules that the admin are allowed to configure */ setAllowedModules: function() { var moduleDetails = {}; var allowedModules = this.supportedModules; var modules = {}; _.each(allowedModules, function(module) { moduleDetails = app.metadata.getModule(module); if (moduleDetails && !moduleDetails.isBwcEnabled && !_.isEmpty(moduleDetails.fields)) { modules[module] = app.lang.getAppListStrings('moduleList')[module]; } }); this.context.set('allowedModules', modules); }, /** * Sets the active tab */ setActiveTabIndex: function() { if (this.collection.length >= 1) { var activeParentTabIndex = this.context.parent.get('activeTab'); // get active index based on current tab name selected on the console var activeIndex = activeParentTabIndex > 0 ? activeParentTabIndex - 1 : 0; this.context.set('activeTabIndex', activeIndex); } }, /** * Removes a model from the collection and triggers events * to re-render the components * @param {string} module Module Name */ removeModelFromCollection: function(module) { var modelToDelete = _.find(this.collection.models, function(model) { return model.get('enabled_module') === module; }); if (!_.isEmpty(modelToDelete)) { this.collection.remove(modelToDelete); this.setActiveTabIndex(); } }, /** * Adds a model from the collection and triggers events * to re-render the components * @param {string} module Module Name * @param {Object} data Model data to add to the collection */ addModelToCollection: function(module, data) { var data = data || {}; var existingBean = _.find(this.collection.models, function(model) { if (_.contains(_.keys(this.context.get('allowedModules')), module)) { return model.get('enabled_module') === module; } }, this); if (_.isEmpty(existingBean)) { var bean = app.data.createBean(this.module, { defaults: data.defaults || {}, enabled: data.enabled || true, enabled_module: data.module || module, order_by_primary: data.order_by_primary || '', order_by_primary_direction: data.order_by_primary_direction || 'asc', order_by_secondary: data.order_by_secondary || '', order_by_secondary_direction: data.order_by_secondary_direction || 'asc', filter_def: data.filter_def || '', }); this.setTabContent(bean); this.setFilterableFields(bean); this.addValidationTasks(bean); bean.on('change:columns', function() { this.setTabContent(bean, true); this.setSortValues(bean); }, this); this.collection.add(bean); } this.setActiveTabIndex(); }, /** * Sets the filterable fields * @param bean */ setFilterableFields: function(bean) { var module = bean.get('enabled_module'); var filterableFields = app.data.getBeanClass('Filters').prototype.getFilterableFields(module); bean.set('filterableFields', filterableFields); }, /** * Sets the tab content for the module on the bean * * @param {Object} bean to edit/add to the collection * @param {boolean} update Flag to show if it's the updating of bean */ setTabContent: function(bean, update) { update = update || false; var tabContent = {}; var module = bean.get('enabled_module'); var multiLineFields = update ? this.getColumns(bean) : this._getMultiLineFields(module); // Set the information about the tab's fields, including which fields // can be used for sorting var fields = {}; var sortFields = {}; var nonSortableTypes = ['id', 'widget']; _.each(multiLineFields, function(field) { if (_.isObject(field) && app.acl.hasAccess('read', module, null, field.name)) { // Set the field information fields[field.name] = field; // Set the sort field information if the field is sortable var label = app.lang.get(field.label || field.vname, module); var isSortable = !_.isEmpty(label) && field.sortable !== false && field.sortable !== 'false' && nonSortableTypes.indexOf(field.type) === -1; if (isSortable) { sortFields[field.name] = label; } } }); tabContent.fields = fields; tabContent.sortFields = sortFields; bean.set('tabContent', tabContent); bean.trigger('change:tabContent'); }, /** * Sets values of the sortable fields * * @param {Object} bean */ setSortValues: function(bean) { const sortValue1 = bean.get('order_by_primary'); const sortValue2 = bean.get('order_by_secondary'); const columns = this.getColumns(bean); if (sortValue2 && !columns[sortValue2]) { bean.set('order_by_secondary', ''); } if (sortValue1 && !columns[sortValue1]) { if (sortValue2) { bean.set('order_by_primary', sortValue2); bean.set('order_by_secondary', ''); } else { bean.set('order_by_primary', ''); } } }, /** * Return values of the sortable fields using selected columns and metadata * * @param {Object} bean * @return {Object} a list fields by selected columns */ getColumns: function(bean) { const module = bean.get('enabled_module'); var columns = bean.get('columns'); var moduleFields = app.metadata.getModule(module, 'fields'); _.each(columns, function(field, key) { // add related_fields from widgets, they should be sortable if (!_.isEmpty(field.console) && !_.isEmpty(field.console.related_fields)) { var relatedFields = field.console.related_fields; _.each(relatedFields, function(field) { if (_.isEmpty(columns[field]) && !_.isEmpty(moduleFields[field])) { columns[field] = moduleFields[field]; } }); } }); return columns; }, /** * Gets a unique list of the underlying fields contained in a multi-line list * @param module * @return {Array} a list of field definitions from the multi-line list metadata * @private */ _getMultiLineFields: function(module) { // Get the unique lists of subfields and related_fields from the multi-line // list metadata of the module var multiLineMeta = app.metadata.getView(module, 'multi-line-list'); var moduleFields = app.metadata.getModule(module, 'fields'); var subfields = []; var relatedFields = []; _.each(multiLineMeta.panels, function(panel) { var panelFields = panel.fields; _.each(panelFields, function(fieldDefs) { subfields = subfields.concat(fieldDefs.subfields); _.each(fieldDefs.subfields, function(subfield) { if (subfield.related_fields) { var related = _.map(subfield.related_fields, function(relatedField) { return moduleFields[relatedField]; }); relatedFields = relatedFields.concat(related); } }); }, this); }, this); // To filter out special fields as they should not be available for sorting or filtering. subfields = _.filter(subfields, function(field) { return _.isEmpty(field.widget_name); }); // Return the combined list of subfields and related fields. Ensure that // the correct field type is associated with the field (important for // filtering) var fields = _.compact(_.uniq(subfields.concat(relatedFields), false, function(field) { return field.name; })); return _.map(fields, function(field) { if (moduleFields[field.name]) { field.type = moduleFields[field.name].type; } return field; }); }, /** * Adds validation tasks to the fields in the layout for the enabled modules */ addValidationTasks: function(bean) { if (bean !== undefined) { bean.addValidationTask('check_order_by_primary', _.bind(this._validatePrimaryOrderBy, bean)); } else { _.each(this.collection.models, function(model) { model.addValidationTask('check_order_by_primary', _.bind(this._validatePrimaryOrderBy, model)); }, this); } }, /** * Validates table header values for the enabled module * * @protected */ _validatePrimaryOrderBy: function(fields, errors, callback) { if (_.isEmpty(this.get('order_by_primary'))) { errors.order_by_primary = errors.order_by_primary || {}; errors.order_by_primary.required = true; } callback(null, fields, errors); } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.ConsoleConfigurationConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseConsoleConfigurationConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'BaseConfigDrawerContentLayout', /** * @inheritdoc */ _render: function() { this._super('_render'); this.$el.addClass('record-panel'); } }) } }} , "datas": {} }, "SugarLive":{"fieldTemplates": { "base": { "selected-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.SugarLive.SelectedFieldListField * @alias SUGAR.App.view.fields.BaseSugarLiveSelectedFieldListField * @extends View.Fields.Base.BaseField */ ({ // Selected-field-list FieldTemplate (base) removeFldIcon: '<i class="sicon sicon-remove console-field-remove"></i>', events: { 'click .sicon.sicon-remove.console-field-remove': 'removePill' }, /** The list of the fields selected. */ selectedFields: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setSelectedFields(); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered when the config is reset. */ bindDataChange: function() { var module = this.model.fieldModule; this.resetListener = _.bind(this.resetToDefaults, this); this.context.on('sugarlive:resetpreview:' + module, this.resetListener); }, /** * Will reset the available fields to the default value. */ resetToDefaults: function() { var module = this.model.fieldModule; var defaults = app.metadata.getView('', 'omnichannel-detail'); this.setSelectedFields(defaults); this.render(); }, /** * From the list of all fields for a module find the fields that must appear selected. * * @param {Object} meta The metadata for the given module. */ setSelectedFields: function(meta) { this.selectedFields = []; var moduleName = this.model.fieldModule; var allFields = app.metadata.getModule(moduleName, 'fields'); meta = meta || app.metadata.getView(moduleName, 'omnichannel-detail'); var fieldsToSelect = _.map(meta.fields, function(field) { return field.name; }); _.each(fieldsToSelect, function(fieldName) { this.selectedFields.push({ 'name': fieldName, 'label': (allFields[fieldName].label || allFields[fieldName].vname), 'displayName': app.lang.get(allFields[fieldName].label || allFields[fieldName].vname, moduleName) }); }, this); }, /** * Removes a pill from the selected fields list. * * @param {e} event Remove icon click event. */ removePill: function(event) { var pill = event.target.parentElement; event.target.remove(); $(pill).addClass('pill outer'); this.getAvailableSortable().append(pill); this.handleColumnsChanging(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.initDragAndDrop(); this.collection.trigger('preview'); }, /** * Initialize drag & drop for the selected field (main) list. */ initDragAndDrop: function() { var sortableEl = this.$('#columns-sortable'); sortableEl.sortable({ cursor: 'move', items: '.outer.pill', containment: 'parent', connectWith: '.connectedSortable', receive: _.bind(this.handleDrop, this), update: _.bind(this.handleColumnsChanging, this) }); }, /** * Event handler for the field drag & drop. The event is fired when an item is dropped to a list. * When moving a field from the right to the left we add the remove icon. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleDrop: function(event, ui) { if ('fields-sortable' == ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } }, /** * Trigger a preview event. */ handleColumnsChanging: function() { this.collection.trigger('preview'); }, /** * Will cache and return the sortable list with the available fields. * * @return {jQuery} The available fields sortable lost node. */ getAvailableSortable: function() { var parentSelector = '#' + this.model.fieldModule + '-side'; return this.availableSortable || (this.availableSortable = $(parentSelector).find('#fields-sortable')); }, /** * @inheritdoc * Remove the preview event listener. */ _dispose: function() { var module = this.model.fieldModule; this.context.off('sugarlive:resetpreview:' + module, this.resetListener); this._super('_dispose'); } }) }, "available-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.SugarLive.AvailableFieldListField * @alias SUGAR.App.view.fields.BaseSugarLiveAvailableFieldListField * @extends View.Fields.Base.BaseField */ ({ // Available-field-list FieldTemplate (base) /** * Fields with these names should not be displayed in fields list. */ ignoredNames: ['deleted', 'mkto_id', 'googleplus', 'team_name', 'auto_invite_parent', 'contact_name', 'invitees'], /** * Fields with these types should not be displayed in fields list. */ ignoredTypes: ['id', 'link', 'tag', 'parent', 'parent_type'], /** * List of fields that are displayed for a given module. */ availableFields: [], /** * @inheritdoc * * Collects all supported fields for all available modules and sets the module specific fields to be displayed. */ initialize: function(options) { this._super('initialize', [options]); this.setAvailableFields(); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered when the config is reset. */ bindDataChange: function() { var module = this.model.fieldModule; this.resetListener = _.bind(this.resetToDefaults, this); this.context.on('sugarlive:resetpreview:' + module, this.resetListener); }, /** * Will reset the available fields to the default value. */ resetToDefaults: function() { var defaultFields = app.metadata.getView('', 'omnichannel-detail'); this.setAvailableFields(defaultFields); this.render(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.handleDragAndDrop(); }, /** * Gets the list of fields that might be available for the given module. * * @param {string} moduleName The selected module name from the available modules. * @return {Array} A list of field defintions that can be rendered. */ getAllFields: function(moduleName) { var allFields = []; var metaFields = app.metadata.getModule(moduleName, 'fields'); _.each(metaFields, function(field) { if (this.isFieldSupported(field)) { allFields.push({ 'name': field.name, 'label': (field.label || field.vname), 'displayName': app.lang.get(field.label || field.vname, moduleName) }); } }, this); // Sort available fields alphabetically return _.sortBy(allFields, 'displayName'); }, /** * Sets the fields that are available for selection. * * @param {Object} meta The metadata for the given module. */ setAvailableFields: function(meta) { var moduleName = this.model.fieldModule; var allFields = this.getAllFields(moduleName); meta = meta || app.metadata.getView(moduleName, 'omnichannel-detail') || {}; var selectedFields = _.map(meta.fields, function(field) { return field.name; }); this.availableFields = _.filter(allFields, function(field) { return !_.contains(selectedFields, field.name); }); }, /** * Restricts specific fields to be shown in available fields list. * * @param {Object} field Field to be verified. * @return {boolean} True if field is supported, false otherwise. */ isFieldSupported: function(field) { // Specified fields names should be ignored. if (!field.name || _.contains(this.ignoredNames, field.name)) { return false; } // Specified field types should be ignored. if (_.contains(this.ignoredTypes, field.type) || field.dbType === 'id') { return false; } return !this.hasNoStudioSupport(field); }, /** * Verify if fields do not have available studio support. * Studio fields have multiple value types (array, bool, string, undefined). * * @param {Object} field Field selected to get verified. * @return {boolean} True if there is no support, false otherwise. */ hasNoStudioSupport: function(field) { // if it's a special field, do not check studio attribute if (!_.isUndefined(field.type) && field.type === 'widget') { return false; } var studio = field.studio; if (!_.isUndefined(studio)) { if (studio === 'false' || studio === false) { return true; } } return false; }, /** * Handles the dragging of the items from available fields list * to the columns list section, but not the other way around. */ handleDragAndDrop: function() { this.$('#fields-sortable').sortable({ connectWith: '.connectedSortable', receive: _.bind(this.cancelDrop, this) }); }, /** * Cancel the drag and drop. * * @param {Object} event Drop event object. * @param {Object} ui Ui tracker object from jquery. */ cancelDrop: function(event, ui) { ui.sender.sortable('cancel'); }, /** * @inheritdoc * Remove the preview listener. */ _dispose: function() { var module = this.model.fieldModule; this.context.off('sugarlive:resetpreview:' + module, this.resetListener); this._super('_dispose'); } }) } }} , "views": { "base": { "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.SugarliveConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseSugarliveConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * @inheritdoc */ _saveConfig: function() { this.getField('save_button').setDisabled(true); this._super('_saveConfig'); }, /** * @inheritdoc */ _beforeSaveConfig: function() { this.model.set({ viewdefs: this.buildSelectedList() }, {silent: true}); return this._super('_beforeSaveConfig'); }, /** * @inheritdoc */ cancelConfig: function() { if (this.triggerBefore('cancel')) { if (app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } } if (app.omniConsoleConfig) { app.omniConsoleConfig.open(); } }, /** * This builds a list of selected fields for SugarLive Summary Panel Configuration * @return {{}} */ buildSelectedList: function() { var selectedList = {}; // the main ul elements of the selected list, one ul for each module var fieldLists = document.querySelectorAll('.drawer.active .columns .field-list'); _.each(fieldLists, function(ul) { var module = ul.getAttribute('module_name'); // init selectedList for this module selectedList[module] = { base: { view: { 'omnichannel-detail': { fields: [] } } } }; _.each(ul.children, function(li) { var field = {}; field.name = li.getAttribute('fieldname'); field.label = li.getAttribute('fieldlabel'); var fieldDef = app.metadata.getField({name: field.name, module: module}); var fieldMeta = app.metadata._patchFields(module, app.metadata.getModule(module), [fieldDef]); selectedList[module].base.view['omnichannel-detail'].fields.push(_.first(fieldMeta)); }); }); return selectedList; } }) }, "config-side-pane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.SugarLive.ConfigSidePaneView * @alias SUGAR.App.view.views.BaseSugarLiveConfigSidePanelView * @extends View.Fields.Base.BaseView */ ({ // Config-side-pane View (base) extendsFrom: 'BaseConfigPanelView', fieldModels: [], events: { 'click .restore-defaults-btn': 'restoreClicked' }, /** * @inheritdoc */ render: function() { this.setFieldModels(); this._super('render'); // Based on the active tab, show the corresponding config-side-pane div. var index = this.context.get('activeTabIndex'); var sidePanes = this.$('.config-side-pane-all .config-side-pane'); $(sidePanes[index]).css('display', 'flex'); }, /** * Create a list of models based on the available modules for summary panel. * The modules are needed for rendering the available and selected list fields. */ setFieldModels: function() { var availableModules = this.getAvailableModules(); this.fieldModels = []; _.each(availableModules, function(module) { var fieldModel = app.data.createBean('SugarLive'); fieldModel.fieldModule = module; this.fieldModels.push(fieldModel); }, this); }, /** * Returns the list of modules the user has access to * and are supported. * * @return {Array} The list of module names. */ getAvailableModules: function() { var selectedModules = this.context.get('enabledModules'); return _.filter(selectedModules, function(module) { return !_.isEmpty(app.metadata.getModule(module)); }); }, /** * Trigger event for restoring default values. */ restoreClicked: function(e) { var module = e.target.dataset.module; this.context.trigger('sugarlive:resetpreview:' + module); } }) }, "config-preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.SugarliveConfigPreviewView * @alias SUGAR.App.view.views.BaseSugarliveConfigPreviewView * @extends View.Fields.Base.ConfigPanelView */ ({ // Config-preview View (base) extendsFrom: 'ConfigPanelView', /** * A list of input types that should be made readonly on the preview. */ inputTypes: ['input', 'select', 'checkbox', 'textarea'], /** * Fields to be displayed in omnichannel detail panel. * @property [Array] */ summaryFields: [ 'LBL_INVITEES', 'LBL_LIST_RELATED_TO', ], /** * Shows the active tab index used for when switching tabs. */ activeTabIndex: 0, /** * Information (titles and fields) to be displayed on distinct preview tabs. */ tabs: [], /** * @inheritdoc */ bindDataChange: function() { this.collection.on('add remove reset preview', this.render, this); }, /** * Returns the list of modules the user has access to * and are supported. * * @return {Array} The list of module names. */ getAvailableModules: function() { var selectedModules = this.context.get('enabledModules'); return _.filter(selectedModules, function(module) { return !_.isEmpty(app.metadata.getModule(module)); }); }, /** * Based on the config saved in the models it will collect the necessary * details for displaying a preview. */ setPreviewTabs: function() { this.tabs = {}; this.tabsLength = 0; var availableModules = this.getAvailableModules(); _.each(availableModules, function(module) { var fieldList = document.querySelector('.drawer.active #' + module + '-side .field-list') || []; var selectedFields = _.map(fieldList.children, function(li) { return li.getAttribute('fieldname'); }); this.tabsLength++; this.tabs[module] = {}; this.tabs[module].module = module; this.setPreviewTitle(module); this.setPreviewFields(module, selectedFields); }, this); }, /** * It will get the title texts to be displayed on the previews. * * @param {string} module The name of a module for which the titles should be retrieved. */ setPreviewTitle: function(module) { var tab = this.tabs[module]; moduleName = app.lang.getModuleName(module) + ' '; tab.title = moduleName + app.lang.get('LBL_SUGARLIVE_SUMMARY_PREVIEW', this.module); tab.detailTitle = moduleName + app.lang.get('LBL_SUGARLIVE_PREVIEW', this.module); }, /** * It will compile a list of field definitions to render. * * @param {string} module The name of the module for which the field list is compiled. * @param {Array} fields A list of field names that should appear on the preview. */ setPreviewFields: function(module, fields) { var tab = this.tabs[module]; var metaField = app.utils.deepCopy(app.metadata.getField({module: module})); // convert from vardefs field type to widget field type, this also patches labels app.metadata._patchFields(module, app.metadata.getModule(module), metaField); tab.fields = []; tab.model = app.data.createBean(module); _.each(fields, function(fieldName) { tab.fields.push({ name: fieldName, type: metaField[fieldName].type, label: metaField[fieldName].label, // Do not look for the related module (relate and custom relate types.) module: ' ', // Do not make requests for enum and extended enum types. options: [] }); }); }, /** * After render make all preview fields read only and remove any content that appears. */ disableInputs: function() { _.each(this.inputTypes, function(tagName) { var inputs = this.$el.find('.omni-cell ' + tagName); _.each(inputs, function(field) { field.setAttribute('readonly', true); field.setAttribute('placeholder', ''); field.className += ' edit-disabled'; }); }, this); }, /** * @inheritdoc */ render: function() { this.setPreviewTabs(); this._super('render'); this.disableInputs(); this.initTabs(); }, /** * Initialize the tabs with the active one. */ initTabs: function() { this.$('#tabs').tabs({ active: this.context.get('activeTabIndex'), activate: _.bind(this.setTabsDisplay, this) }); }, /** * It will set a tab as active. When selecting another tab, * show/hide the corresponding side pane div accordingly. */ setTabsDisplay: function() { var index = this.$('#tabs').tabs('option', 'active'); this.context.set('activeTabIndex', index); var sidePanes = this._getSidePanes(); if (sidePanes) { sidePanes.hide(); $(sidePanes[index]).css('display', 'flex'); } }, /** * Util to retrieve side panes from this config drawer layout * @private */ _getSidePanes: function() { var config = this.closestComponent('config-drawer'); if (config) { return config.$('.config-side-pane-all .config-side-pane'); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.SugarLiveConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseSugarLiveConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'BaseConfigDrawerLayout', /** * The list of modules this config is intended for. */ enabledModules: ['Calls', 'Messages'], /** * Check if we have access to the module. * If yes, allow the first tab to be displayed. */ loadData: function() { if (!this.checkAccess()) { this.blockModule(); return; } this.context.set('activeTabIndex', 0); this.context.set('enabledModules', this.enabledModules); }, /** * @override * To be able to configure the summary panel, we need at least the default values to exist. * This method needs to be overriden in order to allow operations. * * @return {boolean} True if we have the default metadata. */ _checkConfigMetadata: function() { return !_.isEmpty(app.metadata.getView('', 'omnichannel-detail')); }, }) } }} , "datas": {} }, "Quotas":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Teams":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TeamNotices":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Manufacturers":{"fieldTemplates": {} , "views": { "base": { "filter-filter-dropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-filter-dropdown View (base) extendsFrom: 'FilterFilterDropdownView', /** * @inheritdoc */ getFilterList: function() { var list = this._super('getFilterList').filter(function(obj) { if (obj.id == 'favorites') { return false; } return true; }); return list; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Activities":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Comments":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Subscriptions":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Bugs":{"fieldTemplates": {} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Bugs.RecordView * @alias SUGAR.App.view.views.BaseBugsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary', 'KBContent']); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Bugs.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBugsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Feeds":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "iFrames":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TimePeriods":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TaxRates":{"fieldTemplates": {} , "views": { "base": { "selection-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.TaxRates.SelectionListView * @alias SUGAR.App.view.views.BaseTaxRatesSelectionListView * @extends View.Views.Base.SelectionListView */ ({ // Selection-list View (base) extendsFrom: 'SelectionListView', /** * Extending to add the value into the attributes passed back * * @inheritdoc */ _getModelAttributes: function(model) { var attributes = { id: model.id, name: model.get('name'), value: model.get('value') }; //only pass attributes if the user has view access _.each(model.attributes, function(value, field) { if (app.acl.hasAccessToModel('view', model, field)) { attributes[field] = attributes[field] || model.get(field); } }, this); return attributes; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ContractTypes":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Schedulers":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Project":{"fieldTemplates": {} , "views": { "base": { "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Project.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseProjectActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) /** * @inheritdoc */ formatDate: function(date) { return date.formatUser(true); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ProjectTask":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Campaigns":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "CampaignLog":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "CampaignTrackers":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Documents":{"fieldTemplates": { "base": { "send-docusign": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Documents.SendDocusignField * @alias SUGAR.App.view.fields.BaseDocumentsSendDocusignField * @extends View.Fields.Base.RowactionField */ ({ // Send-docusign FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); }, /** * Set properties before init * * @param {Object} */ _beforeInit: function(options) { options.def.events = _.extend({}, options.def.events, { 'click [name=send-docusign]': 'sendToDocuSign', 'click [name=send-docusign-template]': 'sendToDocuSignTemplate' }); }, /** * Init properties */ _initProperties: function() { this.type = 'rowaction'; }, /** * Initiate the send process, by opening the tab */ sendToDocuSign: function(e) { var controllerCtx = app.controller.context; var controllerModel = controllerCtx.get('model'); var module = controllerModel.get('_module'); var modelId = controllerModel.get('id'); var documents = [this.model.id]; var recipients = []; var data = { returnUrlParams: { parentRecord: module, parentId: modelId, token: app.api.getOAuthToken() }, recipients: recipients, documents: documents }; app.events.trigger('docusign:send:initiate', data); }, /** * Initiate the composite send process, by opening the tab */ sendToDocuSignTemplate: function(e) { const controllerCtx = app.controller.context; const controllerModel = controllerCtx.get('model'); const module = controllerModel.get('_module'); const modelId = controllerModel.get('id'); const documents = [this.model.id]; const recipients = []; var data = { returnUrlParams: { parentRecord: module, parentId: modelId, token: app.api.getOAuthToken() }, recipients: recipients, documents: documents }; app.events.trigger('docusign:compositeSend:initiate', data, 'selectTemplate'); } }) }, "text": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Documents.TextField * @alias SUGAR.App.view.fields.BaseDocumentsTextField * @extends View.Fields.Base.TextField */ ({ // Text FieldTemplate (base) extendsFrom: 'TextField', /** * @inheritdoc */ initialize: function(options) { if (options && options.def && options.def.link) { this.plugins.push('FocusDrawer'); } // The revision field should only be editable when the Document is first being created if (options.def.name === 'revision' && options.view.action === 'create') { options.def.readonly = false; } this._super('initialize', [options]); }, /** * Used by the FocusDrawer plugin to get the ID of the record this field * links to * * @return {string} the ID of the related record */ getFocusContextModelId: function() { return this.model && this.model.get('id') ? this.model.get('id') : ''; }, /** * Used by the FocusDrawer plugin to get the name of the module this * field links to * * @return {string} the name of the related module */ getFocusContextModule: function() { return this.model && this.model.get('_module') ? this.model.get('_module') : ''; }, /** * Used by the FocusDrawer plugin to get the name of the record this * field links to * * @return {string} the name of the related record */ getFocusContextTitle: function() { return this.model && this.model.get('document_name') ? this.model.get('document_name') : ''; }, }) }, "relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Documents.RelateField * @alias SUGAR.App.view.fields.BaseDocumentsRelateField * @extends View.Fields.Base.RelateField */ ({ // Relate FieldTemplate (base) extendsFrom: 'BaseRelateField', /** * Formats the filter options for related_doc_rev_number field. * * @param {boolean} force `true` to force retrieving the filter options whether or not it is available in memory. * @return {Object} The filter options. */ getFilterOptions: function(force) { if (this.name && this.name === 'related_doc_rev_number' && this.model && !_.isEmpty(this.model.get('related_doc_id'))) { return new app.utils.FilterOptions() .config({ 'initial_filter': 'revisions_for_doc', 'initial_filter_label': 'LBL_REVISIONS_FOR_DOC', 'filter_populate': { 'document_id': [this.model.get('related_doc_id')], }, }) .format(); } else { return this._super('getFilterOptions', [force]); } }, /** * Provide date formatting for relate field in list view 'last_rev_create_date' since it is both a relate field * and datetime * @inheritdoc */ format: function(value) { value = this._super('format', [value]); // Checking for metadata param set to determine if it should be displayed as a normal date if (this.action === 'list' && this.def.list_display_type === 'datetime') { value = app.date(value); if (!value.isValid()) { return ''; } value = value.formatUser(false); } return value; }, }) }, "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Documents.NameField * @alias SUGAR.App.view.fields.BaseDocumentsNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) extendsFrom: 'NameField', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (!this.model) { return; } // Fill in the document name if its blank the user selects a file this.model.on('change:filename', function() { if (!this.model.get('document_name')) { this.model.set('document_name', this.model.get('filename')); this.render(); } }, this); } }) } }} , "views": { "base": { "subpanel-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Documents.SubpanelListView * @alias SUGAR.App.view.views.BaseDocumentsSubpanelListView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-list View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ initialize: function(options) { this.plugins = this.plugins || []; this.plugins.push('CloudDrive'); this._super('initialize', arguments); this.addCloudRowActions(); }, /** * Add cloud syncing buttons */ addCloudRowActions: function() { let dropdown = this._getSubpanelDropdown(); dropdown.push({ 'type': 'rowaction', 'event': 'button:sync_to_google:click', 'name': 'sync_to_google', 'label': 'LBL_SYNC_TO_GOOGLE_BUTTON_LABEL', 'acl_action': 'view', }, { 'type': 'rowaction', 'event': 'button:sync_to_onedrive:click', 'name': 'sync_to_google', 'label': 'LBL_SYNC_TO_ONEDRIVE_BUTTON_LABEL', 'acl_action': 'view', }, { 'type': 'rowaction', 'event': 'button:sync_to_dropbox:click', 'name': 'sync_to_google', 'label': 'LBL_SYNC_TO_DROPBOX_BUTTON_LABEL', 'acl_action': 'view', }); this.listenTo(this.context, 'button:sync_to_google:click', _.bind(this.syncDocToDrive, this, 'google')); this.listenTo(this.context, 'button:sync_to_onedrive:click', _.bind(this.syncDocToDrive, this, 'onedrive')); this.listenTo(this.context, 'button:sync_to_dropbox:click', _.bind(this.syncDocToDrive, this, 'dropbox')); }, /** * Sync everything to drive * * @param string type */ syncDocToDrive: function(type, model) { const driveDashlet = this._searchForDashlet(type); const driveDashletCid = driveDashlet.cid; const driveDashletPath = driveDashlet.pathFolders; if (!driveDashletCid) { app.alert.show('drive-error', { level: 'error', messages: app.lang.get('LBL_DRIVE_CLOUD_DASHLET_NOT_PRESENT'), }); return false; } let cache = app.cache.get(driveDashletCid); const module = model.module; const recordId = model.get('id'); let path = cache.folderId || 'root'; const url = app.api.buildURL('CloudDrive/files/syncFile'); app.alert.show('drive-syncing', { level: 'process' }); app.api.call('create', url, { module: module, recordId: recordId, path: path, driveId: cache.driveId, type: type, folderPath: driveDashletPath, }, { success: _.bind(this.syncDriveDashlet, this, driveDashletCid), error: function(error) { app.alert.show('drive-error', { level: 'error', messages: error.message, }); }, complete: function() { app.alert.dismiss('drive-syncing'); }, }); }, /** * Get dropdown for row-actions */ _getSubpanelDropdown: function() { if (_.has(this.meta, 'rowactions') && _.has(this.meta.rowactions, 'actions')) { return this.meta.rowactions.actions; } }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Documents.RecordView * @alias SUGAR.App.view.views.BaseDocumentsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.context.on('documentrevisions:save', this.onDocumentRevisionSave, this); }, /** * Handles the Document Revision subpanel save event. Updates the main document record with new model attributes */ onDocumentRevisionSave: function() { this.model.fetch(); }, }) }, "docusign-documents-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Documents.DocusignDocumentsListView * @alias SUGAR.App.view.views.BaseDocumentsDocusignDocumentsListView * @extends View.Views.Base.View */ ({ // Docusign-documents-list View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.filter(this.plugins, function(pluginName) { return pluginName !== 'ResizableColumns' && pluginName !== 'ReorderableColumns'; }); this._super('initialize', [options]); this.context.on('list:document:remove', this.handleDocumentRemove, this); }, _initializeMetadata: function() { return app.metadata.getView('Documents', 'docusign-documents-list') || {}; }, /** * @inheritdoc */ _loadTemplate: function(options) { this.tplName = 'recordlist'; this.template = app.template.getView(this.tplName); }, /** * @inheritdoc */ _render: function() { this.leftColumns = []; this._super('_render'); }, /** * Handle document remove * * @param {Object} model */ handleDocumentRemove: function(model) { this.collection.remove(model); }, /** * @inheritdoc */ setOrderBy: function() { return; }, /** * @inheritdoc */ freezeFirstColumn: function(event) { event.stopPropagation(); let freeze = $(event.currentTarget).is(':checked'); this.isFirstColumnFreezed = freeze; app.user.lastState.set(this._thisListViewUserConfigsKey, {freezeFirstColumn: freeze}); let $firstColumns = this.$('table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'); if (freeze) { $firstColumns.addClass('sticky-column stick-first'); } else { $firstColumns.removeClass('sticky-column stick-first no-border'); } this.showFirstColumnBorder(); }, /** * @inheritdoc */ showFirstColumnBorder: function() { if (!this.isFirstColumnFreezed) { this.hasFirstColumnBorder = false; return; } let scrollPanel = this.$('.flex-list-view-content')[0]; let firstColumnSelector = 'table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'; if (scrollPanel.scrollLeft === 0) { this.$(firstColumnSelector).addClass('no-border'); this.hasFirstColumnBorder = false; } else if (!this.hasFirstColumnBorder) { this.$(firstColumnSelector).removeClass('no-border'); this.hasFirstColumnBorder = true; } }, /** * @inheritdoc */ _dispose: function() { if (this.context) { this.context.off('list:document:remove'); } this._super('_dispose'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "panel-top": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Documents.PanelTopView * @alias SUGAR.App.view.views.BaseDocumentsPanelTopView * @extends View.Views.Base.PanelTopView */ ({ // Panel-top View (base) extendsFrom: 'PanelTopView', /** * @inheritdoc */ initialize: function(options) { this.plugins = this.plugins || []; this.plugins.push('CloudDrive'); this._super('initialize', arguments); this.addCloudButtons(); }, /** * Add cloud syncing buttons */ addCloudButtons: function() { let dropdown = this._getPanelDropdown(); dropdown.push({ 'type': 'rowaction', 'event': 'button:sync_all_to_google:click', 'name': 'sync_all_to_google', 'label': 'LBL_SYNC_ALL_TO_GOOGLE_BUTTON_LABEL', 'acl_action': 'view', }, { 'type': 'rowaction', 'event': 'button:sync_all_to_onedrive:click', 'name': 'sync_all_to_onedrive', 'label': 'LBL_SYNC_ALL_TO_ONEDRIVE_BUTTON_LABEL', 'acl_action': 'view', }, { 'type': 'rowaction', 'event': 'button:sync_all_to_dropbox:click', 'name': 'sync_all_to_onedrive', 'label': 'LBL_SYNC_ALL_TO_DROPBOX_BUTTON_LABEL', 'acl_action': 'view', }); this.listenTo(this.context, 'button:sync_all_to_google:click', _.bind(this.syncToDrive, this, 'google')); this.listenTo(this.context, 'button:sync_all_to_onedrive:click', _.bind(this.syncToDrive, this, 'onedrive')); this.listenTo(this.context, 'button:sync_all_to_dropbox:click', _.bind(this.syncToDrive, this, 'dropbox')); }, /** * Sync everything to drive * * @param string type */ syncToDrive: function(type) { const driveDashlet = this._searchForDashlet(type); const driveDashletCid = driveDashlet.cid; const driveDashletPath = driveDashlet.pathFolders; if (!driveDashletCid) { app.alert.show('drive-error', { level: 'error', messages: app.lang.get('LBL_DRIVE_CLOUD_DASHLET_NOT_PRESENT'), }); return false; } let cache = app.cache.get(driveDashletCid); const module = this.parentModule; const recordId = this.context.parent.get('modelId'); let path = cache.folderId || 'root'; const url = app.api.buildURL('CloudDrive', 'files/syncAll'); app.alert.show('drive-syncing', { level: 'process' }); app.api.call('create', url, { module: module, recordId: recordId, path: path, type: type, driveId: cache.driveId, folderPath: driveDashletPath, }, { success: _.bind(this.syncDriveDashlet, this, driveDashletCid), error: function(error) { app.alert.show('drive-error', { level: 'error', messages: error.message, }); }, complete: function() { app.alert.dismiss('drive-syncing'); }, }); }, /** * Get the dropdown for the panel-top */ _getPanelDropdown: function() { if (_.has(this.meta, 'buttons') && _.has(this.meta.buttons[0], 'buttons')) { return this.meta.buttons[0].buttons; } }, }) }, "docusign-documents-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Documents.DocusignDocumentsHeaderView * @alias SUGAR.App.view.views.BaseDocumentsDocusignDocumentsHeaderView * @extends View.Views.Base.View */ ({ // Docusign-documents-header View (base) className: 'docusign-documents-header', events: { 'click a[name=clear_button]': 'clear', 'click .addDocument': 'openDocumentsSelectionList', 'click .sendEnvelope': 'sendToDocuSign', 'click .selectTemplate': 'selectTemplate', 'click .sendWithTemplate': 'sendWithTemplate' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.listenTo(this.collection, 'change add remove', _.bind(this.toggleFieldsAccessability, this)); }, /** * Toggle fields accessability */ toggleFieldsAccessability: function() { var clearButton = this.$('a[name=clear_button]'); var sendButton = this.$('.sendEnvelope'); if (this.collection.models.length === 0) { clearButton.hide(); sendButton.addClass('disabled'); } else { clearButton.show(); sendButton.removeClass('disabled'); } }, /** * Clear collection */ clear: function() { if (this.collection.models.length === 0) { return; } this.collection.reset(); }, /** * Open documents selection list */ openDocumentsSelectionList: function() { app.drawer.open({ layout: 'multi-selection-list', context: { module: 'Documents', isMultiSelect: true } }, _.bind(function(models) { if (!models) { return; } this.collection.add(models); }, this)); }, /** * Send to DocuSign */ sendToDocuSign: function() { if (this.collection.models.length === 0) { return; } this.context.parent.trigger('sendDocumentsToDocuSign'); }, /** * Select template */ selectTemplate: function() { this.context.parent.trigger('selectTemplate', 'selectTemplate'); }, /** * Select template */ sendWithTemplate: function() { this.context.parent.trigger('sendWithTemplate', 'sendWithTemplate'); }, _render: function() { this._super('_render'); this.toggleFieldsAccessability(); } }) } }} , "layouts": {} , "datas": {} }, "DocumentRevisions":{"fieldTemplates": { "base": { "text": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocumentRevisions.TextField * @alias SUGAR.App.view.fields.BaseDocumentRevisionsTextField * @extends View.Fields.Base.TextField */ ({ // Text FieldTemplate (base) extendsFrom: 'TextField', /** * @inheritdoc * @override */ _initDefaultValue: function() { // Need to grab default values from parent context when creating a Document Revision if (this.name === 'latest_revision' && this.view.action === 'create' && this.context.parent && this.context.parent.get('model')) { var latestRev = this.context.parent.get('model').get('revision'); this.model.set('latest_revision', latestRev); } else if (this.name === 'revision' && this.view.action === 'create' && this.context.parent && this.context.parent.get('model')) { var rev = (parseInt(this.context.parent.get('model').get('revision')) + 1).toString(); this.model.set('revision', rev); } this._super('_initDefaultValue'); }, }) }, "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocumentRevisions.NameField * @alias SUGAR.App.view.fields.BaseDocumentRevisionsNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) extendsFrom: 'NameField', /** * @inheritdoc */ _render: function() { if (this.name === 'document_name' && this.view.action === 'create' && this.context.parent && this.context.parent.get('model')) { this.model.set('document_name', this.context.parent.get('model').get('document_name')); } this._super('_render'); }, }) }, "actiondropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocumentRevisions.ActiondropdownField * @alias SUGAR.App.view.fields.BaseDocumentRevisionsActiondropdownField * @extends View.Fields.Base.ActiondropdownField */ ({ // Actiondropdown FieldTemplate (base) extendsFrom: 'ActiondropdownField', /** * @inheritdoc */ _render: function() { this._super('_render'); // This is to ensure the single button renders with correct padding (since there is no dropdown for Doc Revs) if (this.module === 'DocumentRevisions') { this.$el.toggleClass('btn-group', true); } return this; }, }) } }} , "views": { "base": { "preview-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocumentRevisions.PreviewHeaderView * @alias SUGAR.App.view.views.BaseDocumentRevisionsPreviewHeaderView * @extends View.Views.Base.HeaderView */ ({ // Preview-header View (base) extendsFrom: 'HeaderView', /** * @inheritdoc * * @override Overriding to hide preview because we dont allow editing and dont have the correct fields to either * turn on or off * @private */ _renderFields: function() { // this forces the check in the parent to not enter the control structure and not attempt to render the buttons this.layout.previewEdit = false; this._super('_renderFields'); }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocumentRevisions.CreateView * @alias SUGAR.App.view.views.BaseDocumentRevisionsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Save and close drawer */ saveAndClose: function() { this.initiateSave(_.bind(function() { if (this.closestComponent('drawer')) { app.drawer.close(this.context, this.model); // Triggers an event on the Document record view to update field values to correspond with most recent // document revision this.context.parent.trigger('documentrevisions:save'); } else { app.navigate(this.context, this.model); } }, this)); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Connectors":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Notifications":{"fieldTemplates": { "base": { "severity": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Severity FieldTemplate (base) /** * Severity Widget. * * Extends from EnumField widget adding style property according to specific * severity. */ extendsFrom: 'EnumField', /** * An object where its keys map to specific severity and values to matching * CSS classes. * * @property {Object} * @protected */ _styleMapping: { 'default': 'label-unknown', alert: 'label-important', information: 'label-info', other: 'label-inverse', success: 'label-success', warning: 'label-warning' }, /** * @inheritdoc * * Listen to changes on `is_read` field only if view name matches * notifications. */ bindDataChange: function() { this._super('bindDataChange'); if (this.model && this.view.name === 'notifications') { this.model.on('change:is_read', this.render, this); } }, /** * @inheritdoc * * Inject additional logic to load templates based on different view names * according to the following: * * - `fields/severity/<view-name>-<tpl-name>.hbs` * - `fields/severity/<view-template-name>-<tpl-name>.hbs` */ _loadTemplate: function() { this._super('_loadTemplate'); var template = app.template.getField( this.type, this.view.name + '-' + this.tplName, this.model.module ); if (!template && this.view.meta && this.view.meta.template) { template = app.template.getField( this.type, this.view.meta.template + '-' + this.tplName, this.model.module ); } this.template = template || this.template; }, /** * @inheritdoc * * Defines `severityCss` property based on field value. If current severity * does not match a known value its value is used as label and default * style is used as well. */ _render: function () { var severity = this.model.get(this.name), options = app.lang.getAppListStrings(this.def.options); this.severityCss = this._styleMapping[severity] || this._styleMapping['default']; this.severityLabel = options[severity] || severity; this._super('_render'); } }) }, "html": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Notifications.HtmlField * @alias SUGAR.App.view.fields.BaseNotificationsHtmlField * @extends View.Fields.Base.HtmlField */ ({ // Html FieldTemplate (base) extendsFrom: 'BaseHtmlField', /** * For comment log notifications, we need to show who mentioned them * * @param {string} value The value to format * @return {string} * @override */ format: function(value) { if (value === 'LBL_YOU_HAVE_BEEN_MENTIONED_BY') { value = app.lang.get('LBL_YOU_HAVE_BEEN_MENTIONED_BY', this.module); var href = app.router.buildRoute('Employees', this.model.get('created_by'), 'detail', true); value += ' <a href="#' + href + '">' + _.escape(this.model.get('created_by_name')) + '</a>'; } else if (this.name === 'description') { value = DOMPurify.sanitize(value, {FORBID_TAGS: ['form']}); } return value; } }) }, "read": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Read FieldTemplate (base) events: { 'click [data-action=toggle]': 'toggleIsRead', 'mouseover [data-action=toggle]': 'toggleMouse', 'mouseout [data-action=toggle]': 'toggleMouse' }, /** * @inheritdoc * * The read field is always a readonly field. * * If `mark_as_read` option is enabled on metadata it means we should * automatically mark the notification as read. * */ initialize: function(options) { options.def.readonly = true; this._super('initialize', [options]); if (options.def && options.def.mark_as_read) { this.markAs(true); } }, /** * Event handler for mouse events. * * @param {Event} event Mouse over / mouse out. */ toggleMouse: function(event) { var $target= this.$(event.currentTarget), isRead = this.model.get('is_read'); if (!isRead) { return; } var label = event.type === 'mouseover' ? 'LBL_UNREAD' : 'LBL_READ'; $target.html(app.lang.get(label, this.module)); $target.toggleClass('label-inverse', event.type === 'mouseover'); }, /** * Toggle notification `is_read` flag. */ toggleIsRead: function() { this.markAs(!this.model.get('is_read')); }, /** * Mark notification as read/unread. * * @param {Boolean} read `True` marks notification as read, `false` as * unread. */ markAs: function(read) { if (read === this.model.get('is_read')) { return; } this.model.save({is_read: !!read}, { success: _.bind(function() { if (!this.disposed) { this.render(); } }, this) }); } }) } }} , "views": { "base": { "preview-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.NotificationsPreviewHeaderView * @alias SUGAR.App.view.views.BaseNotificationsPreviewHeaderView * @extends View.Views.Base.PreviewHeaderView */ ({ // Preview-header View (base) extendsFrom: 'PreviewHeaderView', /** * @inheritdoc * * @override To make 'previewEdit' always false. Notifications do not allow any editing (but not via module ACL). */ checkACL: function(model) { this._super('checkACL', [model]); this.layout.previewEdit = false; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Sync":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "HintAccountsets":{"fieldTemplates": {} , "views": { "base": { "dashletconfiguration-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.HintAccountsets.DashletconfigurationHeaderpaneView * @alias SUGAR.App.view.views.BaseHintAccountsetsDashletconfigurationHeaderpaneView * @extends View.Views.Base.DashletconfigurationHeaderpaneView */ ({ // Dashletconfiguration-headerpane View (base) extendsFrom: 'DashletconfigurationHeaderpaneView', events: { 'click a[name=save_button]': 'initSave', 'click a[name=cancel_button]': 'initClose' }, /** * @inheritdoc * From an UI perspective we have a single Save and Cancel button for dashlet configuration. * In reality (in the backround) we have 2 for each. In case of the save button: one will save * the configuration, the other will add the dashlet. It is similar for the cancel button. * The buttons from the UI are children of this component - the other set is not visible and are * children of the news preferences layout and handle the more complex configuration logic. */ initialize: function(options) { this._super('initialize', [options]); this.initModelValues(); this.bindNewsPreferencesEvents(); }, /** * The first phase of adding the dashlet. An event will be triggered which has to be caught by the * news preferences layout, which will run the more complex logic of saving the configuration. */ initSave: function() { app.events.trigger('news-preferences:save', 'save'); }, /** * The first phase of canceling the add of the dashlet. An event will be triggered which has to be * caught by the news preferences layout, which will check if there are any unsaved configurations. */ initClose: function() { app.events.trigger('news-preferences:cancel', 'cancel'); }, /** * We set the hint insights dashlet config here * so on save the dashlet could be generated automatically. */ initModelValues: function() { this.model.set({ componentType: 'view', config: true, label: 'LBL_HINT_NEWS_ALERT', limit: 20, module: 'Home', type: 'hint-news-dashlet' }, {silent: true}); }, /** * After the news preferences save/cancel logic has been executed, events will be * triggered and caught by the following listeners. The defaut save/close methods will be executed. */ bindNewsPreferencesEvents: function() { app.events.on('dashletconfig:news-preferences:save', _.bind(this.save, this)); app.events.on('dashletconfig:news-preferences:cancel', _.bind(this.close, this)); }, /** * @inheritdoc * Have to detach events listening to configuration save/cancel logic events. */ _dispose: function() { app.events.off('dashletconfig:news-preferences:save'); app.events.off('dashletconfig:news-preferences:cancel'); this._super('_dispose'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "HintNotificationTargets":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "HintNewsNotifications":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "HintEnrichFieldConfigs":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ReportMaker":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "DataSets":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "CustomQueries":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "pmse_Inbox":{"fieldTemplates": { "base": { "reassignbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Reassignbutton FieldTemplate (base) extendsFrom: 'RowactionField', initialize: function (options) { this._super("initialize", [options]); this.type = 'rowaction'; }, _render: function () { var value=this.model.get('cas_status'); // if(value==='TODO'){ if(/IN PROGRESS/.test(value)){ this._super("_render"); } else { this.hide(); } }, bindDataChange: function () { if (this.model) { this.model.on("change", this.render, this); } } }) }, "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.pmse_Inbox.EnumField * @alias SUGAR.App.view.fields.Basepmse_InboxEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ _render: function() { this.items = this.model.get('cas_reassign_user_combo_box'); this._super('_render'); } }) }, "sugarbpm-header-label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Label for trademarked `SugarBPM` term. */ ({ // Sugarbpm-header-label FieldTemplate (base) extendsFrom: 'LabelField' }) }, "executebutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Executebutton FieldTemplate (base) extendsFrom: 'RowactionField', initialize: function (options) { this._super("initialize", [options]); this.type = 'rowaction'; }, _render: function () { var value=this.model.get('cas_status'); if(/ERROR/.test(value)){ this._super("_render"); } else { this.hide(); } }, bindDataChange: function () { if (this.model) { this.model.on("change", this.render, this); } } }) }, "relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ // jscs:disable jsDoc /** * relate Widget. * * Extends from BaseRelateField widget * * @class View.Fields.Base.pmse_Inbox.RelateField * @alias SUGAR.App.view.fields.Basepmse_InboxRelateField * @extends View.Fields.Base.RelateField */ // jscs:anable jsDoc ({ // Relate FieldTemplate (base) /** * Renders relate field */ _render: function() { // a way to override viewName if (this.def.view) { this.options.viewName = this.def.view; } this._super('_render'); } }) }, "pmse-link": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Pmse-link FieldTemplate (base) /** * @inheritdoc */ _render: function() { var action = 'view'; if (this.def.link && this.def.route) { action = this.def.route.action; } if (((!app.acl.hasAccess('developer', this.model.get('cas_sugar_module')) || this.model.get('prj_deleted') == '1') && this.def.name == 'pro_title') || (!app.acl.hasAccess(action, this.model.get('cas_sugar_module')) && this.def.name == 'cas_title')) { this.def.link = false; } if (this.def.link) { this.href = this.buildHref(); } app.view.Field.prototype._render.call(this); }, _isErasedField: function() { var erased = false; if (this.def.name == 'cas_title') { var module; if (this.model.attributes.is_a_person) { module = this.model.module; this.model.module = this.model.attributes.cas_sugar_module; this.model.fields.name.type = 'fullname'; this.model.attributes.cas_title = app.utils.formatNameModel( this.model.attributes.cas_sugar_module, this.model.attributes ); } erased = app.utils.isNameErased(this.model); if (this.model.attributes.is_a_person) { this.model.module = module; } } return erased; }, buildHref: function() { var defRoute = this.def.route ? this.def.route : {}, module = this.model.module || this.context.get('module'); switch (this.def.name) { case 'pro_title': return '#' + app.router.buildRoute('pmse_Project', this.model.attributes.prj_id, defRoute.action, this.def.bwcLink); break; case 'cas_title': return '#' + app.router.buildRoute(this.model.attributes.cas_sugar_module, this.model.attributes.cas_sugar_object_id, defRoute.action, this.def.bwcLink); break; } }, unformat: function(value) { return _.isString(value) ? value.trim() : value; } }) }, "event-status-pmse": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * EventStatusField is a field for Meetings/Calls that show the status field of the model as a badge field. * * @class View.Fields.Base.EventStatusField * @alias SUGAR.App.view.fields.BaseEventStatusField * @extends View.Fields.Base.BadgeSelectField */ ({ // Event-status-pmse FieldTemplate (base) extendsFrom: 'BaseField', /** * @inheritdoc */ initialize: function (options) { this._super('initialize', [options]); }, /** * @inheritdoc * * Styles the badge. * * @private */ _render: function () { this._super('_render'); this.styleLabel(); }, /** * Sets the appropriate CSS class on the label based on the value of the * status. * * It is a noop when the field is in edit mode. * * @param {String} status */ styleLabel: function () { var $label; $label = this.$el.children(0); switch (this.value) { case 'IN PROGRESS': $label.addClass('label label-process-in-progress'); break; case 'COMPLETED': $label.addClass('label label-process-completed'); break; case 'TERMINATED': $label.addClass('label label-process-terminate'); break; case 'CANCELLED': $label.addClass('label label-process-cancelled'); break; default: $label.addClass('label label-process-error'); break; } } }) }, "cancelcasebutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Cancelcasebutton FieldTemplate (base) extendsFrom: 'RowactionField', initialize: function (options) { this._super("initialize", [options]); this.type = 'rowaction'; }, _render: function () { var value=this.model.get('cas_status'); // if(value==='TODO' || /Error/.test(value)){ if(/IN PROGRESS/.test(value) || /ERROR/.test(value)){ this._super("_render"); } else { this.hide(); } }, bindDataChange: function () { if (this.model) { this.model.on("change", this.render, this); } } }) } }} , "views": { "base": { "reassignCases-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Inbox.ReassignCasesHeaderpaneView * @alias SUGAR.App.view.views.Basepmse_InboxReassignCasesHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // ReassignCases-headerpane View (base) extendsFrom: "HeaderpaneView", events: { 'click [name=done_button]': '_done', 'click [name=cancel_button]': '_cancel' }, /** * Clicking the Done button will update the process with the new process user * and close the drawer * * @private */ _done: function() { // If collection.models is missing or empty then there is nothing to save if (_.isUndefined(this.collection.models) || (this.collection.models.length == 0)) { app.drawer.close(); return; } var attributes = {}; app.alert.show('saving', {level: 'process', title: 'LBL_SAVING', autoclose: false}); var url = app.api.buildURL('pmse_Inbox', 'reassignFlows', null, null); // we only have one model var model = _.first(this.collection.models); attributes.flow_data = [{ 'cas_id': model.get('cas_id'), 'cas_index': model.get('cas_index'), 'user_id': model.get('id') }]; app.api.call('update', url, attributes, { success: function (data) { app.alert.dismiss('saving'); app.drawer.close('saving'); app.router.refresh(); }, error: function (err) { } }); }, /** * Close the drawer. * * @private */ _cancel: function() { app.drawer.close(); } }) }, "config-log": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Config-log View (base) /** * @inheritdoc * * Sets up the file field to edit mode * * @param {View.Field} field * @private */ _renderField: function(field) { app.view.View.prototype._renderField.call(this, field); app.alert.show('txtConfigLog', {level: 'process', title: 'Loading', autoclose: false}); url = app.api.buildURL(this.module + '/logGetConfig'); app.api.call('READ', url, {},{ success: function(data) { field.model.set('comboLogConfig',data['records'][0]['cfg_value']); app.alert.dismiss('txtConfigLog'); } }); } }) }, "config-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Config-headerpane View (base) extendsFrom: "HeaderpaneView", events: { "click [name=save_button]": "_save", "click [name=cancel_button]": "_cancel" }, /** * Save the drawer. * * @private */ _save: function() { var fieldPmse=new Object(); fieldPmse.logger_level={name: "logger_level", required: true}; fieldPmse.error_timeout={name: "error_timeout", required: true, type: 'int'}; // console.log('mmm',fieldPmse); this.model.doValidate(fieldPmse, _.bind(this.validationCompleteSettings, this)); }, validationCompleteSettings: function(isValid) { var self=this; if (isValid) { app.alert.show('upload', {level: 'process', title: 'LBL_LOADING', autoclose: false}); var value = {}, data = {}; data.logger_level = self.model.get('logger_level'); data.error_timeout = self.model.get('error_timeout'); value.data = data; //console.log('Values->',value); var pmseInboxUrl = app.api.buildURL('pmse_Inbox/settings','',{},{}); app.api.call('update', pmseInboxUrl, value,{ success: function (data){ if(data.success){ app.alert.dismiss('upload'); // app.router.goBack(); app.router.navigate('#Administration',{trigger: true}); } } }); // console.log('Validado'); } }, /** * Close the drawer. * * @private */ _cancel: function() { app.router.navigate(app.router.buildRoute('Administration'), {trigger: true}); } }) }, "process-users-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Process-users-chart View (base) plugins: ['Dashlet', 'Chart'], processCollection: null, currentValue: null, chartCollection: null, hasData: false, total: 0, showProcesses: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.locale = SUGAR.charts.getUserLocale(); this.tooltipTemplate = app.template.getField('chart', 'singletooltiptemplate', 'Reports'); this.chart = sucrose.charts.pieChart() .margin({top: 5, right: 20, bottom: 20, left: 20}) .donut(true) .donutLabelsOutside(true) .donutRatio(0.447) .hole(this.total) .showTitle(false) .tooltips(true) .showLegend(true) .colorData('class') .tooltipContent(_.bind(function(eo, properties) { var point = {}; point.key = eo.key; point.label = app.lang.get('LBL_CHART_COUNT'); point.value = sucrose.utility.numberFormat(eo.value, this.locality.precision, false, this.locality); return this.tooltipTemplate(point).replace(/(\r\n|\n|\r)/gm, ''); }, this)) .strings({ legend: { close: app.lang.get('LBL_CHART_LEGEND_CLOSE'), open: app.lang.get('LBL_CHART_LEGEND_OPEN'), noLabel: app.lang.get('LBL_CHART_UNDEFINED') }, noData: app.lang.get('LBL_CHART_NO_DATA'), noLabel: app.lang.get('LBL_CHART_UNDEFINED') }) .locality(this.locale); this.locality = this.chart.locality(); }, initDashlet: function (view) { var self = this; // loading all Processes list this.showProcesses = !(this.settings.get('isRecord') === '1'); if (this.showProcesses) { app.api.call('GET', app.api.buildURL('pmse_Project/filter?fields=id,name'), null, { success: _.bind(function (data) { var options = {}; this.processCollection = data.records; this.processCollection.unshift({ id: 'all', name: app.lang.get('LBL_PMSE_ALL_PROCESSES_LABEL', 'pmse_Inbox') }); //Filling options _.each(this.processCollection, function (row) { options[row.id] = row.name; }); this.dashletConfig.processes_selector[0].options = options; this.currentValue = 'all'; this.layout.render(); this.layout.loadData(); }, this), complete: view.options ? view.options.complete : null }); this.settings.on('change:processes_selector', function (context, value) { self.currentValue = value; self.loadData(); }); } else { this.currentValue = this.model.get('id'); //this.loadData(); } }, hasChartData: function () { return this.hasData; }, /** * Generic method to render chart with check for visibility and data. * Called by _renderHtml and loadData. */ renderChart: function() { if (!this.isChartReady()) { return; } // Set value of label inside donut chart if is greater than zero if (this.total && this.total > 0) { this.chart.hole(this.total); } d3.select(this.el).select('svg#' + this.cid) .datum(this.chartCollection) .transition().duration(500) .call(this.chart); this.chart_loaded = _.isFunction(this.chart.update); this.displayNoData(!this.chart_loaded); }, /** * @inheritdoc */ loadData: function(options) { var self = this, url; if (this.meta.config) { return; } if (!this.currentValue) { return; } url = app.api.buildURL('pmse_Inbox/processUsersChart/' + this.currentValue); this.hasData = false; app.api.call('GET', url, null, { success: function(data) { self.evaluateResponse(data); self.renderChart(); }, complete: options ? options.complete : null }); }, evaluateResponse: function(response) { this.total = response.properties.total; this.hasData = !!this.total; response.data.map(function(d) { d.value = parseInt(d.value, 10); }); this.chartCollection = response; } }) }, "unattendedCases-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // UnattendedCases-list View (base) extendsFrom: 'RecordlistView', contextEvents: { "list:reassign:fire": "reassignCase" }, _render: function() { if (app.acl.hasAccessToAny('developer')) { this._super('_render'); } else { app.controller.loadView({ layout: 'access-denied' }); } }, reassignCase: function (model) { var self=this; app.drawer.open({ layout: 'reassignCases', context: { module: 'pmse_Inbox', parent: this.context, cas_id: model.get('cas_id'), unattended: true } }, function(variables) { if(variables==='saving'){ self.reloadList(); } }); }, reloadList: function() { this.context.reloadData({ recursive:false, }); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Inbox.RecordlistView * @alias SUGAR.App.view.views.Basepmse_InboxRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', contextEvents: { "list:process:fire": "showCase" }, showCase: function (model) { //var url = model.module + '/' + model.id + '/layout/show-case/' + model.get('flow_id'); var ShowCaseUrl = app.router.buildRoute(model.module, model.get('id2'), 'layout/show-case/' + model.get('flow_id')); var ShowCaseUrlBwc = app.bwc.buildRoute(model.module, '', 'showCase', {id:model.get('flow_id')}); var SugarModule = model.get('cas_sugar_module'); if (app.metadata.getModule(SugarModule).isBwcEnabled) { app.router.navigate(ShowCaseUrlBwc , {trigger: true, replace: true }); } else { app.router.navigate(ShowCaseUrl , {trigger: true, replace: true }); } }, /** * Decorate a row in the list that is being shown in Preview * @override pmse_Inbox uses flow_id instead of id to keep track of records * and add them to the DOM * @param model Model for row to be decorated. Pass a falsy value to clear decoration. */ decorateRow: function (model) { // If there are drawers, make sure we're updating only list views on active drawer. if (_.isUndefined(app.drawer) || app.drawer.isActive(this.$el)) { this._previewed = model; this.$("tr.highlighted").removeClass("highlighted current above below"); if (model) { //use flow_id here since that's what is in the DOM var rowName = model.module + "_" + model.get('flow_id'); var curr = this.$("tr[name='" + rowName + "']"); curr.addClass("current highlighted"); curr.prev("tr").addClass("highlighted above"); curr.next("tr").addClass("highlighted below"); } } } }) }, "reassignCases-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // ReassignCases-list View (base) extendsFrom: 'RecordlistView', initialize: function (options) { this._super("initialize", [options]); this.collection.on('data:sync:complete', this.loaded, this); }, loaded: function () { _.each(this.fields, function (field) { if(field.name === 'assigned_user') { this.auser = field.value; setTimeout(function () { var spans = document.getElementsByClassName('select2-chosen'); for (var i=0;i<spans.length;i++) { if (spans[i].innerText && (spans[i].innerText != app.lang.get('LBL_PMSE_FILTER', 'pmse_Inbox'))) { spans[i].innerText = this.auser; } else if (spans[i].textContent && (spans[i].textContent != app.lang.get('LBL_PMSE_FILTER', 'pmse_Inbox'))) { spans[i].textContent = this.auser; } } },2000); } }); } }) }, "config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Config View (base) // extendsFrom :'RecordView', // className: 'settings', events: { //'click .sugar-cube': 'spinCube' }, initialize: function(options) { if (app.acl.hasAccessToAny('developer')) { var self=this; var url = app.api.buildURL('pmse_Inbox', 'settings', null, options.params); app.api.call('READ', url, options.attributes, { success: function (data) { self.model.set(data); } }); this._super('initialize', [options]); } else { app.controller.loadView({ layout: 'access-denied' }); } } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', events: { 'click .record-edit-link-wrapper': 'handleEdit', 'click [data-action=scroll]': 'paginateRecord', 'click .record-panel-header': 'togglePanel', 'click .tab a': 'setActiveTab' }, initialize: function(options) { options.meta = _.extend({}, app.metadata.getView(null, 'record'), options.meta); options.meta.hashSync = _.isUndefined(options.meta.hashSync) ? true : options.meta.hashSync; this._super('initialize', [options]); this.context.on('approve:case', this.approveCase, this); this.context.on('reject:case', this.rejectCase, this); this.context.on('cancel:case', this.cancelCase, this); this.context.on('button:cancel_button:click', this.cancelClicked, this); //event register for preventing actions // when user escapes the page without confirming deleting // add a callback to close the alert if users navigate from the page app.routing.before('route', this.dismissAlert, this); $(window).on('beforeunload.delete' + this.cid, _.bind(this.warnDeleteOnRefresh, this)); this.delegateButtonEvents(); if (this.createMode) { this.model.isNotEmpty = true; } this.noEditFields = []; // properly namespace SHOW_MORE_KEY key this.MORE_LESS_KEY = app.user.lastState.key(this.MORE_LESS_KEY, this); this.adjustHeaderpane = _.bind(_.debounce(this.adjustHeaderpane, 50), this); $(window).on('resize.' + this.cid, this.adjustHeaderpane); }, approveCase: function(options){ var self = this; var statusApprove = 'approve'; url = App.api.buildURL('pmse_approve', null, {id: statusApprove}); App.api.call('update', url, options.attributes, { success: function () { }, error: function (err) { } }); var redirect = options.module; app.router.navigate(redirect , {trigger: true, replace: true }); }, rejectCase: function(options){ var self = this; var statusApprove = 'reject'; url = App.api.buildURL('pmse_approve', null, {id: statusApprove}); App.api.call('update', url, options.attributes, { success: function () { }, error: function (err) { } }); var redirect = options.module; app.router.navigate(redirect , {trigger: true, replace: true }); }, cancelCase: function(options){ var redirect = options.module; app.router.navigate(redirect , {trigger: true, replace: true }); }, validationComplete: function(isValid) { if (isValid) { this.setButtonStates(this.STATE.VIEW); this.handleSave(); } }, _initButtons: function() { if (this.options.meta && this.options.meta.buttons) { _.each(this.options.meta.buttons, function(button) { this.registerFieldAsButton(button.name); if (button.buttons) { var dropdownButton = this.getField(button.name); if (!dropdownButton) { return; } _.each(dropdownButton.fields, function(ddButton) { this.buttons[ddButton.name] = ddButton; }, this); } }, this); } }, toggleViewButtons: function(isEdit) { this.$('.headerpane span[data-type="badge"]').toggleClass('hide', isEdit); this.$('.headerpane span[data-type="favorite"]').toggleClass('hide', isEdit); this.$('.headerpane span[data-type="follow"]').toggleClass('hide', isEdit); this.$('.headerpane .btn-group-previous-next').toggleClass('hide', isEdit); } }) }, "show-case": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Show-case View (base) /** * @deprecated Since 7.8. Will be removed in 7.10 * @param options */ initialize: function(options) { app.logger.warn('View.Views.Base.pmse_Inbox.ShowCaseView is deprecated. It will be removed in 7.10'); this.inboxId = options.context.attributes.modelId; this.flowId = options.context.attributes.action; app.routing.before('route', this.beforeRouteChange, this); }, loadData: function () { var self = this, sep = '/', pmseInboxUrl = app.api.buildURL(this.options.module + '/case/' + this.inboxId + sep + this.flowId ,'',{},{}); app.api.call('READ', pmseInboxUrl, {},{ success: function(data) { self.initCaseView(data) }, error: function (error) { app.error.handleNotFoundError(); } }); }, initCaseView: function(data){ if(data.case.flow.cas_flow_status==='FORM'){ this.params = { action: 'detail', layout: 'pmse-case', module: data.case.flow.cas_sugar_module, modelId: data.case.flow.cas_sugar_object_id, case: data.case }; app.controller.loadView(this.params); } else if (data.case.flow.cas_flow_status === 'CLOSED') { app.alert.show('message-id', { level: 'warning', messages: app.lang.get('LBL_PA_PROCESS_CLOSED','pmse_Inbox'), autoClose: false }); app.router.goBack(); } else { app.alert.show('message-id', { level: 'warning', messages: app.lang.get('LBL_PA_PROCESS_UNAVAILABLE','pmse_Inbox'), autoClose: false }); } }, beforeRouteChange: function () { app.routing.offBefore('route', this.beforeRouteChange); $('.adam-modal').remove(); } }) }, "casesList-filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // CasesList-filter View (base) _moduleFilterList: [], _allModulesId: 'All', _selectedModule: null, _currentSearch: '', events: { 'keyup .search-name': 'throttledSearch', 'paste .search-name': 'throttledSearch', 'click .add-on.sicon-remove': 'clearInput' }, processStatus: [app.lang.get('LBL_STATUS_COMPLETED', this.module), app.lang.get('LBL_STATUS_TERMINATED', this.module), app.lang.get('LBL_STATUS_IN_PROGRESS', this.module), app.lang.get('LBL_STATUS_CANCELLED', this.module), app.lang.get('LBL_STATUS_ERROR', this.module)], /** * Initialize */ initialize: function(options) { this.events = _.extend({}, this.events, { 'click [data-action=refreshList]': '_refreshList' }); this.cacheKiller = (new Date()).getTime(); this._super('initialize', [options]); }, /** * Converts the input field to a select2 field and adds the module filter for refining the search. * * @private */ _render: function() { app.view.View.prototype._render.call(this); this.buildModuleFilterList(); this.buildFilter(); }, /** * Builds the list of allowed modules to provide the data to the select2 field. */ buildModuleFilterList: function() { var allowedModules = this.collection.allowed_modules; this._moduleFilterList = [ {id: this._allModulesId, text: app.lang.get('LBL_MODULE_ALL')} ]; _.each(allowedModules, function(module) { // this._moduleFilterList.push({id: module, text: app.lang.get('LBL_MODULE_NAME', module)}); this._moduleFilterList.push({id: module, text: module}); }, this); }, /** * Converts the input field to a select2 field and initializes the selected module. */ buildFilter: function() { var $filter = this.getFilterField(); if ($filter.length > 0) { $filter.select2({ data: this._moduleFilterList, allowClear: false, multiple: false, minimumResultsForSearch: -1, formatSelection: _.bind(this.formatModuleSelection, this), formatResult: _.bind(this.formatModuleChoice, this), dropdownCss: {width: 'auto'}, dropdownCssClass: 'search-filter-dropdown', initSelection: _.bind(this.initSelection, this), escapeMarkup: function(m) { return m; }, width: 'off' }); $filter.off('change'); $filter.on('change', _.bind(this.handleModuleSelection, this)); this._selectedModule = this._selectedModule || this._allModulesId; $filter.select2('val', this._selectedModule); } }, /** * Gets the filter DOM field. * * @returns {Object} DOM Element */ getFilterField: function() { return this.$('input.select2'); }, /** * Gets the module filter DOM field. * * @returns {Object} DOM Element */ getModuleFilter: function() { return this.$('span.choice-filter-label'); }, /** * Destroy the select2 plugin. */ unbind: function() { $filter = this.getFilterField(); if ($filter.length > 0) { $filter.off(); $filter.select2('destroy'); } this._super("unbind"); }, /** * Performs a search once the user has entered a term. */ throttledSearch: _.debounce(function(evt) { var newSearch = this.$(evt.currentTarget).val(); if (this._currentSearch !== newSearch && _.indexOf(this.processStatus, this._selectedModule) == -1) { this._currentSearch = newSearch; this.applyFilter(); } }, 400), /** * Initialize the module selection with the value for all modules. * * @param el * @param callback */ initSelection: function(el, callback) { if (el.is(this.getFilterField())) { var module = _.findWhere(this._moduleFilterList, {id: el.val()}); callback({id: module.id, text: module.text}); } }, /** * Format the selected module to display its name. * * @param {Object} item * @return {String} */ formatModuleSelection: function(item) { // update the text for the selected module this.getModuleFilter().text(item.text); return '<span class="select2-choice-type">' + app.lang.get('LBL_PMSE_FILTER', this.module) + '<i class="sicon sicon-chevron-down"></i></span>'; }, /** * Format the choices in the module select box. * * @param {Object} option * @return {String} */ formatModuleChoice: function (option) { return '<div><span class="select2-match"></span>' + option.text + '</div>'; }, /** * Handler for when the module filter dropdown value changes, either via a click or manually calling jQuery's * .trigger("change") event. * * @param {Object} evt jQuery Change Event Object * @param {string} overrideVal (optional) ID passed in when manually changing the filter dropdown value */ handleModuleSelection: function(evt, overrideVal) { var module = overrideVal || evt.val || this._selectedModule || this._allModulesId; // only perform a search if the module is in the approved list if (!_.isEmpty(_.findWhere(this._moduleFilterList, {id: module}))) { this._selectedModule = module; this.getFilterField().select2('val', this._selectedModule); this.getModuleFilter().css('cursor', 'pointer'); this.applyFilter(); } }, /** * Triggers an event that makes a call to search the address book and filter the data set. */ applyFilter: function() { var searchAllModules = (this._selectedModule === this._allModulesId), // pass an empty array when all modules are being searched module = searchAllModules ? [] : [this._selectedModule], // determine if the filter is dirty so the "clearQuickSearchIcon" can be added/removed appropriately isDirty = !_.isEmpty(this._currentSearch); this._toggleClearQuickSearchIcon(isDirty); this.context.trigger('compose:addressbook:search', module, this._currentSearch); }, /** * Append or remove an icon to the quicksearch input so the user can clear the search easily. * @param {Boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.add-on.sicon-close')[0]) { this.$('.filter-view.search').append('<i class="add-on sicon sicon-close"></i>'); } else if (!addIt) { this.$('.add-on.sicon-close').remove(); } }, /** * Clear input */ clearInput: function() { var $filter = this.getFilterField(); this._currentSearch = ''; this._selectedModule = this._allModulesId; this.$('.search-name').val(this._currentSearch); if ($filter.length > 0) { $filter.select2('val', this._selectedModule); } this.applyFilter(); }, /** * Refreshes the list view by applying filters. * * @private */ _refreshList: function() { this.applyFilter(); } }) }, "config-log-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Config-log-headerpane View (base) extendsFrom: "HeaderpaneView", events: { "click [name=save_button]": "_save", "click [name=cancel_button]": "_cancel" }, /** * Save the drawer. * * @private */ _save: function() { app.alert.show('txtConfigLog', {level: 'process', title: 'Saving', autoclose: false}) var value = this.model.attributes; value.frm_action = 'Approve'; value.cfg_value=this.model.get('comboLogConfig'); var url = app.api.buildURL('pmse_Inbox/logSetConfig','',{},{}); app.api.call('update', url, value,{ success: function (){ app.alert.dismiss('txtConfigLog'); app.drawer.close(); } }); }, /** * Close the drawer. * * @private */ _cancel: function() { app.drawer.close(); } }) }, "logView-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // LogView-headerpane View (base) extendsFrom: 'HeaderpaneView', events:{ 'click [name=log_pmse_button]': 'getLogPmse', 'click [name=log_clear_button]': 'logClearClick', 'click [name=log_cron_button]': 'getLogCron' }, initialize: function(options) { this._super('initialize', [options]); this.getLogPmse(); this.context.on('list:cancelCase:fire', this.cancelCases, this); //this.context.on('configLog:fire', this.getLogConfig, this); }, logClearClick: function () { var self = this; app.alert.show('clear_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_WARNING_CLEAR', this.module), onConfirm: function () { app.alert.show('data:sync:process', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false }); self.clearLog(); }, onCancel: $.noop }); }, clearLog: function () { var self = this; var pmseInboxUrl = app.api.buildURL(this.module + '/clearLog/pmse'); app.api.call('update', pmseInboxUrl, {}, { success: function () { self.getLog(); } }); }, getLogPmse: function() { app.alert.show('data:sync:process', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false}); var self = this; var pmseInboxUrl = app.api.buildURL(this.module + '/getLog'); app.api.call('READ', pmseInboxUrl, {},{ success: function(data) { self.getLog(data) } }); }, getLogCron : function() { app.alert.show('data:sync:process', {level: 'process', title: 'Loading', autoclose: false}); var self = this; var pmseInboxUrl = app.api.buildURL(this.module + '/getLog/cron'); app.api.call('READ', pmseInboxUrl, {},{ success: function(data) { $('#logPmseId').html('Cron Log'); self.getLog(data) } }); }, getLog: function(data) { $("textarea").val(data); app.alert.dismiss('data:sync:process'); } }) }, "process-status-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Process-status-chart View (base) plugins: ['Dashlet', 'Chart'], processCollection: null, currentValue: 'all', chartCollection: null, hasData: false, total: 0, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.locale = SUGAR.charts.getUserLocale(); this.tooltipTemplate = app.template.getField('chart', 'multipletooltiptemplate', 'Reports'); this.chart = sucrose.charts.multibarChart() .showTitle(false) .showControls(true) .showValues(false) .stacked(true) .tooltipContent(_.bind(function(eo, properties) { var point = {}; var precision = this.locality.precision; point.groupName = app.lang.get('LBL_PMSE_LABEL_PROCESS', this.module); point.groupLabel = eo.group.label; point.seriesName = app.lang.get('LBL_PMSE_LABEL_STATUS', this.module); point.seriesLabel = eo.series.key; point.valueName = app.lang.get('LBL_CHART_COUNT'); point.valueLabel = sucrose.utility.numberFormat(eo.point.y, precision, false, this.locality); return this.tooltipTemplate(point).replace(/(\r\n|\n|\r)/gm, ''); }, this)) .tooltips(true) .strings({ legend: { close: app.lang.get('LBL_CHART_LEGEND_CLOSE'), open: app.lang.get('LBL_CHART_LEGEND_OPEN'), noLabel: app.lang.get('LBL_CHART_UNDEFINED') }, noData: app.lang.get('LBL_CHART_NO_DATA'), noLabel: app.lang.get('LBL_CHART_UNDEFINED') }) .locality(this.locale); this.locality = this.chart.locality(); }, hasChartData: function () { return this.hasData; }, /** * Generic method to render chart with check for visibility and data. * Called by _renderHtml and loadData. */ renderChart: function() { if (!this.isChartReady()) { return; } d3.select(this.el).select('svg#' + this.cid) .datum(this.chartCollection) .transition().duration(500) .call(this.chart); this.chart_loaded = _.isFunction(this.chart.update); this.displayNoData(!this.chart_loaded); }, /** * @inheritdoc */ loadData: function(options) { var self = this, url; if (this.meta.config) { return; } if (!this.currentValue) { return; } url = app.api.buildURL('pmse_Inbox/processStatusChart/' + this.currentValue); this.hasData = false; app.api.call('GET', url, null, { success: function(data) { self.evaluateResponse(data); self.renderChart(); }, complete: options ? options.complete : null }); }, evaluateResponse: function(response) { var total = d3.sum(response.data, function(d) { return d3.sum(d.values, function(h) { return h.y; }); }); this.hasData = !!total; this.chartCollection = response; } }) }, "casesList-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // CasesList-list View (base) extendsFrom: 'RecordlistView', /** * Removes the event listeners that were added to the mass collection. */ unbindData: function() { var massCollection = this.context.get('mass_collection'); if (massCollection) { massCollection.off(null, null, this); } this._super("unbindData"); }, /** * @inheritdoc */ _setOrderBy: function(options) { this.context.set('sortOptions', options); options.query = this.context.get('query'); options.module_list = this.context.get('module_list'); options.offset = 0; options.update = false; this._super('_setOrderBy', options); }, /** * Override to hook in additional triggers as the mass collection is updated (rows are checked on/off in * the actionmenu field). Also attempts to pre-check any rows when the list is refreshed and selected recipients * are found within the new result set (this behavior occurs when the user searches the address book). * * @private */ _render: function() { if (app.acl.hasAccessToAny('developer')) { this._super('_render'); } else { app.controller.loadView({ layout: 'access-denied' }); } }, /** * @inheritdoc */ _dispose: function() { jQuery('.adam-modal').remove(); this._super('_dispose'); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Inbox.PreviewView * @alias SUGAR.App.view.views.Basepmse_InboxPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', events: { 'click .minify': 'toggleMinify' }, toggleMinify: function(evt) { var $el = this.$('.dashlet-toggle > i'), collapsed = $el.is('.icon-chevron-up'); if(collapsed){ $('.dashlet-toggle > i').removeClass('icon-chevron-up'); $('.dashlet-toggle > i').addClass('icon-chevron-down'); }else{ $('.dashlet-toggle > i').removeClass('icon-chevron-down'); $('.dashlet-toggle > i').addClass('icon-chevron-up'); } $('.dashlet').toggleClass('collapsed'); $('.dashlet-content').toggleClass('hide'); }, /** * Renders the preview dialog with the data from the current model and collection. */ _render: function() { var self = this; //only use id2 if it's available if (this.model.get('id2')) { this.model.set('id', this.model.get('id2')); } var pmseInboxUrl = app.api.buildFileURL({ module: 'pmse_Inbox', id: self.model.get('cas_id') || (self.model.collection.get(self.model)).get('cas_id'), field: 'id' }, {cleanCache: true}); this.image_preview_url = pmseInboxUrl; this._super('_render'); } }) }, "logView-pane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // LogView-pane View (base) /** * @inheritdoc * * Sets up the file field to edit mode * * @param {View.Field} field * @private */ _renderField: function(field) { if (app.acl.hasAccessToAny('developer')) { app.view.View.prototype._renderField.call(this, field); field.$el.children().css('width','100%'); field.$el.children().attr('readonly','readonly'); } else { app.controller.loadView({ layout: 'access-denied' }); } } }) }, "dashlet-inbox": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-inbox View (base) extendsFrom: 'HistoryView', /** * @inheritdoc * * @property {Number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '10'. * @property {String} _defaultSettings.visibility Records visibility * regarding current user, supported values are 'user' and 'group', * defaults to 'user'. */ _defaultSettings: { date:'true', limit: 10, visibility: 'user' }, thresholdRelativeTime: 2, //Show relative time for 2 days and then date time after /** * @inheritdoc */ initialize: function(options) { options.meta = options.meta || {}; options.meta.template = 'tabbed-dashlet'; this._super('initialize', [options]); }, /** * Besides defining new DOM events that will be later bound to methods * through {@link #delegateEvents, the events method also makes sure parent * classes events are explicitly inherited. * * @property {Function} */ events: function() { var prototype = Object.getPrototypeOf(this); var parentEvents = _.result(prototype, 'events'); return _.extend({}, parentEvents, { 'click [data-action=date-switcher]': 'dateSwitcher' }); }, /** * Event handler for date switcher. * * @param {Event} event Click event. */ dateSwitcher: function(event) { var date = this.$(event.currentTarget).val(); if (date === this.getDate()) { return; } this.settings.set('date', date); this.loadData(); }, /** * Get current date state. * Returns default value if can't find in last state or settings. * * @return {String} Date state. */ getDate: function() { var date = app.user.lastState.get( app.user.lastState.key('date', this), this ); return date || this.settings.get('date') || this._defaultSettings.date; }, /** * @inheritdoc * * On load of new data, make sure we reload invitations related data, if * it is defined for the current tab. */ loadData: function(options) { if (this.disposed || this.meta.config) { return; } var tab = this.tabs[this.settings.get('activeTab')]; if (tab.invitations) { tab.invitations.dataFetched = false; } this._super('loadData', [options]); }, /** * @inheritdoc * * FIXME: This should be removed when metadata supports date operators to * allow one to define relative dates for date filters. */ _initTabs: function() { this._super('_initTabs'); }, /** * @inheritdoc */ _getFilters: function(index) { var tab = this.tabs[index], filter = {}, filters = [], defaultFilters = { 'true': {$equal: 'true'}, 'false': {$equal: 'false'} }; filter[tab.filter_applied_to] = defaultFilters[this.getDate()]; filters.push(filter); return filters; }, /** * Updating in fields delete removed * @return {Function} complete callback * @private */ _getRemoveRecord: function() { return _.bind(function(model){ if (this.disposed) { return; } this.collection.remove(model); this.render(); this.context.trigger("tabbed-dashlet:refresh", model.module); }, this); }, /** * Method view alert in process with text modify * show and hide alert */ _refresh: function(model, status) { app.alert.show(model.id + ':refresh', { level:"process", title: status, autoclose: false }); return _.bind(function(model){ var options = {}; this.layout.reloadDashlet(options); app.alert.dismiss(model.id + ':refresh'); }, this); }, /** * Sets property useRelativeTime to show date created as a relative time or as date time. * * @private */ _setRelativeTimeAvailable: function(date) { var diffInDays = Math.abs(app.date().diff(date, 'days', true)); var useRelativeTime = (diffInDays <= this.thresholdRelativeTime); return useRelativeTime; }, /** * @inheritdoc * * New model related properties are injected into each model: * * - {Boolean} overdue True if record is prior to now. * - {String} picture_url Picture url for model's assigned user. */ _renderHtml: function() { var self = this; if (this.meta.config) { this._super('_renderHtml'); return; } var tab = this.tabs[this.settings.get('activeTab')]; if (tab.overdue_badge) { this.overdueBadge = tab.overdue_badge; } _.each(this.collection.models, function(model){ // only admins and developers have access to process definitions model.set({linkToPD: app.acl.hasAccess('admin', 'pmse_Project')}, {silent: true}); var pictureUrl = App.api.buildFileURL({ module: 'Users', id: model.get('assigned_user_id'), field: 'picture' }); var ShowCaseUrl = 'pmse_Inbox/' + model.get('id2') + '/layout/show-case/' + model.get('flow_id'); var ShowCaseUrlBwc = App.bwc.buildRoute('pmse_Inbox', '', 'showCase', {id:model.get('flow_id')}); var SugarModule = model.get('cas_sugar_module'); if (app.metadata.getModule(SugarModule).isBwcEnabled) { model.set('show_case_url', ShowCaseUrlBwc); } else { model.set('show_case_url', ShowCaseUrl); } model.set('picture_url', pictureUrl); model.set('is_assigned', this.isAssigned(model)); if (model.attributes.cas_due_date) { var useRelativeTime = this._setRelativeTimeAvailable(model.attributes.cas_due_date); if (useRelativeTime) { model.useRelativeTime = true; } else { model.useAbsoluteTime = true; } } if (model.attributes.is_a_person) { model.set('name', app.utils.formatNameModel(model.attributes.cas_sugar_module, model.attributes)); } }, this); this._super('_renderHtml'); }, isAssigned: function(model) { return model.get('cas_assignment_method') != 'selfservice'; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "unattendedCases": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // UnattendedCases Layout (base) /** * @inheritdoc */ initialize: function(options) { app.view.Layout.prototype.initialize.call(this, options); this.collection.sync = this.sync; this.collection.allowed_modules = [ app.lang.get('LBL_CAS_ID',options.module), app.lang.get('LBL_PROCESS_DEFINITION_NAME',options.module), app.lang.get('LBL_RECORD_NAME',options.module), app.lang.get('LBL_OWNER',options.module) ]; this.context.on('compose:addressbook:search', this.search, this); //this.context.on('compose:addressbook:search', this.search, this); }, /** * Calls the custom PMSEEngine API endpoint to search for Task for Cases. * * @param method * @param model * @param options */ sync: function(method, model, options) { var callbacks, url; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list)) { options.module_list = ['all']; } else { options.module_list = _.intersection(this.allowed_modules, options.module_list); } // this is a hack to make pagination work while trying to minimize the affect on existing configurations // there is a bug that needs to be fixed before the correct approach (config.maxQueryResult vs. options.limit) // can be determined app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; options = app.data.parseOptionsForSync(method, model, options); callbacks = app.data.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); url = app.api.buildURL('pmse_Inbox', 'unattendedCases', null, options.params); app.api.call('read', url, null, callbacks); }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {String} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.collection.fetch({query: term, module_list: modules, offset: 0, update: false}); } }) }, "config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Config Layout (base) initialize: function(options) { var acls = app.user.getAcls().Forecasts, hasAccess = (!_.has(acls, 'access') || acls.access == 'yes'), isSysAdmin = (app.user.get('type') == 'admin'), isDev = (!_.has(acls, 'developer') || acls.developer == 'yes'); // if user has access AND is a System Admin OR has a Developer role if(hasAccess && (isSysAdmin || isDev)) { // initialize app.view.Layout.prototype.initialize.call(this, options); // load the data app.view.Layout.prototype.loadData.call(this); } else { this.codeBlockForecasts('LBL_FORECASTS_NO_ACCESS_TO_CFG_TITLE', 'LBL_FORECASTS_NO_ACCESS_TO_CFG_MSG'); } }, /** * Blocks forecasts from continuing to load */ codeBlockForecasts: function(title, msg) { var alert = app.alert.show('no_access_to_forecasts', { level: 'error', title: app.lang.get(title, 'pmse_Inbox') + ':', messages: [app.lang.get(msg, 'pmse_Inbox')] }); var $close = alert.getCloseSelector(); $close.on('click', function() { $close.off(); app.router.navigate('#Home', {trigger: true}); }); app.accessibility.run($close, 'click'); }, /** * Overrides loadData to defer it running until we call it in _onceInitSelectedUser * * @override */ loadData: function() { } }) }, "casesList": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // CasesList Layout (base) /** * @class ComposeAddressbookLayout * @extends Layout */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['ProcessActions']); app.view.Layout.prototype.initialize.call(this, options); this.collection.sync = this.sync; // this.collection.allowed_modules = ['Cases Title', 'Process Name', 'Status', 'Owner']; this.collection.allowed_modules = [ app.lang.get('LBL_STATUS_COMPLETED', options.module), app.lang.get('LBL_STATUS_TERMINATED', options.module), app.lang.get('LBL_STATUS_IN_PROGRESS', options.module), app.lang.get('LBL_STATUS_CANCELLED', options.module), app.lang.get('LBL_STATUS_ERROR', options.module)]; this.context.on('compose:addressbook:search', this.search, this); this.context.on('case:status', this.viewStatus, this); this.context.on('case:history', this.viewHistory, this); this.context.on('case:notes', this.viewNotes, this); this.context.on('case:execute', this.executeCase, this); this.context.on('case:reassign', this.executeReassign, this); this.context.on('list:cancelCase:fire', this.cancelCases, this); // this.context.on('list:executeCase:fire', this.executeCases, this); }, viewStatus: function(model){ this.showStatus(model.get('cas_id')); }, viewHistory: function(model){ this.getHistory(model.get('cas_id')); }, viewNotes: function(model){ this.showNotes(model.get('cas_id'), 1); }, executeCase: function(model){ app.alert.show('upload', {level: 'process', title: 'LBL_LOADING', autoclose: false}); this.executeCasesList([model.get('cas_id')]); }, cancelCases: function(model){ var self=this; var msg=app.lang.get('LBL_PMSE_CANCEL_MESSAGE', this.module); msg=msg.replace('[]',model.get('cas_title')); msg=msg.replace('{}',model.get('cas_id')); app.alert.show('cancelCase-id', { level: 'confirmation', messages:msg, // messages:app.lang.get('LBL_CANCEL_MESSAGE', this.module)+model.get('cas_title')+' with Cas Id: '+model.get('cas_id')+'?', autoClose: false, onConfirm: function(){ app.alert.show('upload', {level: 'process', title: 'LBL_LOADING', autoclose: false}); var massCollection=self.context.get('mass_collection'); var value = self.model.attributes; // value.cas_id = this.buildVariablesString(massCollection); value.cas_id = [model.get('cas_id')]; var pmseInboxUrl = app.api.buildURL(self.module + '/cancelCases','',{},{}); app.api.call('update', pmseInboxUrl, value,{ success: function(data) { self.reloadList(); app.alert.dismiss('upload'); // window.location.reload(); } }); }, onCancel: function(){ app.alert.dismiss('cancelCase-id'); } }); }, // executeCases: function(model){ // app.alert.show('upload', {level: 'process', title: 'LBL_LOADING', autoclose: false}); // var massCollection=this.context.get('mass_collection'); // this.executeCasesList(this.buildVariablesString(massCollection)); // }, executeCasesList: function(idCases){ var self=this; var value = this.model.attributes; value.cas_id = idCases; var pmseInboxUrl = app.api.buildURL(this.module + '/reactivateFlows','',{},{}); app.api.call('update', pmseInboxUrl, value,{ success: function(data) { self.reloadList(); app.alert.dismiss('upload'); // window.location.reload(); } }); }, executeReassign: function(model) { app.drawer.open({ layout: 'reassignCases', context: { module: 'pmse_Inbox', parent: this.context, cas_id: model.get('cas_id') } }); }, buildVariablesString: function(recipients) { var listIdCases = [],count=0; _.each(recipients.models, function(model) { listIdCases[count++]=model.attributes.cas_id }); return currentValue = listIdCases; }, /** * Calls the custom Mail API endpoint to search for email addresses. * * @param method * @param model * @param options */ sync: function(method, model, options) { var callbacks, url; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list)) { options.module_list = ['all']; } else { options.module_list = _.intersection(this.allowed_modules, options.module_list); // options.module_list = this.allowed_modules; } // this is a hack to make pagination work while trying to minimize the affect on existing configurations // there is a bug that needs to be fixed before the correct approach (config.maxQueryResult vs. options.limit) // can be determined app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; options = app.data.parseOptionsForSync(method, model, options); callbacks = app.data.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); // url = app.api.buildURL('pmse_Project', 'caseslist/find', null, options.params); url = app.api.buildURL('pmse_Inbox', 'casesList', null, options.params); app.api.call('read', url, null, callbacks); }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {String} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.context.set('query', term); this.context.set('module_list', modules); var sortOptions = this.context.get('sortOptions') || {}; sortOptions.query = term; sortOptions.module_list = modules; sortOptions.offset = 0; sortOptions.update = false; this.context.resetLoadFlag({recursive: false}); this.context.set('skipFetch', false); this.context.loadData(sortOptions); }, reloadList: function() { this.context.reloadData({ recursive:false, }); } }) }, "show-case": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.pmse_Inbox.ShowCaseLayout * @alias SUGAR.App.view.layouts.Basepmse_InboxShowCaseLayout * @extends View.Layout */ ({ // Show-case Layout (base) plugins: ['ProcessActions'], /** * @inheritdoc */ initialize: function(options) { this.inboxId = options.context.get('modelId'); this.flowId = options.context.get('action'); this.recordAction = options.context.get('record') || 'detail'; this._super('initialize', [options]); this.context.set('skipFetch', true); }, /** * Request case data to find record id and module * * @param {Object} [options] Options that are passed to * collection/model's fetch method. */ loadData: function(options) { var self = this, pmseInboxUrl = app.api.buildURL(this.module + '/case/' + this.inboxId + '/' + this.flowId); app.api.call('read', pmseInboxUrl, {}, { success: function (data) { // Make sure we have an options object to work with var options = options || {}; // This allows us to define our own endpoint setter, which is needed // for case view to force consuming our own endpoint options.endpoint = function(method, model, opts, callbacks) { var casModule = data.case.flow.cas_sugar_module; var casModuleId = data.case.flow.cas_sugar_object_id; // This is the endpoint URL we want to consume var resourcePath = 'pmse_Inbox/caseRecord/' + casModule + '/' + casModuleId; //Forced all fields to load from the record view and the //quotes configured header/footer etc var optFields = ''; if (opts.fields) { optFields = opts.fields.join(','); } var url = app.api.buildURL( resourcePath, null, null, {view: 'record', erased_fields: true, fields: optFields} ); // For some reason, options contains a method property that // is causing the subsequent success call to be a READ HTTP // Request Type. So delete the method property of options to // force a GET request to be made. delete opts.method; // Send back the data from our own endpoint return app.api.call('read', url, {}, callbacks, opts); }; self.initCaseView(data, [options]); }, error: function (error) { app.error.handleNotFoundError(); } }); }, /** * Call loadChildLayout to create the module base record view. * Show error messages if process is closed or unavailable. * * @param data Case information * @param loadDataParams */ initCaseView: function(data, loadDataParams){ if (data.case.flow.cas_flow_status === 'FORM') { this.loadChildLayout(data, loadDataParams); } else if (data.case.flow.cas_flow_status === 'CLOSED') { app.alert.show('message-id', { level: 'warning', messages: app.lang.get('LBL_PA_PROCESS_CLOSED','pmse_Inbox'), autoClose: false }); app.router.goBack(); } else { app.alert.show('message-id', { level: 'warning', messages: app.lang.get('LBL_PA_PROCESS_UNAVAILABLE','pmse_Inbox'), autoClose: false }); } }, /** * Get the module specific record layout and view and override * the view metadata to use PA specific buttons (like Approve/Reject). * Update this layout's metadata with the modified record view metadata * and load all the new components. * * @param data Case information * @param loadDataParams */ loadChildLayout: function(data, loadDataParams) { this.case = data.case; //dispose of anything currently here app.plugins.detach(this, 'layout'); _.each(this._components, function(component) { component.dispose(); }); this._components = []; this.recordModule = data.case.flow.cas_sugar_module; // Create the context for the record view var context = this.context.getChildContext({ module: this.recordModule, modelId: data.case.flow.cas_sugar_object_id }); // to display due date in browser time zone we need to fix it before // context is set since we're using raw values set in context inside template this.case.flow.cas_due_date = this.fixDateToLocale(this.case.flow.cas_due_date); context.prepare(); context.set('case', this.case); context.set('layout', 'record'); context.set('action', this.recordAction); this.recordContext = context; this.recordModel = context.get('model'); // Get the current module specific record layout and view var origRecordLayout = app.metadata.getLayout(this.recordModule, 'record'); var record = this._getChildComponent('view', 'record', origRecordLayout); if (!record) { app.logger.fatal('Record not found.'); } // Override the templates and buttons to use SugarBPM templates and buttons record.xmeta = { template: 'pmse-case', buttons: data.case.buttons }; // Set this layout's meta to create a record layout inside it this.meta = { 'components': [{ 'layout': origRecordLayout, 'context': this.recordContext }] }; this.initComponents(); this.recordComponent = this._getNestedComponent(this._components, 'record'); // Override functions on the record view _.extend(this.recordComponent, this.caseViewOverrides()); // Swap out the event handler so we can override the error message. this.recordComponent.model.off('error:validation'); var showInvalidModel = function() { var name = 'invalid-data'; this._viewAlerts.push(name); var msg = this.formAction == 'approve' ? 'ERR_AWF_APPROVE_VALIDATION_ERROR' : 'ERR_AWF_REJECT_VALIDATION_ERROR'; app.alert.show(name, { level: 'error', messages: msg }); }; this.recordComponent.model.on('error:validation', showInvalidModel, this.recordComponent); this._super('loadData', loadDataParams); this.render(); this._delegateEvents(); }, /** * Returns a string containing only date and time for a specified date as * per user preferences * @param date A date String * @return String * @private */ fixDateToLocale: function(date) { // get local date time for the given utc datetime var local = app.date.utc(date).toDate(); var dateObj = app.date(local); // get date and time based on user preferences var fixedDate = dateObj.format(app.date.getUserDateFormat()); var fixedTime = dateObj.format(app.date.getUserTimeFormat()); return fixedDate + ' ' + fixedTime; }, /** * Search the meta recursively to find to find the component * by the passed in name * * @param type Component type (view, layout) * @param name Name of the component * @param meta The meta for the component * @return {*} The component meta for the passed in name * @private */ _getChildComponent: function(type, name, meta) { if (meta.name === name) { return meta; } if (!meta.components) { return; } for (var i = 0, l = meta.components.length; i < l; i++) { var comp = meta.components[i]; if (comp[type] === name) { return comp; } var next = comp.view || comp.layout; if (_.isObject(next)) { var child = this._getChildComponent(type, name, next); if (child) { return child; } } } }, /** * Get the component from inside the layout by looking through _components * * @param components The _components inside the layout * @param name The name of the component we are looking for * @return {*} The actual component * @private */ _getNestedComponent: function(components, name) { if (components.name === name) { return components; } for (var i = 0; i < components.length; i++) { if (components[i].name === name) { return components[i]; } if (components[i]._components) { return this._getNestedComponent(components[i]._components, name); } } }, /** * Set up event listeners on the record view's context * @private */ _delegateEvents: function() { this.recordContext.on('case:cancel', this.cancelCase, this); this.recordContext.on('case:claim', this.caseClaim, this); this.recordContext.on('case:approve', _.bind(this.caseAction, this, 'Approve')); this.recordContext.on('case:reject', _.bind(this.caseAction, this, 'Reject')); this.recordContext.on('case:route', _.bind(this.caseAction, this, 'Route')); this.recordContext.on('case:send_to_docusign', _.bind(this.caseAction, this, 'SendDocuSign')); this.recordContext.on('case:history', this.caseHistory, this); this.recordContext.on('case:status', this.caseStatus, this); this.recordContext.on('case:add:notes', this.caseAddNotes, this); this.recordContext.on('case:change:owner', this.caseChangeOwner, this); this.recordContext.on('case:reassign', this.caseReassign, this); }, /** * When clicking cancel, the case is redirected */ cancelCase: function () { this.redirectCase(); }, caseClaim: function () { app.alert.show('upload', {level: 'process', title: 'LBL_LOADING', autoclose: false}); var frm_action = 'Claim'; var value = this.recordModel.attributes; value.moduleName = this.case.flow.cas_sugar_module; value.beanId = this.case.flow.cas_sugar_object_id; value.cas_id = this.case.flow.cas_id; value.cas_index = this.case.flow.cas_index; value.taskName = this.case.title.activity; var self = this; var pmseInboxUrl = app.api.buildURL('pmse_Inbox/engine_claim','',{},{}); app.api.call('update', pmseInboxUrl, value,{ success: function (){ app.alert.dismiss('upload'); self.redirectCase(frm_action); }, error: function(error) { app.alert.dismiss('upload'); var message = (error && error.message) ? error.message : 'EXCEPTION_FATAL_ERROR'; app.alert.show('error_claim', { level: 'error', messages: message }); } }); }, /** * Validate the model when trying to approve/reject/route/sendToDs the case */ caseAction: function (action) { var allFields = this.recordComponent.getFields(this.recordModule, this.recordModel); var fieldsToValidate = {}; if (action == 'Reject' || action == 'Route') { this.recordComponent.formAction = 'reject'; var erasedFields = this.recordModel.get('_erased_fields'); for (var fieldKey in allFields) { if (app.acl.hasAccessToModel('edit', this.recordModel, fieldKey) && (!_.contains(erasedFields, fieldKey) || this.recordModel.get(fieldKey))) { _.extend(fieldsToValidate, _.pick(allFields, fieldKey)); } } } else { this.recordComponent.formAction = 'approve'; for (var fieldKey in allFields) { if (app.acl.hasAccessToModel('edit', this.recordModel, fieldKey)) { _.extend(fieldsToValidate, _.pick(allFields, fieldKey)); } } } this.recordModel.doValidate(fieldsToValidate, _.bind(this.validationComplete, this, action)); }, /** * Shows a window with current history of the record */ caseHistory: function () { this.getHistory(this.case.flow.cas_id); }, /** * Shows window with picture of current status of the process */ caseStatus: function() { this.showStatus(this.case.flow.cas_id); }, /** * Shows window with notes of current process */ caseAddNotes: function () { this.showNotes(this.case.flow.cas_id, this.case.flow.cas_index); }, /** * Allow changing owner */ caseChangeOwner: function () { var value = this.recordModel.attributes; value.moduleName = this.case.flow.cas_sugar_module; value.beanId = this.case.flow.cas_sugar_object_id; this.showForm(this.case.flow.cas_id, this.case.flow.cas_index, 'adhoc', this.case.flowId, this.case.inboxId, this.case.title.activity, value); }, /** * Reassign the case */ caseReassign: function () { var value = this.recordModel.attributes; value.moduleName = this.case.flow.cas_sugar_module; value.beanId = this.case.flow.cas_sugar_object_id; this.showForm(this.case.flow.cas_id, this.case.flow.cas_index, 'reassign', this.case.flowId, this.case.inboxId, this.case.title.activity, value); }, /** * If validation is valid, save the model and approve the case * * @param {string} action Either Approve, Reject or Route * @param {boolean} isValid `true` if valid, false if validation failed */ validationComplete: function (action, isValid) { var buttonLangStrings = { 'Approve': { confirm: 'LBL_PA_PROCESS_APPROVE_QUESTION', success: 'LBL_PA_PROCESS_APPROVED_SUCCESS' }, 'Reject': { confirm: 'LBL_PA_PROCESS_REJECT_QUESTION', success: 'LBL_PA_PROCESS_REJECTED_SUCCESS' }, 'Route': { confirm: 'LBL_PA_PROCESS_ROUTE_QUESTION', success: 'LBL_PA_PROCESS_ROUTED_SUCCESS' }, 'SendDocuSign': { confirm: 'LBL_PA_PROCESS_SEND_DOCUSIGN_QUESTION', success: 'LBL_PA_PROCESS_SEND_DOCUSIGNED_SUCCESS' } }; if (isValid) { app.alert.show('confirm_save_process', { level: 'confirmation', messages: app.lang.get(buttonLangStrings[action].confirm, 'pmse_Inbox'), onConfirm: _.bind(function () { app.alert.show('upload', { level: 'process', title: app.lang.get('LBL_LOADING'), autoclose: false }); var data = app.data.getEditableFields(this.recordModel); data = _.extend(data, { frm_action: action, idFlow: this.case.flowId, idInbox: this.case.inboxId, cas_id: this.case.flow.cas_id, cas_index: this.case.flow.cas_index, moduleName: this.case.flow.cas_sugar_module, beanId: this.case.flow.cas_sugar_object_id, taskName: this.case.title.activity }); if (action === 'Route' && this.case.taskContinue) { data.taskContinue = true; } var self = this; var pmseInboxUrl = app.api.buildURL('pmse_Inbox/engine_route', '', {}, {}); if (action === 'SendDocuSign') { this.sendToDocuSign({ pmseInboxUrl: pmseInboxUrl, data: data, buttonLangStrings: buttonLangStrings, action: action }); return; } app.api.call('update', pmseInboxUrl, data, { success: function () { app.alert.show('success_save_process', { level: 'success', messages: app.lang.get(buttonLangStrings[action].success, 'pmse_Inbox'), autoClose: true }); self.recordModel.setSyncedAttributes(data); self.redirectCase(); }, error: function(error) { app.alert.dismiss('upload'); var message = (error && error.message) ? error.message : 'EXCEPTION_FATAL_ERROR'; app.alert.show('error_save_process', { level: 'error', messages: message }); } }); }, this), onCancel: $.noop }); } }, /** * Leave the case record view * * @param isRoute */ redirectCase: function(isRoute){ app.alert.dismiss('upload'); switch(isRoute){ case 'Claim': window.location.reload(); break; default: app.router.navigate('Home', {trigger: true}); break; }; }, /** * Send record to DocuSign * * @param {Object} options */ sendToDocuSign: function(options) { this.setupDocuSignDocumentCollection(); const ctxDocumentsCollection = app.controller.context.get('documentCollection'); const dsPayload = { returnUrlParams: { parentRecord: this.recordModel.module, parentId: this.recordModel.get('id'), token: app.api.getOAuthToken() }, documents: _.pluck(ctxDocumentsCollection.models, 'id') }; app.events.trigger('docusign:send:initiate', dsPayload); //listen for ds action to complete in order to mark the case as done this.listenTo(app.events, 'docusign:send:finished', function() { app.api.call('update', options.pmseInboxUrl, options.data, { success: _.bind(function() { app.alert.show('success_save_process', { level: 'success', messages: app.lang.get(options.buttonLangStrings[options.action].success, 'pmse_Inbox'), autoClose: true }); this.recordModel.setSyncedAttributes(options.data); this.redirectCase(); }, this), error: function(error) { app.alert.dismiss('upload'); const message = (error && error.message) ? error.message : 'EXCEPTION_FATAL_ERROR'; app.alert.show('error_save_process', { level: 'error', messages: message }); } }); }.bind(this)); }, /** * Setup a collection with documents to be send to DocuSign */ setupDocuSignDocumentCollection: function() { if (app.controller.context.get('documentCollection') instanceof app.data.beanCollection === false) { const documentsCollection = app.data.createBeanCollection('Documents'); app.controller.context.set('documentCollection', documentsCollection); } let subpanels; try { subpanels = app.controller.layout .getComponent('') .getComponent('sidebar') .getComponent('main-pane') .getComponent('filterpanel') .getComponent('subpanels'); } catch (e) { return; } const subpanelWithDocuments = _.find(subpanels._components, function(subpanel) { return subpanel.module === 'Documents'; }); const ctxDocumentsCollection = app.controller.context.get('documentCollection'); ctxDocumentsCollection.models = subpanelWithDocuments.collection.models; }, /** * Defines and returns functions that will override the case module's record view. * Add functions to the return object anytime you want to change record view behavior. * * @return Object of functions */ caseViewOverrides: function() { return { /** * @override * * Allows checking SugarBPM readonly and required fields */ setEditableFields: function() { delete this.editableFields; this.editableFields = []; var previousField, firstField; _.each(this.fields, function(field) { if (this.checkReadonly(field)) { field.def.readonly = true; } if (field.def.fields && _.isArray(field.def.fields)) { var that = this; var basefield = field; _.each(field.def.fields, function(field) { if (that.checkReadonly(field)) { field.readonly = true; field.action = 'detail'; // Some fields use shouldDisable to enable readonly property, // like 'body' in KBContents if (!_.isUndefined(field.shouldDisable)) { field.setDisabled(true); basefield.def.readonly = true; } } }); } if (field.fields && _.isArray(field.fields)) { var self = this; _.each(field.fields, function(field) { if (self.checkRequired(field)) { field.def.required = true; } }); } var readonlyField = field.def.readonly || _.indexOf(this.noEditFields, field.def.name) >= 0 || field.parent || (field.name && this.buttons[field.name]); // exclude readOnly Fields and record-decor field wrappers if (readonlyField || field.type === this.decoratorField) { return; } if (this.checkRequired(field)) { field.def.required = true; } if (previousField) { previousField.nextField = field; field.prevField = previousField; } else { firstField = field; } previousField = field; this.editableFields.push(field); }, this); if (previousField) { previousField.nextField = firstField; firstField.prevField = previousField; } }, /** * @override * Toggle more fields than on base record view * @param isEdit */ toggleViewButtons: function(isEdit) { this.$('.headerpane span[data-type="badge"]').toggleClass('hide', isEdit); this.$('.headerpane span[data-type="favorite"]').toggleClass('hide', isEdit); this.$('.headerpane span[data-type="follow"]').toggleClass('hide', isEdit); this.$('.headerpane .btn-group-previous-next').toggleClass('hide', isEdit); }, /** * @override * We want to set field metadata here if a field is readonly * @param panels * @private */ _buildGridsFromPanelsMetadata: function(panels) { var lastTabIndex = 0; this.noEditFields = []; _.each(panels, function(panel) { // get user preference for labelsOnTop before iterating through // fields panel.labelsOnTop = this.getLabelPlacement(); // it is assumed that a field is an object but it can also be a string // while working with the fields, might as well take the opportunity to check the user's ACLs for the field _.each(panel.fields, function(field, index) { if (this.checkReadonly(field)) { field.readonly = true; } if (_.isString(field)) { panel.fields[index] = field = {name: field}; } var keys = _.keys(field); // Make filler fields readonly if (keys.length === 1 && keys[0] === 'span') { field.readonly = true; } // disable the pencil icon if the user doesn't have ACLs if (field.type === 'fieldset') { if (field.readonly || _.every(field.fields, function(field) { return !app.acl.hasAccessToModel('edit', this.model, field.name); }, this)) { this.noEditFields.push(field.name); } } else if (field.readonly || !app.acl.hasAccessToModel('edit', this.model, field.name)) { this.noEditFields.push(field.name); } field.labelsOnTop = panel.labelsOnTop; }, this); // Set flag so that show more link can be displayed to show hidden panel. if (panel.hide) { this.hiddenPanelExists = true; } // labels: visibility for the label if (_.isUndefined(panel.labels)) { panel.labels = true; } if (_.isFunction(this.getGridBuilder)) { var options = { fields: panel.fields, columns: panel.columns, labels: panel.labels, labelsOnTop: panel.labelsOnTop, tabIndex: lastTabIndex }, gridResults = this.getGridBuilder(options).build(); panel.grid = gridResults.grid; lastTabIndex = gridResults.lastTabIndex; } }, this); }, /** * Check if the field is set to readonly by SugarBPM * @param field The field * @returns {boolean} `true` or `false` */ checkReadonly: function(field){ var isReadonly = false; if (this.buttons[field.name]) { return false; } _.each(this.context.get('case').readonly, function(caseField) { if (field.name === caseField) { isReadonly = true; } }, this); if (this.context.get('case').reclaim) { isReadonly = true; } return isReadonly; }, /** * Check if the field is set to required by SugarBPM * @param field The field * @returns {boolean} `true` or `false` */ checkRequired: function(field){ var isRequired = false; _.each(this.context.get('case').required, function(caseField) { if (field.name === caseField) { isRequired = true; } }, this); return isRequired; } } } }) }, "reassignCases": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // ReassignCases Layout (base) /** * @inheritdoc */ initialize: function(options) { app.view.Layout.prototype.initialize.call(this, options); this.collection.sync = _.bind(this.sync, this); // this.collection.allowed_modules = ['User Assigned']; this.context.on('compose:addressbook:search', this.search, this); }, /** * Calls the custom PMSEEngine API endpoint to search for Task for Cases. * * @param method * @param model * @param options */ sync: function(method, model, options) { var callbacks, url; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list)) { options.module_list = ['User Assigned']; } else { options.module_list = _.intersection(this.allowed_modules, options.module_list); } // this is a hack to make pagination work while trying to minimize the affect on existing configurations // there is a bug that needs to be fixed before the correct approach (config.maxQueryResult vs. options.limit) // can be determined app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; options = app.data.parseOptionsForSync(method, model, options); callbacks = app.data.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); if (this.context.get('unattended')) { options.params.unattended = true; } url = app.api.buildURL('pmse_Inbox', 'reassignFlows/' + this.context.get('cas_id'), null, options.params); app.api.call('read', url, null, callbacks); }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {String} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.collection.fetch({query: term, module_list: modules, offset: 0, update: false}); } }) } }} , "datas": {} }, "pmse_Project":{"fieldTemplates": { "base": { "enabled-disabled": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Enabled-disabled FieldTemplate (base) extendsFrom: 'RowactionField', initialize: function (options) { this._super("initialize", [options]); this.type = 'rowaction'; }, _render: function () { var value=this.model.get('prj_status'); if (value === 'ACTIVE') { this.label = App.lang.get("LBL_PMSE_LABEL_DISABLE", "pmse_Project"); } else { this.label = App.lang.get("LBL_PMSE_LABEL_ENABLE", "pmse_Project"); } this._super("_render"); }, bindDataChange: function () { if (this.model) { this.model.on("change", this.render, this); } } }) }, "process-status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Process-status FieldTemplate (base) extendsFrom: 'BadgeSelectField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'badge-select'; }, /** * @inheritdoc * * Styles the badge. * * @private */ _render: function() { this._super('_render'); this.styleLabel(this.model.get(this.name)); }, /** * Sets the appropriate CSS class on the label based on the value of the * status. * * It is a noop when the field is in edit mode. * * @param {String} status */ styleLabel: function(status) { var $label; if (this.action !== 'edit') { $label = this.$('.label'); switch (status) { case 'ACTIVE': $label.addClass('label-success'); break; case 'INACTIVE': $label.addClass('label-important'); break; default: break; } } } }) } }} , "views": { "base": { "designer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Designer View (base) className: 'designer', events: { 'click .btn-close-designer': 'closeDesigner' }, designerBackgroundImage: 'grid_20', closeDesigner: function() { var route = app.router.buildRoute(this.module, this.prj_uid); app.router.navigate(route, {trigger: true}); }, loadData: function (options) { this.prj_uid = this.options.context.attributes.modelId; this.cacheKiller = (new Date()).getTime(); }, initialize: function (options) { this._super('initialize', [options]); app.routing.before('route', this.beforeRouteChange, this); this._setDesignerBackgroundImage(); }, render: function () { app.view.View.prototype.render.call(this); renderProject(this.prj_uid); }, beforeRouteChange: function(params) { var self = this, resp = false; if (project.isDirty){ project.showWarning = true; var targetUrl = Backbone.history.getFragment(); //Replace the url hash back to the current staying page app.router.navigate(targetUrl, {trigger: false, replace: true}); app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WARN_UNSAVED_CHANGES', this.module), onConfirm: function () { var targetUrl = Backbone.history.getFragment(); project.dispose(); app.router.navigate(targetUrl , {trigger: true, replace: true }); window.location.reload(); }, onCancel: function () { app.router.navigate('' , {trigger: false, replace: false }) } }); return false; } project.dispose(); return true; }, // Update the wallpaper of the process definition designer to use the correct background image _setDesignerBackgroundImage: function() { let imageName = app.utils.isDarkMode() ? 'dark_grid_20' : 'grid_20'; this.designerBackgroundImage = `modules/pmse_Project/img/${imageName}.png`; }, _dispose: function () { app.routing.offBefore('route', this.beforeRouteChange); this._super("_dispose", arguments); } }) }, "dashlet-processes": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-processes View (base) extendsFrom: 'TabbedDashletView', /** * @inheritdoc * * @property {Number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '10'. * @property {String} _defaultSettings.visibility Records visibility * regarding current user, supported values are 'user' and 'group', * defaults to 'user'. */ _defaultSettings: { limit: 10, visibility: 'user' }, thresholdRelativeTime: 2, //Show relative time for 2 days and then date time after /** * @inheritdoc */ initialize: function(options) { options.meta = options.meta || {}; options.meta.template = 'tabbed-dashlet'; this.plugins = _.union(this.plugins, [ 'LinkedModel' ]); this.tbodyTag = 'ul[data-action="pagination-body"]'; this._super('initialize', [options]); }, /** * @inheritdoc */ _initEvents: function() { this._super('_initEvents'); this.on('dashlet-processes:designer:fire', this.designer, this); this.on('dashlet-processes:delete-record:fire', this.deleteRecord, this); this.on('dashlet-processes:enable-record:fire', this.enableRecord, this); this.on('dashlet-processes:disable-record:fire', this.disableRecord, this); this.on('dashlet-processes:download:fire', this.showExportingWarning, this); this.on('dashlet-processes:description-record:fire', this.descriptionRecord, this); this.on('linked-model:create', this.loadData, this); return this; }, /** * Re-fetches the data for the context's collection. * * FIXME: This will be removed when SC-4775 is implemented. * * @private */ _reloadData: function() { this.context.set('skipFetch', false); this.context.reloadData(); }, /** * Fire dessigner */ designer: function(model){ var verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { var redirect = app.router.buildRoute(model.module, model.id, 'layout/designer'); app.router.navigate(redirect , {trigger: true, replace: true }); } else { app.alert.show('project-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_DEFINITIONS_EDIT', model.module), onConfirm: function () { var redirect = app.router.buildRoute(model.module, model.id, 'layout/designer'); app.router.navigate(redirect , {trigger: true, replace: true }); }, onCancel: $.noop }); } } }); }, /** * Show warning of pmse_Process_Definition */ showExportingWarning: function (model) { var that = this; if (app.cache.get("show_project_export_warning")) { app.alert.show('project-export-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function () { app.cache.set("show_project_export_warning", false); that.exportProcess(model); }, onCancel: $.noop }); } else { that.exportProcess(model); } }, /** * Download record of table pmse_Process_Definition */ exportProcess: function (model) { var url = app.api.buildURL(model.module, 'dproject', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); }, /** * @inheritdoc * * FIXME: This should be removed when metadata supports date operators to * allow one to define relative dates for date filters. */ _initTabs: function() { this._super('_initTabs'); // FIXME: since there's no way to do this metadata driven (at the // moment) and for the sake of simplicity only filters with 'date_due' // value 'today' are replaced by today's date var today = new Date(); today.setHours(23, 59, 59); today.toISOString(); _.each(_.pluck(_.pluck(this.tabs, 'filters'), 'date_due'), function(filter) { _.each(filter, function(value, operator) { if (value === 'today') { filter[operator] = today; } }); }); }, /** * Create new record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ createRecord: function(event, params) { if (this.module !== 'pmse_Project') { this.createRelatedRecord(params.module, params.link); } else { var self = this; app.drawer.open({ layout: 'create', context: { create: true, module: params.module } }, function(context, model) { if (!model) { return; } self.context.resetLoadFlag(); self.context.set('skipFetch', false); if (_.isFunction(self.loadData)) { self.loadData(); } else { self.context.loadData(); } }); } }, importRecord: function(event, params) { App.router.navigate(params.link , {trigger: true, replace: true }); }, /** * Delete record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ deleteRecord: function(model) { var self = this, verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ); var messages = {}; var name = app.utils.getRecordName(model).trim(); var context = app.lang.getModuleName(model.module).toLowerCase() + ' ' + name; messages.confirmation = app.utils.formatString(app.lang.get('NTC_DELETE_CONFIRMATION_FORMATTED'), [context]); this._modelToDelete = true; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.alert.show(model.get('id') + ':deleted', { level: 'confirmation', messages: messages.confirmation, onConfirm: function() { model.destroy({ showAlerts: true, success: self._getRemoveRecord() }); } }); } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PA_PRODEF_HAS_PENDING_PROCESSES'), autoClose: false }); } } }); }, /** * Updating in fields delete removed * @return {Function} complete callback * @private */ _getRemoveRecord: function() { return _.bind(function(model){ if (this.disposed) { return; } this.collection.remove(model); this.render(); this.context.trigger("tabbed-dashlet:refresh", model.module); }, this); }, /** * Method view alert in process with text modify * show and hide alert */ _refresh: function(model, status) { app.alert.show(model.id + ':refresh', { level:"process", title: status, autoClose: false }); return _.bind(function(model){ var options = {}; this.layout.reloadDashlet(options); app.alert.dismiss(model.id + ':refresh'); }, this); }, /** * Disable record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ disableRecord: function(model) { var self = this; var verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ); app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.alert.show('project_disable', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_DISABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusDisabled(model); } }); } else { app.alert.show('project-disable-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_DISABLE_CONFIRMATION_PD', model.module), onConfirm: function () { self._updateProStatusDisabled(model); }, onCancel: $.noop }); } } }); }, /** * Update record in table pmse_Project in fields prj_status by INACTIVE */ _updateProStatusDisabled: function(model) { var self = this; url = App.api.buildURL(model.module, null, {id: model.id}); attributes = {prj_status: 'INACTIVE'}; //App.api.call('update', url, attributes,{success: self._refresh(model, app.lang.get('LBL_PRO_DISABLE', model.module))}); app.api.call('update', url, attributes); app.alert.show(model.id + ':refresh', { level:"process", title: app.lang.get('LBL_PRO_DISABLE', model.module), autoClose: true }); self.refresh_Dashlet(); }, /** * Enable record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ enableRecord: function(model) { var self = this; this._modelToDelete = true; var name = model.get('name') || ''; app.alert.show(model.get('id') + ':deleted', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_ENABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusEnabled(model); } }); }, /** * Update record in table pmse_Project in fields prj_status by ACTIVE */ _updateProStatusEnabled: function(model) { var self = this; url = App.api.buildURL(model.module, null, {id: model.id}); attributes = {prj_status: 'ACTIVE'}; app.api.call('update', url, attributes); app.alert.show(model.id + ':refresh', { level:"process", title: app.lang.get('LBL_PRO_ENABLE', model.module), autoClose: true }); self.refresh_Dashlet(); }, refresh_Dashlet:function(){ var $el = this.$("[data-action=loading]"), self = this, options = {}; if($el.length > 0) { $el.removeClass(this.cssIconDefault).addClass(this.cssIconRefresh); options.complete = function() { if(self.disposed) { return; } $el.removeClass(self.cssIconRefresh).addClass(self.cssIconDefault); }; } this.layout.reloadDashlet(options); }, /** * descriptionRecord: View description in table pmse_Project in fields */ descriptionRecord: function(model) { app.alert.dismiss('message-id'); app.alert.show('message-id', { level: 'info', title: app.lang.get('LBL_DESCRIPTION'), messages: '<br/>' + Handlebars.Utils.escapeExpression(model.get('description')), autoClose: false }); }, //tabs Switcher load tabSwitcher: function(event) { var index = this.$(event.currentTarget).data('index'); if (index === this.settings.get('activeTab')) { return; } this.settings.set('activeTab', index); this.render(); this.refresh_Dashlet(); }, /** * Sets property useRelativeTime to show date created as a relative time or as date time. * * @private */ _setRelativeTimeAvailable: function(date) { var diffInDays = app.date().diff(date, 'days', true); var useRelativeTime = (diffInDays <= this.thresholdRelativeTime); return useRelativeTime; }, /** * @inheritdoc * * New model related properties are injected into each model: * * - {Boolean} overdue True if record is prior to now. * - {String} picture_url Picture url for model's assigned user. * - {String} prj_module_name Name of the triggering module. */ _renderHtml: function() { if (this.meta.config) { this._super('_renderHtml'); return; } var tab = this.tabs[this.settings.get('activeTab')]; if (tab.overdue_badge) { this.overdueBadge = tab.overdue_badge; } _.each(this.collection.models, function(model) { var pictureUrl = app.api.buildFileURL({ module: 'Users', id: model.get('assigned_user_id'), field: 'picture' }); model.set('picture_url', pictureUrl); model.useRelativeTime = this._setRelativeTimeAvailable(model.attributes.date_entered); // Update the triggering module names. var module = model.get('prj_module'); var label = app.lang.getModString('LBL_MODULE_NAME', module); if (_.isUndefined(label)) { label = module; } model.set('prj_module_name', label); }, this); this._super('_renderHtml'); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @override * @param {Object} options */ initialize: function(options) { this.contextEvents = _.extend({}, this.contextEvents, { "list:opendesigner:fire": "openDesigner", "list:exportprocess:fire": "showExportingWarning", "list:enabledDisabledRow:fire": "enableDisableProcess" }); this._super('initialize', [options]); }, openDesigner: function(model) { var verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.navigate(this.context, model, 'layout/designer'); } else { app.alert.show('project-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_DEFINITIONS_EDIT', model.module), onConfirm: function () { app.navigate(this.context, model, 'layout/designer'); }, onCancel: $.noop }); } } }); }, showExportingWarning: function (model) { var that = this; if (app.cache.get("show_project_export_warning")) { app.alert.show('project-export-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function () { app.cache.set("show_project_export_warning", false); that.exportProcess(model); }, onCancel: $.noop }); } else { that.exportProcess(model); } }, exportProcess: function(model) { var url = app.api.buildURL(model.module, 'dproject', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); }, enabledProcess: function(model) { var self = this; var name = model.get('name') || ''; app.alert.show(model.get('id') + ':deleted', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_ENABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusEnabled(model); } }); }, _showSuccessAlert: function () { app.alert.show("data:sync:success", { level: "success", messages: App.lang.get('LBL_RECORD_SAVED'), autoClose: true }); }, _updateProStatusEnabled: function(model) { model.set('prj_status', 'ACTIVE'); model.save(); this._showSuccessAlert(); }, disabledProcess: function(model) { var self = this; var name = model.get('name') || ''; var verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ); app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.alert.show('project_disable', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_DISABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusDisabled(model); } }); } else { app.alert.show('project-disable-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_DISABLE_CONFIRMATION_PD', model.module), onConfirm: function () { self._updateProStatusDisabled(model); }, onCancel: $.noop }); } } }); }, _updateProStatusDisabled: function(model) { model.set('prj_status', 'INACTIVE'); model.save(); this._showSuccessAlert(); }, enableDisableProcess: function (model) { var status = model.get("prj_status"); if (status === 'ACTIVE') { this.disabledProcess(model); } else { this.enabledProcess(model); } }, getDeleteMessages: function(model) { var messages = {}; var name = Handlebars.Utils.escapeExpression(app.utils.getRecordName(model)).trim(); var context = app.lang.getModuleName(model.module).toLowerCase() + ' ' + name; messages.confirmation = app.utils.formatString(app.lang.get('NTC_DELETE_CONFIRMATION_FORMATTED'), [context]); messages.success = app.utils.formatString(app.lang.get('NTC_DELETE_SUCCESS'), [context]); return messages; }, deleteModel: function() { var self = this, model = this._modelToDelete; model.destroy({ //Show alerts for this request showAlerts: { 'process': true, 'success': { messages: self.getDeleteMessages(model).success } }, success: function() { var redirect = self._targetUrl !== self._currentUrl; self._modelToDelete = null; if (self.collection) { self.collection.remove(model, {silent: redirect}); } else { redirect = true; } if (redirect) { self.unbindBeforeRouteDelete(); //Replace the url hash back to the current staying page app.router.navigate(self._targetUrl, {trigger: true}); return; } app.events.trigger("preview:close"); if (!self.disposed) { self.render(); } self.layout.trigger("list:record:deleted", model); } }); }, warnDelete: function(model) { var verifyURL = app.api.buildURL( this.module, 'verify', { id : model.get('id') } ), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { namePd = Handlebars.Utils.escapeExpression(app.utils.getRecordName(model)).trim(); if ( (namePd !== '') && (app.lastNamePdDel !== namePd) ) { self._targetUrl = Backbone.history.getFragment(); //Replace the url hash back to the current staying page if (self._targetUrl !== self._currentUrl) { app.router.navigate(self._currentUrl, {trigger: false, replace: true}); } app.alert.show('delete_confirmation', { level: 'confirmation', messages: self.getDeleteMessages(model).confirmation, onConfirm: function() { self._modelToDelete = model; self.deleteModel(); app.lastNamePdDel = namePd; } }); } } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PA_PRODEF_HAS_PENDING_PROCESSES'), autoClose: false }); } } }); } }) }, "dependency-picker": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Project.DependencyPickerView * @alias SUGAR.App.view.views.Basepmse_ProjectDependencyPickerView * @extends View.View */ ({ // Dependency-picker View (base) /** * The dependency collections map */ collections: {}, /** * The dependency models map */ models: {}, /** * Indicates if there are any dependencies */ hasDependencies: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.context.on('updateData', this.processData, this); this.brCollection = app.data.createBeanCollection('br'); this.etCollection = app.data.createBeanCollection('et'); this.massCollection = app.data.createBeanCollection('pmse_elements'); this.brModel = app.data.createBean('br', {elementType: 'business_rule'}); this.etModel = app.data.createBean('et', {elementType: 'email_template'}); this._setUpCollectionMap(); this._setUpModelMap(); this.context.set('mass_collection', this.massCollection); this._bindMassCollectionEvents(); this.leftColumns = [{ type: 'fieldset', fields: [ { name: 'actionmenu', type: 'actionmenu', buttons: [], disable_select_all_alert: true } ], value: false, sortable: false }]; }, /** * Save collection to object for easy accessing * @private */ _setUpCollectionMap: function() { this.collections.business_rule = this.brCollection; this.collections.email_template = this.etCollection; this.collections.mass_collection = this.massCollection; }, /** * Save models to object for easy accessing * @private */ _setUpModelMap: function() { this.models.business_rules = this.brModel; this.models.email_template = this.etModel; }, /** * Add dependencies to their respective collections and render * @param data */ processData: function(data) { this._resetCollections(); this.hasDependencies = false; // No dependencies so don't do anything if (!data || !data.dependencies) { this.render(); return; } _.each(data.dependencies, function(defs, type) { var collection = this._getCollectionForType(type); // add dependency only when there's a definition if (collection && !_.isEmpty(defs)) { collection.add(defs); this.hasDependencies = true; } }, this); this._cleanFieldsForView(); this.render(); }, /** * Set up the mass collection's events * @private */ _bindMassCollectionEvents: function() { this.context.on('mass_collection:add', _.bind(this._updateModels, this, true)); this.context.on('mass_collection:add:all', _.bind(this._updateAllModels, this, true)); this.context.on('mass_collection:remove', _.bind(this._updateModels, this, false)); this.context.on('mass_collection:remove:all', _.bind(this._updateAllModels, this, false)); }, /** * Add or remove a model or models in the mass collection. If all checkboxes are in a group are checked, * toggle the select all checkbox to checked. When removing a model, uncheck the select all checkbox * * @param {boolean} `true` to add model, `false` to remove * @param {Data.Bean|Data.Bean[]} models The model or the list of models to add/remove. * @private */ _updateModels: function(addModel, models) { models = _.isArray(models) ? models : [models]; var type = _.first(models).elementType; if (addModel) { this.massCollection.add(models); if (this._isAllChecked(type)) { this._toggleAllCheckbox(type, true); } } else { this.massCollection.remove(models); this._toggleAllCheckbox(type, false); } }, /** * Add or remove all models for the collection group to the mass collection * * @param {boolean} `true` to add all models, `false` to remove all * @param {Data.Bean} checkbox Model containing elementType to indicate which * check-all box was checked * @private */ _updateAllModels: function(addModels, checkbox) { var type = checkbox.get('elementType'); var models = this._getCollectionForType(type).models; this._updateModels(addModels, models); }, /** * Checks if all elements in the collection are in the mass collection * * @param {string} type The element type * @return {boolean} `true` if all elements are in the mass collection * @private */ _isAllChecked: function(type) { var collection = this._getCollectionForType(type); if (this.massCollection.length < collection.length) { return false; } var allChecked = _.every(collection.models, function(model) { return this.massCollection.get(model.id); }, this); return allChecked; }, /** * Check/Uncheck the check-all checkbox * @param {string} type The element type * @param {boolean} check `true` to mark checked * @private */ _toggleAllCheckbox: function(type, check) { var checkboxField = this.getField('actionmenu', this._getModelForType(type)); checkboxField.$(checkboxField.fieldTag).prop('checked', check); }, /** * Get the collection for type * @param {string} type The element type * @return {Data.BeanCollection|null} The collection asked for * @private */ _getCollectionForType: function(type) { return this.collections[type] || null; }, /** * Get the model for type * @param {string} type The element type * @return {Data.Bean|null} The model asked for * @private */ _getModelForType: function(type) { return this.models[type] || null; }, /** * Set up fields for the model so we render correctly * @private */ _cleanFieldsForView: function() { _.each(this.collections, function(collection, type) { _.each(collection.models, function(model) { model.fields = this.meta.fields; model.elementType = type; }, this); }, this); }, /** * Remove all the models from the collections * @param {Data.BeanCollection[]} collections * @private */ _resetCollections: function(collections) { collections = collections || this.collections; _.each(collections, function(collection) { collection.reset(); }); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', initialize: function (options) { this._super('initialize', [options]); this.context.on('button:open_designer:click', this.openDesigner, this); this.context.on('button:export_process:click', this.showExportingWarning, this); }, openDesigner: function(model) { var verifyURL = app.api.buildURL( this.module, 'verify', { id : this.model.get('id') } ), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.navigate(this.context, model, 'layout/designer'); } else { app.alert.show('project-export-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_DEFINITIONS_EDIT', model.module), onConfirm: function () { app.navigate(this.context, model, 'layout/designer'); }, onCancel: $.noop }); } } }); }, showExportingWarning: function (model) { var that = this; if (app.cache.get("show_project_export_warning")) { app.alert.show('project-export-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function () { app.cache.set("show_project_export_warning", false); that.exportProcess(model); }, onCancel: $.noop }); } else { that.exportProcess(model); } }, exportProcess: function(model) { var url = app.api.buildURL(model.module, 'dproject', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); }, warnDelete: function() { var verifyURL = app.api.buildURL( this.module, 'verify', { id : this.model.get('id') } ), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { self._super('warnDelete', []); } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PA_PRODEF_HAS_PENDING_PROCESSES'), autoClose: false }); } } }); }, duplicateClicked: function() { var self = this, prefill = app.data.createBean(this.model.module); prefill.copy(this.model); this._copyNestedCollections(this.model, prefill); prefill.fields.prj_module.readonly = true; self.model.trigger('duplicate:before', prefill); prefill.unset('id'); app.drawer.open({ layout: 'create', context: { create: true, model: prefill, copiedFromModelId: this.model.get('id') } }, function(context, newModel) { if (newModel && newModel.id) { app.router.navigate(self.model.module + '/' + newModel.id, {trigger: true}); } }); prefill.trigger('duplicate:field', self.model); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Project.CreateView * @alias SUGAR.App.view.views.pmse_ProjectCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', saveOpenDesignName: 'save_open_design', SAVEACTIONS: { SAVE_OPEN_DESIGN: 'saveOpenDesign' }, initialize: function(options) { options.meta = _.extend({}, app.metadata.getView(null, 'create'), options.meta); this._super('initialize', [options]); this.context.on('button:' + this.saveOpenDesignName + ':click', this.saveOpenDesign, this); }, save: function () { switch (this.context.lastSaveAction) { case this.SAVEACTIONS.SAVE_OPEN_DESIGN: this.saveOpenDesign(); break; default: this.saveAndClose(); } }, saveOpenDesign: function() { this.context.lastSaveAction = this.SAVEACTIONS.SAVE_OPEN_DESIGN; this.initiateSave(_.bind(function () { app.navigate(this.context, this.model, 'layout/designer'); }, this)); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Project.PreviewView * @alias SUGAR.App.view.views.Basepmse_ProjectPreviewView * @extends View.Views.Base.RecordView */ ({ // Preview View (base) extendsFrom: 'PreviewView', events: { 'click .minify': 'toggleMinify' }, toggleMinify: function (evt) { var $el = this.$('.dashlet-toggle > i'), collapsed = $el.is('.sicon-chevron-up'); if (collapsed) { $('.dashlet-toggle > i').removeClass('sicon-chevron-up'); $('.dashlet-toggle > i').addClass('sicon-chevron-down'); } else { $('.dashlet-toggle > i').removeClass('sicon-chevron-down'); $('.dashlet-toggle > i').addClass('sicon-chevron-up'); } $('.dashlet').toggleClass('collapsed'); $('.dashlet-content').toggleClass('hide'); }, /** * @override Overriding so we can set this.image_preview_url for the * Process Definition image */ _render: function() { if (this.model) { var pmseInboxUrl = app.api.buildFileURL({ module: 'pmse_Project', id: this.model.get('id'), field: 'id' }, {cleanCache: true}); this.image_preview_url = pmseInboxUrl; } this._super('_render'); } }) }, "project-import-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Project-import-headerpane View (base) extendsFrom: 'HeaderpaneView', events:{ 'click [name=project_finish_button]': 'initiateFinish', 'click [name=project_cancel_button]': 'initiateCancel' }, initiateFinish: function() { var that = this; if (app.cache.get("show_project_import_warning")) { app.alert.show('project-import-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_IMPORT_CONFIRMATION'), onConfirm: function () { app.cache.set("show_project_import_warning", false); that.context.trigger('project:import:finish'); }, onCancel: function () { app.router.goBack(); } }); } else { that.context.trigger('project:import:finish'); } }, initiateCancel : function() { app.router.navigate(app.router.buildRoute(this.module), {trigger: true}); } }) }, "project-import": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Project-import View (base) events: { 'change input[name=project_import]': 'readFile', }, initialize: function(options) { app.view.View.prototype.initialize.call(this, options); this.context.off("project:import:finish", null, this); this.context.on("project:import:finish", this.importProject, this); }, /** * Gets the file and parses its data */ readFile: function() { var file = $('[name=project_import]')[0].files.item(0); if (!file) { this.context.trigger('updateData'); return; } var callback = _.bind(function(text) { var json = {}; try { json = JSON.parse(text); } catch (error) { } this.context.trigger('updateData', json); }, this); this.fileToText(file, callback); }, /** * Use FileReader to read the file * * @param file * @param callback */ fileToText: function(file, callback) { var reader = new FileReader(); reader.readAsText(file); reader.onload = function() { callback(reader.result); }; }, /** * @inheritdoc * * Sets up the file field to edit mode * * @param {View.Field} field * @private */ _renderField: function(field) { app.view.View.prototype._renderField.call(this, field); if (field.name === 'project_import') { field.setMode('edit'); } }, /** * Import the Process Definition File (.bpm) */ importProject: function() { var self = this, projectFile = $('[name=project_import]'); // Check if a file was chosen if (_.isEmpty(projectFile.val())) { app.alert.show('error_validation_process', { level:'error', messages: app.lang.get('LBL_PMSE_PROCESS_DEFINITION_EMPTY_WARNING', self.module), autoClose: false }); } else { app.alert.show('upload', {level: 'process', title: 'LBL_UPLOADING', autoclose: false}); var callbacks = { success: function(data) { app.alert.dismiss('upload'); var route = app.router.buildRoute(self.module, data.project_import.id); route = route + '/layout/designer?imported=true'; app.router.navigate(route, {trigger: true}); app.alert.show('process-import-saved', { level: 'success', messages: app.lang.get('LBL_PMSE_PROCESS_DEFINITION_IMPORT_SUCCESS', self.module), autoClose: true }); // Shows warning message if PD contains BR if (data.project_import.br_warning) { app.alert.show('process-import-save-with-br', { level: 'warning', messages: app.lang.get('LBL_PMSE_PROCESS_DEFINITION_IMPORT_BR', self.module), autoClose: false }); } }, error: function(error) { var messages = [ app.lang.get('LBL_PMSE_PROCESS_DEFINITION_IMPORT_ERROR', self.module), ' ', error.message ]; app.alert.dismiss('upload'); app.alert.show('process-import-saved', { level: 'error', messages: messages, autoClose: false }); } }; var ids = this._getSelectedIds(); var attributes = { id: undefined, module: this.model.module, field: 'project_import' }; var ajaxParams = { processData: false, contentType: false, }; var fd = new FormData(); fd.append('selectedIds', JSON.stringify(ids)); var attachedFile = projectFile[0]; // we check if we really have files to work with if (!_.isUndefined(attachedFile) && attachedFile.files && attachedFile.files.length) { fd.append(attributes.field, attachedFile.files[0]); } var urlOptions = { 'htmlJsonFormat': false, 'deleteIfFails': true }; var url = app.api.buildFileURL(attributes, urlOptions); app.api.call('create', url, fd, callbacks, ajaxParams); } }, /** * Get IDs for models selected in mass collection * @return {Array} An array of IDs * @private */ _getSelectedIds: function() { var collection = this.context.get('mass_collection'); return collection ? _.pluck(collection.models, 'id') : []; } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "pmse_Business_Rules":{"fieldTemplates": { "base": { "hidden": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Hidden FieldTemplate (base) _render: function () { if (this.name === 'rst_source_definition') { this.view.$('[data-name=rst_source_definition].record-cell').addClass('hide'); } this._super("_render", arguments); } }) } }} , "views": { "base": { "dashlet-businessrules": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-businessrules View (base) extendsFrom: 'TabbedDashletView', /** * @inheritdoc * * @property {Number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '10'. * @property {String} _defaultSettings.visibility Records visibility * regarding current user, supported values are 'user' and 'group', * defaults to 'user'. */ _defaultSettings: { limit: 10, visibility: 'user' }, thresholdRelativeTime: 2, //Show relative time for 2 days and then date time after /** * @inheritdoc */ initialize: function(options) { options.meta = options.meta || {}; options.meta.template = 'tabbed-dashlet'; this.plugins = _.union(this.plugins, [ 'LinkedModel' ]); this._super('initialize', [options]); }, /** * @inheritdoc */ _initEvents: function() { this._super('_initEvents'); this.on('dashlet-businessrules:businessRulesLayout:fire', this.businessRulesLayout, this); this.on('dashlet-businessrules:delete-record:fire', this.deleteRecord, this); this.on('dashlet-businessrules:download:fire', this.warnExportBusinessRules, this); this.on('dashlet-businessrules:description-record:fire', this.descriptionRecord, this); this.on('linked-model:create', this.loadData, this); return this; }, /** * Re-fetches the data for the context's collection. * * FIXME: This will be removed when SC-4775 is implemented. * * @private */ _reloadData: function() { this.context.set('skipFetch', false); this.context.reloadData(); }, /** * Fire dessigner */ businessRulesLayout: function (model) { var redirect = model.module+"/"+model.id+"/layout/businessrules"; var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.router.navigate(redirect , {trigger: true, replace: true }); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_EDIT', model.module), onConfirm: function () { app.router.navigate(redirect , {trigger: true, replace: true }); }, onCancel: $.noop }); } } }); }, /** * @inheritdoc * * FIXME: This should be removed when metadata supports date operators to * allow one to define relative dates for date filters. */ _initTabs: function() { this._super('_initTabs'); // FIXME: since there's no way to do this metadata driven (at the // moment) and for the sake of simplicity only filters with 'date_due' // value 'today' are replaced by today's date var today = new Date(); today.setHours(23, 59, 59); today.toISOString(); _.each(_.pluck(_.pluck(this.tabs, 'filters'), 'date_due'), function(filter) { _.each(filter, function(value, operator) { if (value === 'today') { filter[operator] = today; } }); }); }, /** * Create new record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ createRecord: function(event, params) { if (this.module !== 'pmse_Business_Rules') { this.createRelatedRecord(params.module, params.link); } else { var self = this; app.drawer.open({ layout: 'create', context: { create: true, module: params.module } }, function(context, model) { if (!model) { return; } self.context.resetLoadFlag(); self.context.set('skipFetch', false); if (_.isFunction(self.loadData)) { self.loadData(); } else { self.context.loadData(); } }); } }, importRecord: function(event, params) { App.router.navigate(params.link , {trigger: true, replace: true }); }, /** * Delete record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ deleteRecord: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.alert.show('delete_confirmation', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_DELETE_CONFIRMATION', model.module)), onConfirm: function () { model.destroy({ showAlerts: true, success: self._getRemoveRecord() }); }, onCancel: function () { self._modelToDelete = null; } }); } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_DELETE', model.module), autoClose: false }); self._modelToDelete = null; } } }); }, /** * Updating in fields delete removed * @return {Function} complete callback * @private */ _getRemoveRecord: function() { return _.bind(function(model){ if (this.disposed) { return; } this.collection.remove(model); this.render(); this.context.trigger("tabbed-dashlet:refresh", model.module); }, this); }, /** * Method view alert in process with text modify * show and hide alert */ _refresh: function(model, status) { app.alert.show(model.id + ':refresh', { level:"process", title: status, autoclose: false }); return _.bind(function(model){ var options = {}; this.layout.reloadDashlet(options); app.alert.dismiss(model.id + ':refresh'); }, this); }, /** * Disable record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ disableRecord: function(model) { var self = this; this._modelToDelete = true; var name = model.get('name') || ''; app.alert.show(model.get('id') + ':deleted', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_DISABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusDisabled(model); } }); }, /** * Update record in table pmse_Project in fields prj_status by INACTIVE */ _updateProStatusDisabled: function(model) { var self = this; url = App.api.buildURL(model.module, null, {id: model.id}); attributes = {prj_status: 'INACTIVE'}; App.api.call('update', url, attributes, { success: self._refresh(model, app.lang.get('LBL_PRO_DISABLE', model.module)), error: function (err) { // app.error.handleHttpError(err); if (callback) callback(err); // self.isWaitingResponse = false; // self.mergeDirtyElements(); // self.isDirty = false; } }); }, /** * Enable record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ enableRecord: function(model) { var self = this; this._modelToDelete = true; var name = model.get('name') || ''; app.alert.show(model.get('id') + ':deleted', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_ENABLE_CONFIRMATION', model.module),[name.trim()]), onConfirm: function() { self._updateProStatusEnabled(model); } }); }, /** * Update record in table pmse_Project in fields prj_status by ACTIVE */ _updateProStatusEnabled: function(model) { var self = this; url = App.api.buildURL(model.module, null, {id: model.id}); attributes = {prj_status: 'ACTIVE'}; App.api.call('update', url, attributes, { success: self._refresh(model,app.lang.get('LBL_PRO_ENABLE', model.module)), error: function (err) { // app.error.handleHttpError(err); if (callback) callback(err); // self.isWaitingResponse = false; // self.mergeDirtyElements(); // self.isDirty = false; } }); }, /** * Show warning of pmse_bussiness_rules */ warnExportBusinessRules: function (model) { var that = this; if (app.cache.get("show_br_export_warning")) { app.alert.show('show-br-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function() { app.cache.set("show_br_export_warning", false); that.exportBusinessRules(model); }, onCancel: $.noop }); } else { that.exportBusinessRules(model); } }, /** * Download record of table pmse_business_rules */ exportBusinessRules: function (model) { var url = app.api.buildURL(model.module, 'brules', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Business Rule download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); }, /** * descriptionRecord: View description in table pmse_Business Rules in fields */ descriptionRecord: function(model) { app.alert.dismiss('message-id'); app.alert.show('message-id', { level: 'info', title: app.lang.get('LBL_DESCRIPTION'), messages: '<br/>' + Handlebars.Utils.escapeExpression(model.get('description')), autoClose: false }); }, /** * Sets property useRelativeTime to show date created as a relative time or as date time. * * @private */ _setRelativeTimeAvailable: function(date) { var diffInDays = app.date().diff(date, 'days', true); var useRelativeTime = (diffInDays <= this.thresholdRelativeTime); return useRelativeTime; }, /** * @inheritdoc * * New model related properties are injected into each model: * * - {Boolean} overdue True if record is prior to now. * - {String} picture_url Picture url for model's assigned user. * - {String} rst_module_name Name of the triggering module. */ _renderHtml: function() { if (this.meta.config) { this._super('_renderHtml'); return; } var tab = this.tabs[this.settings.get('activeTab')]; if (tab.overdue_badge) { this.overdueBadge = tab.overdue_badge; } _.each(this.collection.models, function(model) { var pictureUrl = app.api.buildFileURL({ module: 'Users', id: model.get('assigned_user_id'), field: 'picture' }); model.set('picture_url', pictureUrl); model.useRelativeTime = this._setRelativeTimeAvailable(model.attributes.date_entered); // Update the triggering module names. var module = model.get('rst_module'); var label = app.lang.getModString('LBL_MODULE_NAME', module); if (_.isUndefined(label)) { label = module; } model.set('rst_module_name', label); }, this); this._super('_renderHtml'); } }) }, "businessrules": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Businessrules View (base) className: 'businessrules', /** * @inheritdoc */ loadData: function (options) { this.br_uid = this.options.context.attributes.modelId; }, /** * @inheritdoc */ initialize: function(options) { app.view.View.prototype.initialize.call(this, options); this.context.off("businessRules:save:finish", null, this); this.context.on("businessRules:save:finish", this.saveBusinessRules, this); this.context.off("businessRules:save:save", null, this); this.context.on("businessRules:save:save", this.saveOnlyBusinessRules, this); this.context.off("businessRules:cancel:button", null, this); this.context.on("businessRules:cancel:button", this.cancelBusinessRules, this); this.myDefaultLayout = this.closestComponent('sidebar'); app.routing.before('route', _.bind(this.beforeRouteChange, this), this, true); this._currentUrl = Backbone.history.getFragment(); this._decisionTable = null; this._brName = null; this._brModule = null; }, /** * Updates the Business Rules decision table header text. * @param name * @param module * @private */ _updateBRHeader: function (name, module) { this.$('.brTitle').text(name); var brModule = app.lang.get('LBL_RST_MODULE', this.module) + ': ' + module; this.$('.brModule').text(brModule); }, /** * Creates the Business Rules decision table * @param data * @private */ _addDecisionTable: function (data) { var module = 'pmse_Business_Rules'; var pmseCurrencies = []; var currencies = App.metadata.getCurrencies(); var that = this; for (currID in currencies) { if (currencies.hasOwnProperty(currID)) { if (currencies[currID].status === 'Active') { pmseCurrencies.push({ id: currID, iso: currencies[currID].iso4217, name: currencies[currID].name, rate: parseFloat(currencies[currID].conversion_rate), preferred: currID === App.user.getCurrency().currency_id, symbol: currencies[currID].symbol }); } } } $.extend(true, data, { dateFormat: App.date.getUserDateFormat(), timeFormat: App.user.getPreference("timepref"), currencies: pmseCurrencies }); this._decisionTable = new DecisionTable(data); if (!this._decisionTable.correctlyBuilt) { this.$('#save').hide(); } this._decisionTable.onDirty = function (state) { if (state) { updateName = that._brName + " *"; } else { updateName = that._brName; } that.$(".brTitle").text(updateName); }; this._decisionTable.onAddColumn = this._decisionTable.onAddRow = this._decisionTable.onRemoveColumn = this._decisionTable.onRemoveRow; this.$('#businessruledesigner').prepend(this._decisionTable.getHTML()); }, /** * Initialize the Business Rules decision table. * @param params * @private */ _initDecisionTable: function (params) { var data; this._brName = params.data.name; this._brModule = App.lang.getModuleName(params.data.rst_module, {plural: true}); //errorLog = $('#error-log'); if (params.data && params.data.rst_source_definition) { data = JSON.parse(params.data.rst_source_definition); } else { data = { "saveedit":"1", "btnSubmitEdit":"Save and Edit", "id":params.data.id, "name":params.data.name, "base_module":params.data.rst_module, "type":"single", "columns":{ "conditions":[], "conclusions":[] }, "ruleset":[ { "conditions":[], "conclusions":[] } ] } } this._updateBRHeader(this._brName, this._brModule); this._addDecisionTable(data); this._decisionTable.setIsDirty(false); }, /** * @inheritdoc */ render: function () { var that = this; app.view.View.prototype.render.call(this); var params = { br_uid: this.br_uid }; App.api.call("read", App.api.buildURL("pmse_Business_Rules", null, {id: this.br_uid }), {}, { success: function (response) { params.data = response; that._initDecisionTable(params); } }); }, /** * Saves the Buiness Rules decision table data. * @param route * @param id */ _saveBR: function (id, route) { var json, base64encoded, url, validation = this._decisionTable.isValid(), that = this; if (this._decisionTable && validation.valid) { json = this._decisionTable.getJSON(); base64encoded = JSON.stringify(json); url = App.api.buildURL('pmse_Business_Rules', null, {id: id}); attributes = {rst_source_definition: base64encoded}; App.alert.show('upload', {level: 'process', title: 'LBL_SAVING', autoclose: false}); App.api.call('update', url, attributes, { success: function (data) { App.alert.dismiss('upload'); App.alert.show('save-success', { level: 'success', messages: App.lang.get('LBL_SAVED'), autoClose: true }); if (route) { that._decisionTable.setIsDirty(false, true); App.router.navigate(route, {trigger: true}); } else { that._decisionTable.setIsDirty(false); } }, error: function (err) { App.alert.dismiss('upload'); } }); } else { App.alert.show('br-save-error', { level: 'error', messages: validation.location + ": " + validation.message, autoClose: true }); } }, /** * Handler for the 'businessRules:save:finish' event. */ saveBusinessRules: function() { this._saveBR(this.model.id, App.router.buildRoute("pmse_Business_Rules")); }, /** * Handler for the 'businessRules:save:save' */ saveOnlyBusinessRules: function() { this._saveBR(this.model.id); }, /** * Handler for the 'businessRules:cancel:button' event. */ cancelBusinessRules: function () { app.router.navigate('pmse_Business_Rules', {trigger: true}); }, /** * @inheritdoc * @returns {boolean} */ beforeRouteChange: function () { var targetUrl = Backbone.history.getFragment(), that = this; if (this._decisionTable.getIsDirty()) { //Replace the url hash back to the current staying page app.router.navigate(this._currentUrl, {trigger: false, replace: true}); app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WARN_UNSAVED_CHANGES', this.module), onConfirm: function () { that._decisionTable.setIsDirty(false, true); app.router.navigate(targetUrl , {trigger: true, replace: true }); }, onCancel: $.noop }); return false; } return true; }, /** * @inheritdoc */ _dispose: function () { this._super('_dispose', arguments); } }) }, "businessrules-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Businessrules-headerpane View (base) extendsFrom: 'HeaderpaneView', events:{ 'click [name=project_finish_button]': 'initiateFinish', 'click [name=project_save_button]': 'initiateSave', 'click [name=project_cancel_button]': 'initiateCancel' }, initiateFinish: function() { this.context.trigger('businessRules:save:finish'); }, initiateSave: function() { this.context.trigger('businessRules:save:save'); }, initiateCancel : function() { this.context.trigger('businessRules:cancel:button'); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * Override the edit event name so that double click to edit works on list views of this module */ editEventName: 'list:edit_businessrules:fire', /** * @override * @param {Object} options */ initialize: function(options) { this.contextEvents = _.extend({}, this.contextEvents, { "list:editbusinessrules:fire": "openBusinessRules", "list:exportbusinessrules:fire": "warnExportBusinessRules", "list:edit_businessrules:fire": "warnEditBusinessRules", "list:deletebusinessrules:fire": "warnDeleteBusinessRules" }); this._super('initialize', [options]); }, openBusinessRules: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.navigate(this.context, model, 'layout/businessrules'); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_EDIT', model.module), onConfirm: function () { app.navigate(this.context, model, 'layout/businessrules'); }, onCancel: $.noop }); } } }); }, warnEditBusinessRules: function(model){ var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { self.toggleRow(model.id, true); self.resize(); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_EDIT', model.module), onConfirm: function () { self.toggleRow(model.id, true); self.resize(); }, onCancel: $.noop }); } } }); }, warnDeleteBusinessRules: function (model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { self._targetUrl = Backbone.history.getFragment(); //Replace the url hash back to the current staying page if (self._targetUrl !== self._currentUrl) { app.router.navigate(self._currentUrl, {trigger: false, replace: true}); } app.alert.show('delete_confirmation', { level: 'confirmation', messages: self.getDeleteMessages(model).confirmation, onConfirm: function () { self.deleteModel(); }, onCancel: function () { self._modelToDelete = null; } }); } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_DELETE', model.module), autoClose: false }); self._modelToDelete = null; } } }); }, warnExportBusinessRules: function (model) { var that = this; if (app.cache.get("show_br_export_warning")) { app.alert.show('show-br-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function() { app.cache.set("show_br_export_warning", false); that.exportBusinessRules(model); }, onCancel: $.noop }); } else { that.exportBusinessRules(model); } }, exportBusinessRules: function(model) { var url = app.api.buildURL(model.module, 'brules', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', initialize: function (options) { this._super('initialize', [options]); this.context.on('button:design_businessrules:click', this.designBusinessRules, this); this.context.on('button:export_businessrules:click', this.warnExportBusinessRules, this); this.context.on('button:delete_businessrules:click', this.warnDeleteBusinessRules, this); this.context.on('button:edit_businessrules:click', this.warnEditBusinessRules, this); }, warnEditBusinessRules: function(model){ var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { self.editClicked(); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_EDIT', model.module), onConfirm: function () { self.editClicked(); }, onCancel: $.noop }); } } }); }, warnDeleteBusinessRules: function (model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.alert.show('delete_confirmation', { level: 'confirmation', messages: self.getDeleteMessages(model).confirmation, onConfirm: function () { self.deleteModel(); }, onCancel: function () { self._modelToDelete = null; } }); } else { app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_DELETE', model.module), autoClose: false }); self._modelToDelete = null; } } }); }, handleEdit: function(e, cell) { this.warnEditBusinessRules(this.model); }, designBusinessRules: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}), self = this; app.api.call('read', verifyURL, null, { success: function(data) { if (!data) { app.navigate(this.context, model, 'layout/businessrules'); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: App.lang.get('LBL_PMSE_PROCESS_BUSINESS_RULES_EDIT', model.module), onConfirm: function () { app.navigate(this.context, model, 'layout/businessrules'); }, onCancel: $.noop }); } } }); }, warnExportBusinessRules: function (model) { var that = this; if (app.cache.get("show_br_export_warning")) { app.alert.show('show-br-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: function() { app.cache.set("show_br_export_warning", false); that.exportBusinessRules(model); }, onCancel: $.noop }); } else { that.exportBusinessRules(model); } }, exportBusinessRules: function(model) { var url = app.api.buildURL(model.module, 'brules', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }, {iframe: this.$el}); }, duplicateClicked: function() { var self = this, prefill = app.data.createBean(this.model.module); prefill.copy(this.model); this._copyNestedCollections(this.model, prefill); prefill.fields.rst_module.readonly = true; self.model.trigger('duplicate:before', prefill); prefill.unset('id'); app.drawer.open({ layout: 'create', context: { create: true, model: prefill, copiedFromModelId: this.model.get('id') } }, function(context, newModel) { if (newModel && newModel.id) { app.router.navigate(self.model.module + '/' + newModel.id, {trigger: true}); } }); prefill.trigger('duplicate:field', self.model); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Business_Rules.CreateView * @alias SUGAR.App.view.views.pmse_Business_RulesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', saveOpenBusinessRulesName: 'save_open_businessrules', SAVEACTIONS: { SAVE_OPEN_BUSINESRULES: 'saveOpenBusinessRules' }, initialize: function(options) { options.meta = _.extend({}, app.metadata.getView(null, 'create'), options.meta); this._super('initialize', [options]); this.context.on('button:' + this.saveOpenBusinessRulesName + ':click', this.saveOpenBusinessRules, this); }, save: function () { switch (this.context.lastSaveAction) { case this.SAVEACTIONS.SAVE_OPEN_BUSINESRULES: this.saveOpenBusinessRules(); break; default: this.saveAndClose(); } }, saveOpenBusinessRules: function() { this.context.lastSaveAction = this.SAVEACTIONS.SAVE_OPEN_BUSINESRULES; this.initiateSave(_.bind(function () { app.navigate(this.context, this.model, 'layout/businessrules'); }, this)); } }) }, "businessrules-import-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Businessrules-import-headerpane View (base) extendsFrom: 'HeaderpaneView', events:{ 'click [name=businessrules_finish_button]': 'initiateFinish', 'click [name=businessrules_cancel_button]': 'initiateCancel' }, initiateFinish: function() { this.context.trigger('businessrules:import:finish'); }, initiateCancel : function() { app.router.navigate(app.router.buildRoute(this.module), {trigger: true}); } }) }, "businessrules-import": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Businessrules-import View (base) initialize: function(options) { app.view.View.prototype.initialize.call(this, options); this.context.off("businessrules:import:finish", null, this); this.context.on("businessrules:import:finish", this.warnImportBusinessRules, this); }, /** * @inheritdoc * * Sets up the file field to edit mode * * @param {View.Field} field * @private */ _renderField: function(field) { app.view.View.prototype._renderField.call(this, field); if (field.name === 'businessrules_import') { field.setMode('edit'); } }, warnImportBusinessRules: function () { var that = this; if (app.cache.get('show_br_import_warning')) { app.alert.show('br-import-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + "<br/><br/>" + app.lang.get('LBL_PMSE_IMPORT_CONFIRMATION'), onConfirm: function () { app.cache.set('show_br_import_warning', false); that.importBusinessRules(); }, onCancel: function () { app.router.goBack(); } }); } else { this.importBusinessRules(); } }, /** * Import the Business Rules file (.pbr) */ importBusinessRules: function() { var self = this, projectFile = $('[name=businessrules_import]'); // Check if a file was chosen if (_.isEmpty(projectFile.val())) { app.alert.show('error_validation_businessrules', { level:'error', messages: app.lang.get('LBL_PMSE_BUSINESS_RULES_EMPTY_WARNING', self.module), autoClose: false }); } else { app.alert.show('upload', {level: 'process', title: 'LBL_UPLOADING', autoclose: false}); var callbacks = { success: function (data) { app.alert.dismiss('upload'); app.router.goBack(); app.alert.show('process-import-saved', { level: 'success', messages: app.lang.get('LBL_PMSE_BUSINESS_RULES_IMPORT_SUCCESS', self.module), autoClose: true }); }, error: function (error) { app.alert.dismiss('upload'); app.alert.show('process-import-saved', { level: 'error', messages: error.error_message, autoClose: false }); } }; this.model.uploadFile('businessrules_import', projectFile, callbacks, {deleteIfFails: true, htmlJsonFormat: true}); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "pmse_Emails_Templates":{"fieldTemplates": { "base": { "from_address": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // From_address FieldTemplate (base) 'events': { 'keyup input[name=name]': 'handleKeyup', "click .btn": "_showVarBook" }, fieldTag: 'input.inherit-width', _render: function() { if (this.view.name === 'record') { this.def.link = false; } else if (this.view.name === 'preview') { this.def.link = true; } this._super('_render'); }, /** * Gets the recipients DOM field * * @returns {Object} DOM Element */ getFieldElement: function() { return this.$(this.fieldTag); }, /** * When in edit mode, the field includes an icon button for opening an address book. Clicking the button will * trigger an event to open the address book, which calls this method to do the dirty work. The selected recipients * are added to this field upon closing the address book. * * @private */ _showVarBook: function() { /** * Callback to add recipients, from a closing drawer, to the target Recipients field. * @param {undefined|Backbone.Collection} recipients */ var addEmails = _.bind(function(emails) { if (emails && emails.length > 0) { this.model.set(this.name, this.buildVariablesString(emails)); } }, this); app.drawer.open( { layout: "compose-addressbook", context: { module: "Emails", mixed: true } }, function(emails) { addEmails(emails); } ); }, buildVariablesString: function(recipients) { var result = '' , newExpression = '', i = 0; _.each(recipients.models, function(model) { newExpression += (i > 0) ? ', ': ''; newExpression += model.attributes.email; i += 1; }); result = newExpression; return result; }, /** * Handles the keyup event in the account create page */ handleKeyup: _.throttle(function() { var searchedValue = this.$('input.inherit-width').val(); if (searchedValue && searchedValue.length >= 3) { this.context.trigger('input:name:keyup', searchedValue); } }, 1000, {leading: false}) }) }, "subject": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Subject FieldTemplate (base) events: { 'keyup input[name=name]': 'handleKeyup', 'click .btn': '_showVarBook' }, fieldTag: 'input.inherit-width', _render: function() { if (this.view.name === 'record') { this.def.link = false; } else if (this.view.name === 'preview') { this.def.link = true; } this._super('_render'); }, /** * Gets the recipients DOM field * * @returns {Object} DOM Element */ getFieldElement: function() { return this.$(this.fieldTag); }, /** * When in edit mode, the field includes an icon button for opening an address book. Clicking the button will * trigger an event to open the address book, which calls this method to do the dirty work. The selected recipients * are added to this field upon closing the address book. * * @private */ _showVarBook: function() { /** * Callback to add recipients, from a closing drawer, to the target Recipients field. * @param {undefined|Backbone.Collection} recipients */ var addVariables = _.bind(function(variables) { if (variables && variables.length > 0) { this.model.set(this.name, this.buildVariablesString(variables)); } }, this); app.drawer.open( { layout: "compose-varbook", context: { module: "pmse_Emails_Templates", mixed: true } }, function(variables) { addVariables(variables); } ); }, /** * Adds placeholders fields to the subject field textbox. * * @param {Object} recipients List of fields to create the placeholders. * @return {string} textbox content with the placeholders. */ buildVariablesString: function(recipients) { var currentValue; var newExpression = this.buildPlaceholders(recipients); var input = this.getFieldElement().get(0); currentValue = input.value; i = input.selectionStart; result = currentValue.substr(0, i) + newExpression + currentValue.substr(i); return result; }, /** * Creates the placeholders for Email Template Modules. * * @param {Object} recipients List of fields to create the placeholders. * @return {string} newExpression. */ buildPlaceholders: function(recipients) { var newExpression = ''; _.each(recipients, function(model) { newExpression += '{::' + model.get('rhs_module') + '::' + model.get('id'); if (model.get('process_et_field_type') == 'old') { newExpression += '::' + model.get('process_et_field_type'); } newExpression += '::}'; }); return newExpression; }, /** * Handles the keyup event in the account create page */ handleKeyup: _.throttle(function() { var searchedValue = this.$('input.inherit-width').val(); if (searchedValue && searchedValue.length >= 3) { this.context.trigger('input:name:keyup', searchedValue); } }, 1000, {leading: false}) }) }, "readonly": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.pmse_Emails_Templates.ReadonlyField * @alias SUGAR.App.view.fields.Basepmse_Emails_TemplatesReadonlyField * @extends View.Fields.Base.BaseField */ ({ // Readonly FieldTemplate (base) fieldTag: 'input.inherit-width', /** * @inheritdoc */ initialize: function(options) { options.def.readonly = true; this._super('initialize', [options]); }, _render: function() { if (this.view.name === 'record') { this.def.link = false; } else if (this.view.name === 'preview') { this.def.link = true; } this._super('_render'); }, /** * Gets the recipients DOM field * * @returns {Object} DOM Element */ getFieldElement: function() { return this.$(this.fieldTag); }, /** * @inheritdoc */ format: function(value) { return app.lang.getModuleName(value, {plural: true}) } }) }, "pmse_htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Pmse_htmleditable_tinymce FieldTemplate (base) extendsFrom: 'Htmleditable_tinymceField', /** * @inheritdoc */ addCustomButtons: function (editor) { this._registerIcons(editor); editor.ui.registry.addButton('sugarfieldbutton', { title: app.lang.get('LBL_SUGAR_FIELD_SELECTOR', 'pmse_Emails_Templates'), class: 'mce_selectfield', icon: 'preferences', onAction: _.bind(this._showVariablesBook, this), }); editor.ui.registry.addButton('sugarlinkbutton', { title: app.lang.get('LBL_SUGAR_LINK_SELECTOR', 'pmse_Emails_Templates'), class: 'mce_selectfield', icon: 'sugar-record-link', onAction: _.bind(this._showLinksDrawer, this), }); }, /** * Add custom icons to TinyMCE * @private */ _registerIcons: function(editor) { const icons = [ { name: 'sugar-record-link', svg: `<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M11.3529 4.17647C11.5061 4.17647 11.6532 4.23775 11.7635 4.34804L12.6581 5.24877C12.7623 5.35907 12.8174 5.51226 12.8174 5.66544C12.8174 5.81863 12.7623 5.97181 12.652 6.08211L11.3836 7.34436C11.2733 7.45466 11.1201 7.51593 10.9669 7.51593C10.7831 7.51593 10.6422 7.4424 10.5196 7.31373C10.7218 7.11152 10.9608 6.93995 10.9608 6.62745C10.9608 6.3027 10.6973 6.03922 10.3725 6.03922C10.06 6.03922 9.88848 6.27819 9.68628 6.48039C9.5576 6.35784 9.4902 6.21691 9.4902 6.03922C9.4902 5.88603 9.55147 5.73284 9.66177 5.62255L10.9363 4.34804C11.0466 4.23775 11.1936 4.17647 11.3529 4.17647ZM7.03309 8.48407C7.21691 8.48407 7.35784 8.5576 7.48039 8.68627C7.27819 8.88848 7.03922 9.06005 7.03922 9.37255C7.03922 9.6973 7.3027 9.96078 7.62745 9.96078C7.93995 9.96078 8.11152 9.72181 8.31373 9.51961C8.4424 9.64216 8.50368 9.78309 8.50368 9.96078C8.50368 10.114 8.44853 10.2672 8.33824 10.3775L7.06373 11.652C6.95343 11.7623 6.80637 11.8235 6.64706 11.8235C6.49387 11.8235 6.34681 11.7623 6.23652 11.652L5.34191 10.7512C5.23775 10.6409 5.17647 10.4877 5.17647 10.3346C5.17647 10.1814 5.23775 10.0282 5.34804 9.91789L6.61642 8.65564C6.72672 8.54534 6.8799 8.48407 7.03309 8.48407ZM11.3529 3C10.8811 3 10.44 3.1777 10.1029 3.51471L8.82843 4.78922C8.49755 5.1201 8.31373 5.57353 8.31373 6.03922C8.31373 6.52328 8.5098 6.97672 8.85294 7.31373L8.31373 7.85294C7.97672 7.5098 7.51716 7.31373 7.03309 7.31373C6.5674 7.31373 6.1201 7.49142 5.78922 7.8223L4.52083 9.08456C4.18382 9.41544 4 9.86275 4 10.3346C4 10.8002 4.1777 11.2475 4.50858 11.5784L5.40319 12.4792C5.72794 12.81 6.18137 13 6.64706 13C7.11887 13 7.56005 12.8223 7.89706 12.4853L9.17157 11.2108C9.50245 10.8799 9.68628 10.4265 9.68628 9.96078C9.68628 9.47672 9.4902 9.02328 9.14706 8.68627L9.68628 8.14706C10.0233 8.4902 10.4828 8.68627 10.9669 8.68627C11.4326 8.68627 11.8799 8.50858 12.2108 8.1777L13.4792 6.91544C13.8162 6.58456 14 6.13726 14 5.66544C14 5.19975 13.8223 4.75245 13.4914 4.42157L12.5968 3.52083C12.2721 3.18995 11.8186 3 11.3529 3Z" fill="#595959"/> <path d="M0.429443 7.38951C0.816162 7.37946 1.13006 7.27651 1.37113 7.08064C1.61722 6.87974 1.78045 6.60603 1.86081 6.25949C1.94116 5.91295 1.98385 5.32031 1.98887 4.48159C1.9939 3.64286 2.00896 3.0904 2.03408 2.82422C2.07928 2.40234 2.16214 2.06334 2.28268 1.8072C2.40824 1.55106 2.56142 1.34766 2.74222 1.19699C2.92303 1.04129 3.15405 0.92327 3.4353 0.842913C3.62615 0.79269 3.93754 0.767578 4.36945 0.767578H4.79133V1.95034H4.55779C4.03547 1.95034 3.68893 2.04576 3.51817 2.23661C3.34741 2.42243 3.26203 2.8418 3.26203 3.4947C3.26203 4.81055 3.23441 5.64174 3.17916 5.98828C3.08876 6.52567 2.93307 6.94001 2.71209 7.23131C2.49613 7.5226 2.15461 7.78125 1.68753 8.00726C2.23999 8.23828 2.63927 8.59236 2.88536 9.06948C3.13647 9.54157 3.26203 10.3175 3.26203 11.3973C3.26203 12.3767 3.27208 12.9593 3.29217 13.1451C3.33235 13.4866 3.43279 13.7252 3.59351 13.8608C3.75924 13.9964 4.08067 14.0642 4.55779 14.0642H4.79133V15.2469H4.36945C3.87727 15.2469 3.52068 15.2068 3.2997 15.1264C2.97827 15.0109 2.71209 14.8225 2.50115 14.5614C2.29021 14.3052 2.1521 13.9788 2.08681 13.582C2.02654 13.1853 1.9939 12.5349 1.98887 11.6309C1.98385 10.7268 1.94116 10.1016 1.86081 9.75502C1.78045 9.40848 1.61722 9.13477 1.37113 8.93387C1.13006 8.73298 0.816162 8.62751 0.429443 8.61747V7.38951Z" fill="#595959"/> <path d="M17.3618 8.61049C16.9751 8.62054 16.6612 8.72349 16.4201 8.91936C16.174 9.12026 16.0108 9.39397 15.9305 9.74051C15.8501 10.0871 15.8074 10.6797 15.8024 11.5184C15.7974 12.3571 15.7823 12.9096 15.7572 13.1758C15.712 13.5977 15.6291 13.9367 15.5086 14.1928C15.383 14.4489 15.2298 14.6523 15.049 14.803C14.8682 14.9587 14.6372 15.0767 14.356 15.1571C14.1651 15.2073 13.8537 15.2324 13.4218 15.2324L12.9999 15.2324L12.9999 14.0497L13.2335 14.0497C13.7558 14.0497 14.1023 13.9542 14.2731 13.7634C14.4438 13.5776 14.5292 13.1582 14.5292 12.5053C14.5292 11.1895 14.5569 10.3583 14.6121 10.0117C14.7025 9.47433 14.8582 9.05999 15.0792 8.76869C15.2951 8.4774 15.6366 8.21875 16.1037 7.99274C15.5513 7.76172 15.152 7.40764 14.9059 6.93052C14.6548 6.45843 14.5292 5.68248 14.5292 4.60268C14.5292 3.62332 14.5192 3.04074 14.4991 2.85491C14.4589 2.51339 14.3585 2.27483 14.1978 2.13923C14.032 2.00363 13.7106 1.93582 13.2335 1.93582L12.9999 1.93582L12.9999 0.753068L13.4218 0.753068C13.914 0.753068 14.2706 0.793245 14.4916 0.873603C14.813 0.989116 15.0792 1.17745 15.2901 1.43861C15.501 1.69475 15.6392 2.0212 15.7045 2.41797C15.7647 2.81473 15.7974 3.46512 15.8024 4.36914C15.8074 5.27316 15.8501 5.89844 15.9305 6.24498C16.0108 6.59152 16.174 6.86523 16.4201 7.06613C16.6612 7.26702 16.9751 7.37249 17.3618 7.38253L17.3618 8.61049Z" fill="#595959"/> </svg>`, }, ]; icons.map((icon) => editor.ui.registry.addIcon(icon.name, `<span class="tox-custom-icon-wrapp"> ${icon.svg} </span>`) ); }, /** * Save the TinyMCE editor's contents to the model * @private */ _saveEditor: function(force){ var save = force | this._isDirty; if(save){ this.model.set(this.name, this.getEditorContent(), {silent: true}); this._isDirty = false; } }, /** * Finds textarea or iframe element in the field template * * @return {HTMLElement} element from field template * @private */ _getHtmlEditableField: function() { return this.$el.find(this.fieldSelector); }, /** * Sets TinyMCE editor content * * @param {String} value HTML content to place into HTML editor body */ setEditorContent: function(value) { if(_.isEmpty(value)){ value = ""; } if (this._isEditView() && this._htmleditor && this._htmleditor.dom) { this._htmleditor.setContent(value); } }, /** * Retrieves the TinyMCE editor content * * @return {String} content from the editor */ getEditorContent: function() { return this._htmleditor.getContent({format: 'raw'}); }, /** * Destroy TinyMCE Editor on dispose * * @private */ _dispose: function() { this.destroyTinyMCEEditor(); app.view.Field.prototype._dispose.call(this); }, /** * When in edit mode, the field includes an icon button for opening an address book. Clicking the button will * trigger an event to open the address book, which calls this method to do the dirty work. The selected recipients * are added to this field upon closing the address book. * * @private */ _showVariablesBook: function() { /** * Callback to add recipients, from a closing drawer, to the target Recipients field. * @param {undefined|Backbone.Collection} recipients */ var addVariables = _.bind(function(variables) { if (variables && variables.length > 0) { this.model.set(this.name, this.buildVariablesString(variables)); } }, this); app.drawer.open( { layout: "compose-varbook", context: { module: "pmse_Emails_Templates", mixed: true } }, function(variables) { addVariables(variables); } ); }, /** * Adds placeholders fields the textbox content. * * @param {Object} recipients List of fields to create the placeholders. * @return {string} textbox content with the placeholders. */ buildVariablesString: function(recipients) { var newExpression = this.buildPlaceholders(recipients); var bm = this._htmleditor.selection.getBookmark(); this._htmleditor.selection.moveToBookmark(bm); this._htmleditor.selection.setContent(newExpression); return this._htmleditor.getContent(); }, /** * Creates the placeholders for Email Template Modules. * * @param {Object} recipients List of fields to create the placeholders. * @return {string} newExpression. */ buildPlaceholders: function(recipients) { var newExpression = ''; _.each(recipients, function(model) { newExpression += '{::' + model.get('rhs_module') + '::' + model.get('id'); if (model.get('process_et_field_type') == 'old') { newExpression += '::' + model.get('process_et_field_type'); } newExpression += '::}'; }); return newExpression; }, /** * Open a drawer with a list of related fields that we want to link to in an email * Create a variable like {::href_link::Accounts::contacts::name::} which is understood * by the backend to replace the variable with the correct Sugar link * * @private */ _showLinksDrawer: function() { var self = this; var baseModule = this.model.get('base_module'); app.drawer.open({ layout: "compose-sugarlinks", context: { module: "pmse_Emails_Templates", mixed: true, skipFetch: true, baseModule: baseModule } }, function(field) { if (_.isUndefined(field)) { return; } var link = '{::href_link::' + baseModule; //Target module doesn't need second part of variable //The second part is for related modules //Example {::href_link::Accounts::name::}} is for the target module Account's record //{{::href_link::Accounts::contacts::name::}} is for the related contacts's record if (baseModule !== field.get('value')) { link += '::' + field.get('value'); } link += '::name::}'; self._htmleditor.selection.setContent(link); self.model.set(self.name, self._htmleditor.getContent()) } ); } }) } }} , "views": { "base": { "dashlet-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-email View (base) extendsFrom: 'TabbedDashletView', /** * @inheritdoc * * @property {Number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '10'. * @property {String} _defaultSettings.visibility Records visibility * regarding current user, supported values are 'user' and 'group', * defaults to 'user'. */ _defaultSettings: { limit: 10, visibility: 'user' }, thresholdRelativeTime: 2, //Show relative time for 2 days and then date time after /** * @inheritdoc */ initialize: function(options) { options.meta = options.meta || {}; options.meta.template = 'tabbed-dashlet'; this.plugins = _.union(this.plugins, [ 'LinkedModel' ]); this._super('initialize', [options]); }, /** * @inheritdoc */ _initEvents: function() { this._super('_initEvents'); this.on('dashlet-email:edit:fire', this.editRecord, this); this.on('dashlet-email:delete-record:fire', this.deleteRecord, this); this.on('dashlet-email:enable-record:fire', this.enableRecord, this); this.on('dashlet-email:download:fire', this.warnExportEmailsTemplates, this); this.on('dashlet-email:description-record:fire', this.descriptionRecord, this); this.on('linked-model:create', this.loadData, this); return this; }, /** * Re-fetches the data for the context's collection. * * FIXME: This will be removed when SC-4775 is implemented. * * @private */ _reloadData: function() { this.context.set('skipFetch', false); this.context.reloadData(); }, /** * Fire dessigner */ editRecord: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToEdit = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onEditRecordVerify, this) }); }, /** * Callback after checking if the template to be edited is already in use. * * @param {boolean} data: True if the template is being used (e.g. in a process), false otherwise. * * @private */ _onEditRecordVerify: function(data) { var model = this._modelToEdit; var redirect = model.module + '/' + model.id + '/layout/emailtemplates'; if (!data) { app.router.navigate(redirect, {trigger: true, replace: true}); } else { app.alert.show('email-templates-edit-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_EDIT', model.module), onConfirm: _.bind(this._onWarnEditActiveRecordConfirm, this, redirect), onCancel: _.bind(this._onWarnEditActiveRecordCancel, this) }); } }, /** * onConfirm callback for edit record warning. * @param {string} redirect: The redirect location made in the _onEditRecordVerify call that lead to this. * * @private */ _onWarnEditActiveRecordConfirm: function(redirect) { app.router.navigate(redirect, {trigger: true, replace: true}); this._modelToEdit = null; }, /** * onCancel callback for edit record warning. * * @private */ _onWarnEditActiveRecordCancel: function() { this._modelToEdit = null; }, /** * Show warning of pmse_email_templates */ warnExportEmailsTemplates: function(model) { var that = this; if (app.cache.get('show_emailtpl_export_warning')) { app.alert.show('emailtpl-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + '<br/><br/>' + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), //model is passed to _.bind to pass it as a parameter to _onWarnExportEmailsTemplatesConfirm onConfirm: _.bind(this._onWarnExportEmailsTemplatesConfirm, this, model), onCancel: $.noop }); } else { that.exportEmailsTemplates(model); } }, /** * onConfirm callback for warnExportEmailsTemplates call. * Set the cache so the warning isn't sent again and start the download. * * @param {Object} model: The model passed to the warnExportsEmailsTemplates call * * @private */ _onWarnExportEmailsTemplatesConfirm: function(model) { app.cache.set('show_emailtpl_export_warning', false); this.exportEmailsTemplates(model); }, /** * Download record of table pmse_emails_templates */ exportEmailsTemplates: function(model) { var url = app.api.buildURL(model.module, 'etemplate', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Email Template download uri.'); return; } app.api.fileDownload(url, { error: this._onExportEmailsTemplatesDownloadError }, {iframe: this.$el}); }, /** * error callback for exportEmailsTemplates fileDownload call. * @param {Object} data: The data from the api call * * @private */ _onExportEmailsTemplatesDownloadError: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); }, /** * @inheritdoc * * FIXME: This should be removed when metadata supports date operators to * allow one to define relative dates for date filters. */ _initTabs: function() { this._super('_initTabs'); }, /** * Create new record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ createRecord: function(event, params) { if (this.module !== 'pmse_Emails_Templates') { this.createRelatedRecord(params.module, params.link); } else { var self = this; app.drawer.open({ layout: 'create', context: { create: true, module: params.module } }, _.bind(self._onCreateRecordDrawerClose, self)); } }, /** * Callback used by the createRecord call to app.drawer.open * * @param {Object} context: Something that app.drawer.open calls this with * @param {Object} model: Model of the created record. Will be falsy if user cancels. * * @private */ _onCreateRecordDrawerClose: function(context, model) { if (!model) { return; } this.context.resetLoadFlag(); this.context.set('skipFetch', false); if (_.isFunction(this.loadData)) { this.loadData(); } else { this.context.loadData(); } }, importRecord: function(event, params) { app.router.navigate(params.link, {trigger: true, replace: true}); }, /** * Delete record. * * @param {Event} event Click event. * @param {String} params.layout Layout name. * @param {String} params.module Module name. */ deleteRecord: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onDeleteRecordVerify, this) }); }, /** * Callback after api call to verify whether the email template is active in a process. * @param {boolean} data: true if the email template is being used (e.g. in a process), false otherwise. * @private */ _onDeleteRecordVerify: function(data) { var model = this._modelToDelete; if (!data) { // Is NOT actively in use. app.alert.show('delete_confirmation', { level: 'confirmation', messages: app.utils.formatString(app.lang.get('LBL_PRO_DELETE_CONFIRMATION', model.module)), onConfirm: _.bind(this._onWarnDeleteInactiveRecordConfirm, this), onCancel: _.bind(this._onWarnDeleteInactiveRecordCancel, this) }); } else { // Is actively in use, do not allow deletion. app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_DELETE', model.module), autoClose: false }); this._modelToDelete = null; } }, /** * onConfirm callback for delete record warning. * Called by _onDeleteRecordVerify if the template is not active. * * @private */ _onWarnDeleteInactiveRecordConfirm: function() { var model = this._modelToDelete; model.destroy({ showAlerts: true, success: _.bind(this._getRemoveRecord, this) }); }, /** * onCancel callback for delete record warning. * Called by _onDeleteRecordVerify if the template is not active. * * @private */ _onWarnDeleteInactiveRecordCancel: function() { this._modelToDelete = null; }, /** * Updating in fields delete removed * @private */ _getRemoveRecord: function(model) { if (this.disposed) { return; } this.collection.remove(model); this.render(); this.context.trigger('tabbed-dashlet:refresh', model.module); }, /** * Method view alert in process with text modify * show and hide alert */ _refresh: function(model, status) { app.alert.show(model.id + ':refresh', { level: 'process', title: status, autoclose: false }); return _.bind(this._refreshReturn, this); }, /** * Function that _refresh returns * * @param {Object} model: The model passed to _refresh * @private */ _refreshReturn: function(model) { var options = {}; this.layout.reloadDashlet(options); app.alert.dismiss(model.id + ':refresh'); }, /** * descriptionRecord: View description in table pmse_Emails_Templates in fields */ descriptionRecord: function(model) { app.alert.dismiss('message-id'); app.alert.show('message-id', { level: 'info', title: app.lang.get('LBL_DESCRIPTION'), messages: '<br/>' + Handlebars.Utils.escapeExpression(model.get('description')), autoClose: false }); }, /** * Sets property useRelativeTime to show date created as a relative time or as date time. * * @private */ _setRelativeTimeAvailable: function(date) { var diffInDays = app.date().diff(date, 'days', true); var useRelativeTime = (diffInDays <= this.thresholdRelativeTime); return useRelativeTime; }, /** * @inheritdoc * * New model related properties are injected into each model: * * - {Boolean} overdue True if record is prior to now. * - {String} picture_url Picture url for model's assigned user. * - {String} base_module_name Name of the triggering module. */ _renderHtml: function() { if (this.meta.config) { this._super('_renderHtml'); return; } // Render each of the templates. _.each(this.collection.models, this._renderItemHtml, this); this._super('_renderHtml'); }, /** * Render an individual process emails template in the dashlet. Used by _renderHtml. * * @param {Object} model: The model object of the process emails template. * @private */ _renderItemHtml: function(model) { model.useRelativeTime = this._setRelativeTimeAvailable(model.attributes.date_entered); // Update the triggering module names. var module = model.get('base_module'); var label = app.lang.getModString('LBL_MODULE_NAME', module); if (_.isUndefined(label)) { label = module; } model.set('base_module_name', label); } }) }, "emailtemplates": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Emailtemplates View (base) className: 'emailtemplates', loadData: function () { this.et_uid = this.options.context.attributes.modelId; var self = this; App.api.call("read", App.api.buildURL("pmse_Emails_Templates/getFields", null, {id: this.et_uid }), {}, { success: function (a) { self.et_uid = self.options.context.attributes.modelId; self.body = a.body; self.bodyHtml = a.body_html; self.templateName = a.name; self.templateDescription = a.description; self.fromName = a.from_name; self.fromAddres = a.from_address; self.subject = a.subject; self.targetFields = a.fields; self.relatedModules = a.related_modules; self.targetModule = a.base_module; self.render(); $(init(self)); } }); } }) }, "compose-varbook-list-bottom": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-varbook-list-bottom View (base) extendsFrom: "ListBottomView", /** * Assign proper label for 'show more' link. * Label should be "More recipients...". */ setShowMoreLabel: function() { this.showMoreLabel = app.lang.get('LBL_PMSE_SHOW_MORE_VARIABLES', this.module); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * Override the edit event name so that double click to edit works on list views of this module */ editEventName: 'list:edit_emailstemplates:fire', /** * @override * @param {Object} options */ initialize: function(options) { this.contextEvents = _.extend({}, this.contextEvents, { 'list:editemailstemplates:fire': 'openEmailsTemplates', 'list:exportemailstemplates:fire': 'warnExportEmailsTemplates', 'list:deleteemailstemplates:fire': 'warnDeleteEmailsTemplates', 'list:edit_emailstemplates:fire': 'warnEditEmailsTemplates' }); this._super('initialize', [options]); }, openEmailsTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToDesign = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onDesignRecordVerify, this) }); }, /** * Callback after checking if the template to be designed is already in use. * * @param {boolean} data: True if the template is being used (e.g. in a process), false otherwise. * * @private */ _onDesignRecordVerify: function(data) { var model = this._modelToDesign; if (!data) { app.navigate(this.context, model, 'layout/emailtemplates'); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_EDIT', model.module), onConfirm: _.bind(this._onWarnDesignActiveRecordConfirm, this, model), onCancel: $.noop }); } }, /** * onConfirm callback for design record warning. * @param {Object} model: The model of the template to be designed. * * @private */ _onWarnDesignActiveRecordConfirm: function(model) { app.navigate(this.context, model, 'layout/emailtemplates'); }, warnEditEmailsTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToEdit = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onEditRecordVerify, this) }); }, /** * Callback after checking if the template to be edited is already in use. * * @param {boolean} data: True if the template is being used (e.g. in a process), false otherwise. * * @private */ _onEditRecordVerify: function(data) { var model = this._modelToEdit; if (!data) { // this.toggleRow(model.id, true); this.resize(); } else { app.alert.show('business-rule-design-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_EDIT', model.module), onConfirm: _.bind(this._onWarnEditActiveRecordConfirm, this), onCancel: $.noop }); } }, /** * onConfirm callback for edit record warning. * * @private */ _onWarnEditActiveRecordConfirm: function() { var model = this._modelToEdit; this.toggleRow(model.id, true); this.resize(); this._modelToEdit = null; }, warnDeleteEmailsTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onDeleteRecordVerify, this) }); }, /** * Callback for api call to verify whether the email template is active in a process. * @param {boolean} data: true if the email template is being used (e.g. in a process), false otherwise. * @private */ _onDeleteRecordVerify: function(data) { if (!data) { this._warnDeleteInactiveRecord(); } else { this._blockDeleteActiveRecord(); } }, /** * Get the user's confirmation before deleting the record. * Separated to reduce complexity of function for testing. * * @private */ _warnDeleteInactiveRecord: function() { var model = this._modelToDelete; this._targetUrl = Backbone.history.getFragment(); //Replace the url hash back to the current staying page if (this._targetUrl !== this._currentUrl) { app.router.navigate(this._currentUrl, {trigger: false, replace: true}); } app.alert.show('delete_confirmation', { level: 'confirmation', messages: this.getDeleteMessages(model).confirmation, onConfirm: _.bind(this._onWarnDeleteInactiveRecordConfirm, this), onCancel: _.bind(this._clearModelToDelete, this) }); }, /** * Prevent the user from deleting an email template that is in use. * Separated to reduce complexity of function for testing. * * @private */ _blockDeleteActiveRecord: function() { var model = this._modelToDelete; app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_DELETE', model.module), autoClose: false }); this._clearModelToDelete(); }, /** * onConfirm callback for delete record warning. * * @private */ _onWarnDeleteInactiveRecordConfirm: function() { this.deleteModel(); }, /** * onCancel callback for delete record warning. * * @private */ _clearModelToDelete: function() { this._modelToDelete = null; }, warnExportEmailsTemplates: function(model) { var that = this; if (app.cache.get('show_emailtpl_export_warning')) { app.alert.show('emailtpl-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + '<br/><br/>' + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: _.bind(that._onWarnExportEmailsTemplatesConfirm, that, model), onCancel: $.noop }); } else { that.exportEmailsTemplates(model); } }, /** * onConfirm callback for warnExportEmailsTemplates call. * Set the cache so the warning isn't sent again and start the download. * * @param {Object} model: The model passed to the warnExportsEmailTemplates call * * @private */ _onWarnExportEmailsTemplatesConfirm: function(model) { app.cache.set('show_emailtpl_export_warning', false); this.exportEmailsTemplates(model); }, exportEmailsTemplates: function(model) { var url = app.api.buildURL(model.module, 'etemplate', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: this._onExportEmailsTemplatesDownloadError }, {iframe: this.$el}); }, /** * error callback for exportEmailsTemplates fileDownload call. * @param {Object} data: The data from the api call * * @private */ _onExportEmailsTemplatesDownloadError: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }) }, "compose-sugarlinks-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.ComposeSugarLinksListView * @alias SUGAR.App.view.views.ComposeSugarlinksListView * @extends View.FlexListView */ ({ // Compose-sugarlinks-list View (base) extendsFrom: 'FlexListView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); }, /** * @inheritdoc * Load data from api endpoint */ loadData: function() { var self = this; var baseModule = this.context.get('baseModule'); var url = app.api.buildURL('pmse_Emails_Templates', baseModule + '/find_modules', null, {module_list: baseModule}); app.alert.show('sugar_link_load', {level: 'process'}) app.api.call('GET', url, null, { success: function(data) { var processedData = self._processResults(data.result); if (self.collection) { self.collection.add(processedData); self.collection.dataFetched = true; self.render(); } app.alert.dismiss('sugar_link_load', {level: 'process'}); }, error: function(e) { app.alert.dismiss('sugar_link_load', {level: 'process'}); } }); }, /** * Sanitize the results by cleaning up names and adding how module is related * to the target module * @param results * @returns {*} array of target module and related modules * @private */ _processResults: function(results) { var targetModule = _.first(results); var relatedModules = _.rest(results, 1) var currentActivity = this._getCurrentActivity(); //strip off '<' and '>' from target module's name targetModule.text = targetModule.text.substring(1, targetModule.text.length-1); targetModule.relatedTo = app.lang.get('LBL_BASE_MODULE', 'pmse_Emails_Templates'); _.map(relatedModules, function(relatedModule){ return _.extend(relatedModule, {relatedTo: app.lang.get('LBL_RELATED_TO_TARGET_MODULE', 'pmse_Emails_Templates')}) }); relatedModules.unshift(targetModule); relatedModules.push(currentActivity); return relatedModules; }, _getCurrentActivity: function() { var processModule = 'pmse_Inbox'; var moduleLabel = app.lang.getModuleName(processModule, {plural: true}); return { module: processModule, module_label: moduleLabel, text: moduleLabel, relatedTo: app.lang.get('LBL_PMSE_CURRENT_ACTIVITY', processModule), relationship: 'current_activity', value: 'current_activity' }; } }) }, "compose-sugarlinks-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-sugarlinks-headerpane View (base) extendsFrom: 'HeaderpaneView', events: { 'click [name=select_button]': '_select', 'click [name=cancel_button]': '_cancel' }, /** * Close the drawer and pass in the selected model * * @private */ _select: function() { var selectedModel = this.context.get('selection_model'); if (selectedModel) { app.drawer.close(selectedModel); } else { this._cancel(); } }, /** * Close the drawer * * @private */ _cancel: function() { app.drawer.close(); } }) }, "compose-varbook-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-varbook-list View (base) /** * @class View.ComposeAddressbookListView * @alias SUGAR.App.view.views.ComposeAddressbookListView * @extends View.FlexListView */ extendsFrom: 'FlexListView', plugins: ['ListColumnEllipsis', 'Pagination'], /** * Override to inject field names into the request when fetching data for the list. * * @param module * @returns {Array} */ getFieldNames: function(module) { // id and module always get returned, so name and email just need to be added return ['name', 'email']; }, /** * Override to force translation of the module names as columns are added to the list. * * @param field * @private */ _renderField: function(field) { if (field.name == 'process_et_field_type') { field.setViewName('edit'); field.action = 'edit'; } if (field.name == '_module') { field.model.set(field.name, app.lang.get('LBL_MODULE_NAME', field.module)); } this._super('_renderField', [field]); } }) }, "compose-varbook-filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-varbook-filter View (base) /** * @class View.ComposeAddressbookFilterView * @alias SUGAR.App.view.views.ComposeAddressbookFilterView * @extends View */ _moduleFilterList: [], _allModulesId: 'All', _selectedModule: null, _currentSearch: '', events: { 'keyup .search-name': 'throttledSearch', 'paste .search-name': 'throttledSearch', 'click .add-on.sicon-close': 'clearInput' }, /** * Converts the input field to a select2 field and adds the module filter for refining the search. * * @private */ _render: function() { app.view.View.prototype._render.call(this); this.buildModuleFilterList(); this.buildFilter(); }, /** * Builds the list of allowed modules to provide the data to the select2 field. */ buildModuleFilterList: function() { this._moduleFilterList = [ {id: this._allModulesId, text: app.lang.get('Target Module')} ]; url = app.api.buildURL('pmse_Emails_Templates', this.collection.baseModule + '/find_modules', null, {module_list: this.collection.baseModule}); app.api.call('read', url, null, { success: _.bind(this._onGetModuleFilterListSuccess, this) } ); }, /** * API success callback for buildModuleFilterList. Created to make unit testing possible. */ _onGetModuleFilterListSuccess: function(result) { if (result.success && this.collection) { _.each(result.result, function(module) { if (module.value != this.collection.baseModule) { this._moduleFilterList.push({id: module.value, text: module.text}); } }, this); } }, /** * Converts the input field to a select2 field and initializes the selected module. */ buildFilter: function() { var $filter = this.getFilterField(); if ($filter.length > 0) { $filter.select2({ data: this._moduleFilterList, allowClear: false, multiple: false, minimumResultsForSearch: -1, formatSelection: _.bind(this.formatModuleSelection, this), formatResult: _.bind(this.formatModuleChoice, this), dropdownCss: {width: 'auto'}, dropdownCssClass: 'search-filter-dropdown', initSelection: _.bind(this.initSelection, this), escapeMarkup: function(m) { return m; }, width: 'off' }); $filter.off('change'); $filter.on('change', _.bind(this.handleModuleSelection, this)); this._selectedModule = this._selectedModule || this._allModulesId; $filter.select2('val', this._selectedModule); } }, /** * Gets the filter DOM field. * * @returns {Object} DOM Element */ getFilterField: function() { return this.$('input.select2'); }, /** * Gets the module filter DOM field. * * @returns {Object} DOM Element */ getModuleFilter: function() { return this.$('span.choice-filter-label'); }, /** * Destroy the select2 plugin. */ unbind: function() { $filter = this.getFilterField(); if ($filter.length > 0) { $filter.off(); $filter.select2('destroy'); } this._super('unbind'); }, /** * Performs a search once the user has entered a term. */ throttledSearch: _.debounce(function(evt) { var newSearch = this.$(evt.currentTarget).val(); if (this._currentSearch !== newSearch) { this._currentSearch = newSearch; this.applyFilter(); } }, 400), /** * Initialize the module selection with the value for all modules. * * @param el * @param callback */ initSelection: function(el, callback) { if (el.is(this.getFilterField())) { var module = _.findWhere(this._moduleFilterList, {id: el.val()}); callback({id: module.id, text: module.text}); } }, /** * Format the selected module to display its name. * * @param {Object} item * @return {String} */ formatModuleSelection: function(item) { // update the text for the selected module this.getModuleFilter().text(item.text); return '<span class="select2-choice-type">' + app.lang.get('LBL_MODULE') + '<i class="sicon sicon-chevron-down"></i></span>'; }, /** * Format the choices in the module select box. * * @param {Object} option * @return {String} */ formatModuleChoice: function(option) { return '<div><span class="select2-match"></span>' + option.text + '</div>'; }, /** * Handler for when the module filter dropdown value changes, either via a click or manually calling jQuery's * .trigger("change") event. * * @param {Object} evt jQuery Change Event Object * @param {string} overrideVal (optional) ID passed in when manually changing the filter dropdown value */ handleModuleSelection: function(evt, overrideVal) { var module = overrideVal || evt.val || this._selectedModule || this._allModulesId; // only perform a search if the module is in the approved list if (!_.isEmpty(_.findWhere(this._moduleFilterList, {id: module}))) { this._selectedModule = module; this.getFilterField().select2('val', this._selectedModule); this.getModuleFilter().css('cursor', 'pointer'); this.applyFilter(); } }, /** * Triggers an event that makes a call to search the address book and filter the data set. */ applyFilter: function() { var searchAllModules = (this._selectedModule === this._allModulesId), // pass an empty array when all modules are being searched module = searchAllModules ? [] : [this._selectedModule], // determine if the filter is dirty so the "clearQuickSearchIcon" can be added/removed appropriately isDirty = !_.isEmpty(this._currentSearch); this._toggleClearQuickSearchIcon(isDirty); this.context.trigger('compose:addressbook:search', module, this._currentSearch); }, /** * Append or remove an icon to the quicksearch input so the user can clear the search easily. * @param {Boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.add-on.sicon-close')[0]) { this.$('.filter-view.search').append('<i class="add-on sicon sicon-close"></i>'); } else if (!addIt) { this.$('.add-on.sicon-close').remove(); } }, /** * Clear input */ clearInput: function() { var $filter = this.getFilterField(); this._currentSearch = ''; this._selectedModule = this._allModulesId; this.$('.search-name').val(this._currentSearch); if ($filter.length > 0) { $filter.select2('val', this._selectedModule); } this.applyFilter(); } }) }, "compose-varbook-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-varbook-headerpane View (base) extendsFrom: "HeaderpaneView", events: { "click [name=done_button]": "_done", "click [name=cancel_button]": "_cancel" }, /** * The user clicked the Done button so trigger an event to add selected recipients from the address book to the * target field and then close the drawer. * * @private */ _done: function() { var selectedList = this.selectList(this.collection.models); !_.isEmpty(selectedList) ? app.drawer.close(selectedList) : this._cancel(); }, /** * Close the drawer. * * @private */ _cancel: function() { app.drawer.close(); }, /** * Creates and returns a list of all the fields the User selected for either * Current, Old or Both values. * If the value is Both there will be 2 models with the values Current and Old * for the same field. * Current translates to future on the backend. * * @param {Object} models List of all the modules. * @return {Object} selectedList. */ selectList: function(models) { var selectedList = []; var i; var old; var future; for (i = 0 ; i < models.length; i++) { if (models[i].attributes.process_et_field_type === 'none') { continue; } if (models[i].attributes.process_et_field_type == 'both') { // Get clones of the model for old and new future = models[i].clone(); old = models[i].clone(); // Set the field type for the current field future.attributes.process_et_field_type = 'future'; // Set the field type for the old field old.attributes.process_et_field_type = 'old'; // Add them to the stack selectedList.push(future); selectedList.push(old); } else { // Since this is one or the other, take it as is selectedList.push(models[i]); } } return selectedList; } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', initialize: function(options) { this._super('initialize', [options]); this.context.on('button:design_emailtemplates:click', this.designEmailTemplates, this); this.context.on('button:export_emailtemplates:click', this.warnExportEmailTemplates, this); this.context.on('button:delete_emailstemplates:click', this.warnDeleteEmailsTemplates, this); this.context.on('button:edit_emailstemplates:click', this.warnEditEmailTemplates, this); }, _render: function() { this._super('_render'); this.$('.record-cell[data-name=subject]').remove(); this.$('.record-cell[data-name=body_html]').remove(); }, designEmailTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToDesign = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onDesignRecordVerify, this) }); }, /** * Callback after checking if the template to be designed is already in use. * * @param {boolean} data: True if the template is being used (e.g. in a process), false otherwise. * * @private */ _onDesignRecordVerify: function(data) { var model = this._modelToDesign; if (!data) { app.navigate(this.context, model, 'layout/emailtemplates'); } else { app.alert.show('email-templates-edit-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_EDIT', model.module), onConfirm: _.bind(this._onWarnDesignActiveRecordConfirm, this, model), onCancel: $.noop }); } }, /** * onConfirm callback for design record warning. * * @private */ _onWarnDesignActiveRecordConfirm: function(model) { app.navigate(this.context, model, 'layout/emailtemplates'); this._modelToDesign = null; }, warnEditEmailTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToEdit = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onEditRecordVerify, this) }); }, /** * Callback after checking if the template to be edited is already in use. * * @param {boolean} data: True if the template is being used (e.g. in a process), false otherwise. * * @private */ _onEditRecordVerify: function(data) { var model = this._modelToEdit; if (!data) { // Not in use, continue with edit. this.editClicked(); } else { // Template in use, warn user. app.alert.show('email-templates-edit-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_EDIT', model.module), onConfirm: _.bind(this._onWarnEditActiveRecordConfirm, this), onCancel: $.noop }); } }, /** * onConfirm callback for edit record warning. * * @private */ _onWarnEditActiveRecordConfirm: function() { this.editClicked(); this._modelToEdit = null; }, handleEdit: function(e, cell) { this.warnEditEmailTemplates(this.model); }, warnDeleteEmailsTemplates: function(model) { var verifyURL = app.api.buildURL( 'pmse_Project', 'verify', {id: model.get('id')}, {baseModule: this.module}); this._modelToDelete = model; app.api.call('read', verifyURL, null, { success: _.bind(this._onDeleteRecordVerify, this) }); }, /** * Callback for api call to verify whether the email template is active in a process. * @param {boolean} data: true if the email template is being used (e.g. in a process), false otherwise. * @private */ _onDeleteRecordVerify: function(data) { var model = this._modelToDelete; if (!data) { // Template not in use, warn user. app.alert.show('delete_confirmation', { level: 'confirmation', messages: this.getDeleteMessages(model).confirmation, onConfirm: _.bind(this._onWarnDeleteInactiveRecordConfirm, this), onCancel: _.bind(this._clearModelToDelete, this) }); } else { // Template in use, block deletion app.alert.show('message-id', { level: 'warning', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_PMSE_PROCESS_EMAIL_TEMPLATES_DELETE', model.module), autoClose: false }); this._clearModelToDelete(); } }, /** * onConfirm callback for delete record warning. * * @private */ _onWarnDeleteInactiveRecordConfirm: function() { this.deleteModel(); }, /** * Unset _modelToDelete as it is used by the parent record.js file. * * @private */ _clearModelToDelete: function() { this._modelToDelete = null; }, warnExportEmailTemplates: function(model) { var that = this; if (app.cache.get('show_emailtpl_export_warning')) { app.alert.show('emailtpl-export-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + '<br/><br/>' + app.lang.get('LBL_PMSE_EXPORT_CONFIRMATION'), onConfirm: _.bind(that._onWarnExportEmailTemplatesConfirm, that, model), onCancel: $.noop }); } else { that.exportEmailTemplates(model); } }, /** * onConfirm callback for warnExportEmailTemplates call. * Set the cache so the warning isn't sent again and start the download. * * @param {Object} model: The model passed to the warnExportsEmailTemplates call * * @private */ _onWarnExportEmailTemplatesConfirm: function(model) { app.cache.set('show_emailtpl_export_warning', false); this.exportEmailTemplates(model); }, exportEmailTemplates: function(model) { var url = app.api.buildURL(model.module, 'etemplate', {id: model.id}, {platform: app.config.platform}); if (_.isEmpty(url)) { app.logger.error('Unable to get the Project download uri.'); return; } app.api.fileDownload(url, { error: this._onExportEmailTemplatesDownloadError }, {iframe: this.$el}); }, /** * error callback for exportEmailTemplates fileDownload call. * @param {Object} data: The data from the api call * * @private */ _onExportEmailTemplatesDownloadError: function(data) { // refresh token if it has expired app.error.handleHttpError(data, {}); } }) }, "compose": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Emails_Templates.ComposeView * @alias SUGAR.App.view.views.Basepmse_Emails_TemplatesComposeView * @extends View.Views.Base.RecordView */ ({ // Compose View (base) extendsFrom: 'RecordView', MIN_EDITOR_HEIGHT: 300, EDITOR_RESIZE_PADDING: 5, buttons: null, initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [name=save_button]': 'save', 'click [name=save_buttonExit]': 'saveExit', 'click [name=cancel_button]': 'cancel' }); this.context.on('tinymce:oninit', this.handleTinyMceInit, this); this.action = 'edit'; this._lastSelectedSignature = app.user.getPreference("signature_default"); }, /** * Set the title to the module name * @private */ _render: function () { this._super('_render'); this.setTitle(app.lang.get('LBL_MODULE_NAME', this.module)); this.toggleViewButtons(true); }, /** * Cancel and close the drawer */ cancel: function() { this.toggleEdit(false); this.inlineEditMode = false; App.router.navigate("pmse_Emails_Templates", {trigger: true}); }, /** * This is kept very simple because we always stay in detail mode * * @override */ handleSave: function() { if (this.disposed) { return; } this._saveModel(); }, /** * Send the email immediately or warn if user did not provide subject or body */ save: function () { this.model.doValidate(this.getFields(this.module), _.bind(this.validationComplete, this)); }, validationCompleteApprove: function (model,exit) { var url, attributes, bodyHtml, subject, route = this.context.get("module"); url = App.api.buildURL('pmse_Emails_Templates', null, {id: this.context.attributes.modelId}); bodyHtml = model.get('body_html');//bodyHtml = this.model.get('body_html'); subject = model.get('subject');//subject = this.model.get('subject'); attributes = { body_html: bodyHtml, subject: subject, description:model.get('description'),//description:this.model.get('description'), name: model.get('name')//name: this.model.get('name'), }; App.alert.show('upload', {level: 'process', title: 'LBL_SAVING', autoclose: false}); App.api.call('update', url, attributes, { success: function (data) { App.alert.dismiss('upload'); App.alert.show('save-success', { level: 'success', messages: App.lang.get('LBL_SAVED'), autoClose: true }); if(exit) { model.revertAttributes(); App.router.redirect(route); } }, error: function (err) { App.alert.dismiss('upload'); } }); }, saveExit: function() { this.model.doValidate(this.getFields(this.module), _.bind(function(isValid) { if (isValid) { this.validationCompleteApprove(this.model,true); } }, this)); }, /** * Email Templates Designer's URL should not change because it doesn't contain the action in it * @override */ setRoute: _.noop, _dispose: function() { if (App.drawer) { App.drawer.off(null, null, this); } this._super("_dispose"); }, handleTinyMceInit: function() { this.resizeEditor(); }, /** * Resize the html editor based on height of the drawer it is in * * @param drawerHeight current height of the drawer or height the drawer will be after animations */ resizeEditor: function(drawerHeight) { var $editor, headerHeight, recordHeight, showHideHeight, diffHeight, editorHeight, newEditorHeight; $editor = this.$('.mceLayout .mceIframeContainer iframe'); //if editor not already rendered, cannot resize if ($editor.length === 0) { return; } drawerHeight = drawerHeight || app.drawer.getHeight(); headerHeight = this.$('.headerpane').outerHeight(true); recordHeight = this.$('.record').outerHeight(true); showHideHeight = this.$('.show-hide-toggle').outerHeight(true); editorHeight = $editor.height(); //calculate the space left to fill - subtracting padding to prevent scrollbar diffHeight = drawerHeight - headerHeight - recordHeight - showHideHeight - this.EDITOR_RESIZE_PADDING; //add the space left to fill to the current height of the editor to get a new height newEditorHeight = editorHeight + diffHeight; //maintain min height if (newEditorHeight < this.MIN_EDITOR_HEIGHT) { newEditorHeight = this.MIN_EDITOR_HEIGHT; } //set the new height for the editor $editor.height(newEditorHeight); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.pmse_Emails_Templates.CreateView * @alias SUGAR.App.view.views.pmse_Emails_TemplatesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', saveOpenEmailsTemplatesName: 'save_open_emailstemplates', SAVEACTIONS: { SAVE_OPEN_EMAILS_TEMPLATES: 'saveOpenEmailsTemplates' }, initialize: function(options) { options.meta = _.extend({}, app.metadata.getView(null, 'create'), options.meta); this._super('initialize', [options]); this.context.on('button:' + this.saveOpenEmailsTemplatesName + ':click', this.saveOpenEmailsTemplates, this); }, save: function () { switch (this.context.lastSaveAction) { case this.SAVEACTIONS.SAVE_OPEN_EMAILS_TEMPLATES: this.saveOpenEmailsTemplates(); break; default: this.saveAndClose(); } }, saveOpenEmailsTemplates: function() { this.context.lastSaveAction = this.SAVEACTIONS.SAVE_OPEN_EMAILS_TEMPLATES; this.initiateSave(_.bind(function () { app.navigate(this.context, this.model, 'layout/emailtemplates'); }, this)); } }) }, "emailtemplates-import-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Emailtemplates-import-headerpane View (base) extendsFrom: 'HeaderpaneView', events:{ 'click [name=emailtemplates_finish_button]': 'initiateFinish', 'click [name=emailtemplates_cancel_button]': 'initiateCancel' }, initiateFinish: function() { this.context.trigger('emailtemplates:import:finish'); }, initiateCancel : function() { app.router.navigate(app.router.buildRoute(this.module), {trigger: true}); } }) }, "emailtemplates-import": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Emailtemplates-import View (base) initialize: function(options) { app.view.View.prototype.initialize.call(this, options); this.context.off('emailtemplates:import:finish', null, this); this.context.on('emailtemplates:import:finish', this.warnImportEmailTemplates, this); }, /** * @inheritdoc * * Sets up the file field to edit mode * * @param {View.Field} field * @private */ _renderField: function(field) { app.view.View.prototype._renderField.call(this, field); if (field.name === 'emailtemplates_import') { field.setMode('edit'); } }, warnImportEmailTemplates: function() { var that = this; if (app.cache.get('show_emailtpl_import_warning')) { app.alert.show('emailtpl-import-confirmation', { level: 'confirmation', messages: app.lang.get('LBL_PMSE_IMPORT_EXPORT_WARNING') + '<br/><br/>' + app.lang.get('LBL_PMSE_IMPORT_CONFIRMATION'), onConfirm: _.bind(that._onWarnImportEmailTemplatesConfirm, that), onCancel: _.bind(that._onWarnImportEmailTemplatesCancel, that) }); } else { that.importEmailTemplates(); } }, /** * onConfirm callback for warnImportEmailTemplates alert. * Set the cache so the warning isn't sent again and start the import. * * @private */ _onWarnImportEmailTemplatesConfirm: function() { app.cache.set('show_emailtpl_import_warning', false); this.importEmailTemplates(); }, /** * onCancel callback for warnImportEmailTemplates alert. * Navigate the user back to where they were before. * * @private */ _onWarnImportEmailTemplatesCancel: function() { app.router.goBack(); }, /** * Import the Email Templates file (.pet) */ importEmailTemplates: function() { var self = this, projectFile = $('[name=emailtemplates_import]'); // Check if a file was chosen if (_.isEmpty(projectFile.val())) { app.alert.show('error_validation_emailtemplates', { level: 'error', messages: app.lang.get('LBL_PMSE_EMAIL_TEMPLATES_EMPTY_WARNING', self.module), autoClose: false }); } else { app.alert.show('upload', {level: 'process', title: 'LBL_UPLOADING', autoclose: false}); var callbacks = { success: _.bind(self._onImportEmailTemplatesSuccess, self), error: self._onImportEmailTemplatesError }; this.model.uploadFile('emailtemplates_import', projectFile, callbacks, {deleteIfFails: true, htmlJsonFormat: true}); } }, /** * success callback for template import. * @param {Object} data: response data. * * @private */ _onImportEmailTemplatesSuccess: function(data) { app.alert.dismiss('upload'); app.router.goBack(); app.alert.show('process-import-saved', { level: 'success', messages: app.lang.get('LBL_PMSE_EMAIL_TEMPLATES_IMPORT_SUCCESS', this.module), autoClose: true }); }, /** * error callback for template import. * @param {Object} error: response data. * * @private */ _onImportEmailTemplatesError: function(error) { app.alert.dismiss('upload'); app.alert.show('process-import-saved', { level: 'error', messages: error.error_message, autoClose: false }); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "compose-varbook": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Compose-varbook Layout (base) /** * @class ComposeAddressbookLayout * @extends Layout */ initialize: function(options) { app.view.Layout.prototype.initialize.call(this, options); this.collection.sync = this.sync; this.collection.baseModule = this.layout.context.attributes.model.attributes.base_module; this.collection.allowed_modules = ['Accounts', 'Contacts', 'Leads', 'Prospects', 'Users']; // url = app.api.buildURL('pmse_Emails_Templates', 'modules/find', null, {'module': this.collection.baseModule}); // app.api.call('read', url, null, { // success:function (modules){ // self.collection.allowed_modules= modules; // // } // } // ); this.context.on('compose:addressbook:search', this.search, this); }, /** * Calls the custom Mail API endpoint to search for email addresses. * * @param method * @param model * @param options */ sync: function(method, model, options) { var callbacks, baseModule, url; options = options || {}; // only fetch from the approved modules if (_.isEmpty(options.module_list) || this.module_list.length > 1) { options.module_list = [this.baseModule]; } else { options.module_list = [this.module_list[0]]; } // this is a hack to make pagination work while trying to minimize the affect on existing configurations // there is a bug that needs to be fixed before the correct approach (config.maxQueryResult vs. options.limit) // can be determined app.config.maxQueryResult = app.config.maxQueryResult || 20; options.limit = options.limit || app.config.maxQueryResult; options = app.data.parseOptionsForSync(method, model, options); callbacks = app.data.getSyncCallbacks(method, model, options); this.trigger('data:sync:start', method, model, options); _.extend(options.params, {base_module: model.baseModule}); url = app.api.buildURL('pmse_Emails_Templates', 'variables/find', null, options.params); app.api.call('read', url, null, callbacks); }, /** * Adds the set of modules and term that should be used to search for recipients. * * @param {Array} modules * @param {String} term */ search: function(modules, term) { // reset offset to 0 on a search. make sure that it resets and does not update. this.collection.fetch({query: term, module_list: modules, offset: 0, update: false}); } }) } }} , "datas": {} }, "BusinessCenters":{"fieldTemplates": { "base": { "business-day-status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * business-day-status is a field that stores whether a Business Center is open * or closed any particular day of the week. It distinguishes between three * possible statuses: "Open", "Closed", and "Open 24 Hours". The server * only recognizes Open (1) and Closed (0) - "Open 24 Hours" is handled * entirely client-side. When receiving a 1, it uses the values of * the opening and closing times received to determine * * @class View.Fields.Base.BusinessCenters.BusinessDayStatusField * @alias SUGAR.App.view.fields.BaseBusinessCentersBusinessDayStatusField * @extends View.Fields.Base.EnumField */ ({ // Business-day-status FieldTemplate (base) extendsFrom: 'EnumField', /** * Converts (client-side) model values to dropdown keys. * * @property {Object} */ _valueToStatus: { 0: 'Closed', 1: 'Open', 2: 'Open 24 Hours', }, /** * Defines the start and end times of the day. * * @property {Object} */ _dayStartEnd: { startHour: 0, startMinute: 0, endHour: 23, endMinute: 59 }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'enum'; this.def.options = 'business_day_status_dom'; // name should be of the format "is_open_<day>" this.day = this.name.slice(this.name.lastIndexOf('_') + 1); this._openClosedFields = this._getOpenClosedFields(); if (this.model && this.model.isNew()) { this.view.once('render', function() { this._handleModelChange(this.model, this.getValue()); }, this); } }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange', arguments); var changesToTrack = _.map(this._openClosedFields, function(field) { return 'change:' + field; }); this.model.on('sync ' + changesToTrack.join(' '), function() { if (this.isOpenAllDay()) { this.model.set(this.name, 2); this.render(); } }, this); var clearTimeDisplays = _.bind(function() { this._handleModelChange(); }, this); this.model.on('change:' + this.name, clearTimeDisplays, this); this.model.once('sync', clearTimeDisplays, this); this.model.on('attributes:revert', clearTimeDisplays, this); }, /** * Update the open/close hours as appropriate. * * @private */ _handleModelChange: function() { if (this.disposed) { return; } var options = {}; var value = this.getValue(); if (this.isOpenAllDayValue(value)) { // set the open/close hours to all day as necessary options[this._openClosedFields[0]] = this._dayStartEnd.startHour; options[this._openClosedFields[1]] = this._dayStartEnd.startMinute; options[this._openClosedFields[2]] = this._dayStartEnd.endHour; options[this._openClosedFields[3]] = this._dayStartEnd.endMinute; this.model.set(options); this._hideTimeselectFields(); } else if (this.isClosedValue(value)) { // note that since you can't update to null, we have to make do with all zeroes options[this._openClosedFields[0]] = 0; options[this._openClosedFields[1]] = 0; options[this._openClosedFields[2]] = 0; options[this._openClosedFields[3]] = 0; this.model.set(options); this._hideTimeselectFields(); } else { this._showTimeselectFields(); } }, /** * Format the given value as a dropdown key for business day statuses. * * @param {*} value Value to format. * @return {string} The dom key for business_day_status_dom. */ format: function(value) { // the server will send true or false initially, but client-side it's always stored as 0, 1, or 2. // false always means "Closed", but true can mean either "Open" or "Open 24 Hours" - the values // of the opening and closing times sent down are used to disambiguate if (value === true) { value = 1; } else if (_.isNull(value) || _.isUndefined(value) || value === false) { value = 0; } return this._valueToStatus[value]; }, /** * Convert the dropdown key to a value we can store in the model. * Even though the is_open_<day> fields are booleans on the server, * here we store them as numbers to allow the client side to * distinguish between "open part of the day" and "open all day". * * @param {string} value Dropdown key. * @return {number} 0, 1, or 2, for use as lookup keys in _valueToStatus. */ unformat: function(value) { switch (value) { case 'Open': return 1; case 'Open 24 Hours': // note: the values are booleans on the server, so they will convert 2 to 1. return 2; case 'Closed': default: return 0; } }, /** * Get the current numeric status of this business day. * * @return {number} The numeric status (0, 1, or 2). */ getValue: function() { return this.unformat(this.format(this.model.get(this.name))); }, /** * Is this value the special "closed" value? * * @param {*} value The value to check. * @return {boolean} `true` if the given value denotes "closed" and * `false` otherwise. */ isClosedValue: function(value) { return value === 0; }, /** * Is this value the special "open" value? * * @param {*} value The value to check. * @return {boolean} `true` if the given value denotes "open" and * `false` otherwise. */ isOpenValue: function(value) { return value === 1; }, /** * Is this value the special "open all day" value? * * Note that "Open 24 Hours" will result in `false`. * * @param {*} value The value to check. * @return {boolean} `true` if the given value denotes "open all day" and * `false` otherwise. */ isOpenAllDayValue: function(value) { return value === 2; }, /** * Check if this field should be marked as "Open 24 Hours" based on the * open/close time fields of the model. * * @return {boolean} `true` if this field should be marked as open all day * based on the values of the other open/close time fields on the model. */ isOpenAllDay: function() { var openClosedValues = _.map( this._openClosedFields, function(field) { return parseInt(this.model.get(field), 10) || 0; }, this ); return openClosedValues[0] === this._dayStartEnd.startHour && openClosedValues[1] === this._dayStartEnd.startMinute && openClosedValues[2] === this._dayStartEnd.endHour && openClosedValues[3] === this._dayStartEnd.endMinute; }, /** * Get the opening and closing hour fields for this business day. * * @return {string[]} The list of names of open and close fields. * @private */ _getOpenClosedFields: function() { if (this._openClosedFields) { return this._openClosedFields; } var openHourField = this.day + '_open_hour'; var openMinuteField = this.day + '_open_minutes'; var closeHourField = this.day + '_close_hour'; var closeMinuteField = this.day + '_close_minutes'; return [openHourField, openMinuteField, closeHourField, closeMinuteField]; }, /** * This field always has a value * @override */ isFieldEmpty: function() { return false; }, /** * Hide the timeselect fields. * * @private */ _hideTimeselectFields: function() { this._showHideTimeselectFields(false); }, /** * Show the timeselect fields. * * @private */ _showTimeselectFields: function() { this._showHideTimeselectFields(true); }, /** * Show or hide the timeselect fields. * * @param {boolean} show If `true`, show. Hide otherwise. * @private */ _showHideTimeselectFields: function(show) { var openTimeselectField = this.day + '_open'; var closeTimeselectField = this.day + '_close'; _.each([openTimeselectField, closeTimeselectField], function(fieldName) { var field = this.view.getField(fieldName); show ? field.show() : field.hide(); }, this); } }) } }} , "views": { "base": { "dashablerecord": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.BusinessCenters.DashablerecordView * @alias SUGAR.App.view.views.BusinessCentersDashablerecordView * @extends View.Views.Base.DashablerecordView */ ({ // Dashablerecord View (base) /** * @inheritdoc */ hasUnsavedChanges: function() { const changedAttributes = this.model.changedAttributes(this.model.getSynced()); if (!changedAttributes) { return false; } let castModelData = {}; for (key in changedAttributes) { let modelValue = this.model.get(key); castModelData[key] = typeof changedAttributes[key] === 'boolean' ? Boolean(modelValue) : String(modelValue); } if (_.isEqual(changedAttributes, castModelData)) { return false; } return this._super('hasUnsavedChanges'); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', _days: [ 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.model && this.model.addValidationTask( 'all_open_hours_before_close_hours_' + this.cid, _.bind(this.validateBusinessHours, this) ); }, /** * Ensure that when business day status is set to Open for any day, * it has valid hours. * * NOTE: This function is also used by the BusinessCenters create view. * Any properties of `this` used in this function must be available * to the create view as well. * * @param {Object} fields List of fields that can be validated. * @param {Object} errors Existing validation errors. * @param {Function} callback To be called after completion. */ validateBusinessHours: function(fields, errors, callback) { var newErrors = {}; _.each(this._days, function(day) { var openName = day + '_open'; var closeName = day + '_close'; var open = this.model.get(openName); var close = this.model.get(closeName); // special case: when closed or open all day, it's acceptable for all four values to be set to zero var businessDayStatusName = 'is_open_' + day; var businessDayStatus = this.getField(businessDayStatusName); var businessDayStatusValue = businessDayStatus.getValue(); if (businessDayStatus.isOpenAllDayValue(businessDayStatusValue) || businessDayStatus.isClosedValue(businessDayStatusValue) ) { if ( open.hour === close.hour && open.minute === close.minute && !open.hour && !open.minute ) { return; } } open = app.date(open); close = app.date(close); if (!open.isBefore(close)) { newErrors[openName] = { ERROR_TIME_IS_BEFORE: this.getField(closeName).label }; newErrors[closeName] = { ERROR_TIME_IS_AFTER: this.getField(openName).label }; } }, this); callback(null, fields, _.extend(errors, newErrors)); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.BusinessCenters.CreateView * @alias SUGAR.App.view.views.BusinessCentersCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._days = app.view.views.BaseBusinessCentersRecordView.prototype._days; this.model && this.model.addValidationTask( 'all_open_hours_before_close_hours_' + this.cid, _.bind(this.validateBusinessHours, this) ); }, validateBusinessHours: function() { // to avoid duplicating substantial code, borrow the implementation from the record view var args = Array.prototype.slice.call(arguments); return app.view.views.BaseBusinessCentersRecordView.prototype.validateBusinessHours.apply(this, args); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Shifts":{"fieldTemplates": {} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Shifts.RecordView * @alias SUGAR.App.view.views.ShiftsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', _days: [ 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.model && this.model.addValidationTask( 'all_open_hours_before_close_hours_' + this.cid, _.bind(this.validateHoursList, this) ); }, /* * Validation of the time fields * * @param {Object} fields List of fields that can be validated. * @param {Object} errors Existing validation errors. * @param {Function} callback To be called after completion. */ validateHoursList: function(fields, errors, callback) { let newErrors = {}; _.each(this._days, function(day) { _.extend(newErrors, this.validateHours(day)); }, this); callback(null, fields, _.extend(errors, newErrors)); }, /* * Validation a one day * * @param {String} day * @return {Object} Existing validation errors */ validateHours: function(day) { let newErrors = {}; const openName = day + '_open'; const closeName = day + '_close'; const open = app.date(this.model.get(openName)); const close = app.date(this.model.get(closeName)); const isOpen = this.model.get('is_open_' + day); if (isOpen && !open.isBefore(close)) { newErrors[openName] = { ERROR_TIME_IS_BEFORE: this.getField(closeName).label, }; newErrors[closeName] = { ERROR_TIME_IS_AFTER: this.getField(openName).label, }; } return newErrors; }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Shifts.CreateView * @alias SUGAR.App.view.views.ShiftsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._days = app.view.views.BaseShiftsRecordView.prototype._days; this.model && this.model.addValidationTask( 'all_open_hours_before_close_hours_' + this.cid, _.bind(this.validateHoursList, this) ); }, /* * The wrapper to execute the function from the record view */ validateHoursList: function() { var args = Array.prototype.slice.call(arguments); return app.view.views.BaseShiftsRecordView.prototype.validateHoursList.apply(this, args); }, /* * The wrapper to execute the function from the record view */ validateHours: function() { var args = Array.prototype.slice.call(arguments); return app.view.views.BaseShiftsRecordView.prototype.validateHours.apply(this, args); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ShiftExceptions":{"fieldTemplates": { "base": { "all-day": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ShiftExceptions.ShiftExceptionsAllDayField * @extends View.Fields.Base.BoolField */ ({ // All-day FieldTemplate (base) extendsFrom: 'BoolField', /** * Defines the start and end times of the day * * @property {Object} */ _defaultDayStartEnd: { start_hour: 0, start_minutes: 0, end_hour: 23, end_minutes: 59, }, /** * Object for saving current time values between switches * * @property {Object} */ _currentDayStartEnd: {}, /** * Start/End fields * * @property {string} */ _timeFields: [ 'start_time', 'end_time', ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.type = 'bool'; this._currentDayStartEnd = {}; if (this.model.isNew()) { this._currentDayStartEnd = { start_hour: 0, start_minutes: 0, end_hour: 0, end_minutes: 0, }; this.view.once('render', function() { this._updateTimeFields(false); }, this); } if (_.contains(['preview', 'dashablerecord'], this.view.name)) { this.view.once('render', function() { this._toggleTimeFields(); }, this); } // Do not validate start and end time for list view because // 1. start time and end time are not available in list view // 2. start date, end date, and all_day are readonly in list view, // so no need to validate start and end time if (this.view.tplName != 'list') { this.model.addValidationTask( 'start_time_before_end_time' + this.cid, _.bind(this._validateStartTime, this) ); } }, /** * Validate the "Start Time" field */ _validateStartTime: function(fields, errors, callback) { let newErrors = {}; // if "All day" is not active if (!this.getValue()) { const openName = 'start_time'; const closeName = 'end_time'; const start = app.date(this.model.get(openName)); const end = app.date(this.model.get(closeName)); if (this.model.get('start_date') === this.model.get('end_date') && !start.isBefore(end)) { newErrors[openName] = { ERROR_TIME_IS_BEFORE: this.view.getField(closeName).label, }; newErrors[closeName] = { ERROR_TIME_IS_AFTER: this.view.getField(openName).label }; } } callback(null, fields, _.extend(errors, newErrors)); }, /** * Restore temporary values */ _restoreTime: function() { if (!_.isEmpty(this._currentDayStartEnd)) { this.model.set(this._currentDayStartEnd); } }, /** * Set default value for the saving * @param {boolean} save It shows if it needs to save current values */ _clearTime: function(save) { if (save) { this._saveTime(); } this.model.set(this._defaultDayStartEnd); }, /** * Update the model and show/hide time fields * @param {boolean} save It shows if it needs to save current values */ _updateTimeFields: function(save) { const isAllDay = this.getValue(); isAllDay ? this._clearTime(save) : this._restoreTime(); if (this.view.name !== 'recordlist') { this._toggleTimeFields(); } }, /** * Toggle (show/hide) time fields */ _toggleTimeFields: function() { const isAllDay = this.getValue(); $.each(this._timeFields, function(key, item) { const field = this.view.getField(item); field.$el.closest('.record-cell').toggle(!isAllDay); }.bind(this)); }, /** * Add temporary values to object */ _saveTime: function() { $.each(this._defaultDayStartEnd, function(key) { this._currentDayStartEnd[key] = this.model.get(key); }.bind(this)); }, bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:' + this.name, function() { this._updateTimeFields(true); }, this); this.context.on('button:save_button:click', this._saveTime, this); }, unformat: function(value) { return (value && value !== '0') ? 1 : 0; }, getValue: function() { const value = this.model.get(this.name); return !!(value && value !== '0'); }, }) } }} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Purchases":{"fieldTemplates": {} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Purchases.RecordView * @alias SUGAR.App.view.views.BasePurchasesRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this._super('initialize', [options]); }, /** * @inheritdoc */ setupDuplicateFields: function(prefill) { var calculatedFields = ['start_date', 'end_date']; _.each(calculatedFields, function(field) { prefill.unset(field); }); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "PurchasedLineItems":{"fieldTemplates": { "base": { "dates-name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.PurchasedLineItems.DatesNameField * @alias SUGAR.App.view.fields.BasePurchasedLineItemsDatesNameField * @extends View.Fields.Base.NameField */ ({ // Dates-name FieldTemplate (base) extendsFrom: 'NameField', /** * Format date according to user preferences. This is copied from the date field controller * @param rawValue * @return {string} * @private */ _formatDate: function(rawValue) { let value = app.date(rawValue); if (!value.isValid()) { return ''; } return value.formatUser(true); }, /** * Formats the relevant date fields in addition to the base name field * @param value * @return {string} */ format: function(value) { if (this.model && this.model.has('service_start_date') && this.model.has('service_end_date')) { this.serviceStartDate = this._formatDate(this.model.get('service_start_date')); this.serviceEndDate = this._formatDate(this.model.get('service_end_date')); } return this._super('format', [value]); } }) }, "relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.PurchasedLineItems.RelateField * @alias SUGAR.App.view.fields.BasePurchasedLineItemsRelateField * @extends View.Fields.Base.RelateField */ ({ // Relate FieldTemplate (base) extendsFrom: 'BaseRelateField', /** * @override * * Hiding the revenuelineitem_name field when the in Opps Only mode */ _render: function() { var oppConfig = app.metadata.getModule('Opportunities', 'config'); if (oppConfig && !_.isUndefined(this.def.showInMode) && oppConfig.opps_view_by !== this.def.showInMode) { this.$el.closest('.record-cell').hide(); } this._super('_render'); }, }) }, "service-enddate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.PurchasedLineItems.ServiceEnddateField * @alias SUGAR.App.view.fields.BasePurchasedLineItemsServiceEnddateField * @extends View.Fields.Base.ServiceEnddateField */ ({ // Service-enddate FieldTemplate (base) extendsFrom: 'BaseServiceEnddateField', /** * @override * * Override to calculate end date when the field is initialized. Because we * allow "goods" to have a service end date on PLIs, we need to calculate as * soon as the create drawer opens to avoid ever having a null end date. */ initialize: function(options) { this._super('initialize', [options]); if (this.view.action === 'create') { this.calculateEndDate(); } }, /** * @override */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:purchase_name', this.calculateEndDate, this); }, /** * @override * * Overrides calculate end date function of base service end date field, * since we do have a desired end date for non-service PLIs */ calculateEndDate: function() { if (this.model.get('service')) { this._super('calculateEndDate'); } else { this.model.set(this.name, this.model.get('service_start_date')); } } }) }, "create-add-on": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.PurchasedLineItems.CreateAddOnField * @alias SUGAR.App.view.fields.BasePurchasedLineItemsCreateAddOnField * @extends View.Fields.Base.RowactionField */ ({ // Create-add-on FieldTemplate (base) extendsFrom: 'RowactionField', events: { 'click [data-action=existing_opportunity]': 'existingOpportunityClicked', 'click [data-action=new_opportunity]': 'newOpportunityClicked', 'click [data-action=link]': 'toggleAddOns' }, _render: function() { if (this.parent && this.parent.model && this.parent.model.get('service')) { this._super('_render'); this.$('[data-action=existing_opportunity]').hide(); this.$('[data-action=new_opportunity]').hide(); this.$('.dropdown-inset').hide(); } }, /** * Toggles new and existing opportunity buttons * @param {Event} evt */ toggleAddOns: function(evt) { if (evt) { evt.preventDefault(); evt.stopPropagation(); } this.$('[data-action=existing_opportunity]').toggle(); this.$('[data-action=new_opportunity]').toggle(); this.$('.dropdown-inset').toggle(); }, /** * Handles Existing Opportunity button being clicked * @param {Event} evt */ existingOpportunityClicked: function(evt) { var parentModel = this.parent.model; var filterOptions = new app.utils.FilterOptions() .config({ initial_filter: 'filterOpportunityTemplate', initial_filter_label: 'LBL_FILTER_OPPORTUNITY_TEMPLATE', filter_populate: { account_id: parentModel.get('account_id') } }).format(); // Open existing opportunities app.drawer.open({ layout: 'selection-list', context: { module: 'Opportunities', filterOptions: filterOptions, parent: this.context, } }, _.bind(this.selectExistingOpportunityDrawerCallback, this)); }, /** * Handles New Opportunity button being clicked * @param {Event} evt */ newOpportunityClicked: function(evt) { var pliModel = this.parent.model; // Set the values for the new Opportunity var opportunityModel = app.data.createBean('Opportunities'); opportunityModel.set({ account_id: pliModel.get('account_id'), account_name: pliModel.get('account_name'), }); // Set the basic values for the new RLI var addOnToData = { add_on_to_id: pliModel.get('id'), add_on_to_name: pliModel.get('name'), service: '1' }; var self = this; this._getAddOnRelatedFieldValues(pliModel, addOnToData, function(rliData) { app.drawer.open({ layout: 'create', context: { create: true, module: 'Opportunities', model: opportunityModel, addOnToData: rliData } }, _.bind(self.refreshRLISubpanel, self)); }); }, /** * Open new RevenueLineItem drawer when an opportunity is selected * @param {Object} model */ selectExistingOpportunityDrawerCallback: function(model) { if (!model || _.isEmpty(model.id)) { return; } var revenueLineItemModel = app.data.createBean('RevenueLineItems'); var pliModel = this.parent.model; // set up RLI to open when opportunity is selected var addOnToData = { add_on_to_id: pliModel.get('id'), add_on_to_name: pliModel.get('name'), service: '1', opportunity_name: model.name, opportunity_id: model.id }; var self = this; this._getAddOnRelatedFieldValues(pliModel, addOnToData, function(rliData) { revenueLineItemModel.set(rliData); app.drawer.open({ layout: 'create', context: { create: true, module: 'RevenueLineItems', model: revenueLineItemModel } }, _.bind(self.refreshRLISubpanel, self)); }); }, /** * Retrieves data from a PLI model and its Product Template if applicable. * Used by "Add On To" fields to populate default values from multiple sources * based on the value of the "Add On To" field * * @param pliModel the PLI model * @param addOnToData the object holding attributes related to the "Add On To" field * @param callback the callback function to call when data is finished being retrieved * @private */ _getAddOnRelatedFieldValues: function(pliModel, addOnToData, callback) { // Get the values to include on the RLI based on the PLI and/or its related // Product Template var rliFields = app.metadata.getModule('RevenueLineItems', 'fields'); if (rliFields && rliFields.add_on_to_name && !_.isEmpty(rliFields.add_on_to_name.copyFromPurchasedLineItem)) { _.each(rliFields.add_on_to_name.copyFromPurchasedLineItem, function(fromField, toField) { if (_.isEmpty(addOnToData[toField])) { addOnToData[toField] = pliModel.get(fromField); } }, this); } if (rliFields && rliFields.add_on_to_name && !_.isEmpty(rliFields.add_on_to_name.copyFromProductTemplate) && addOnToData.product_template_id) { // The PLI is using a product template, and there are fields to copy // from it, so fetch its data before opening the create drawer var productTemplateBean = app.data.createBean('ProductTemplates', {id: addOnToData.product_template_id}); app.alert.show('fetching_product_template', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false }); productTemplateBean.fetch({ success: _.bind(function(templateData) { _.each(rliFields.add_on_to_name.copyFromProductTemplate, function(toField, fromField) { if (_.isEmpty(addOnToData[toField])) { addOnToData[toField] = templateData.get(fromField); } }, this); }, this), complete: _.bind(function() { app.alert.dismiss('fetching_product_template'); callback(addOnToData); }, this) }); } else { // The PLI is not using a product template, or there are no fields to // copy from it, so just open the create drawer callback(addOnToData); } }, refreshRLISubpanel: function(model) { if (!model) { return; } var ctx = this.listContext || this.context; ctx.reloadData({recursive: false}); // Refresh RevenueLineItems subpanel when drawer is closed if (!_.isUndefined(ctx.children)) { _.each(ctx.children, function(child) { if (_.contains(['RevenueLineItems'], child.get('module'))) { child.reloadData({recursive: false}); } }); } }, /** * @inheritdoc * Check access. */ hasAccess: function() { var pliViewAccess = app.acl.hasAccess('view', 'PurchasedLineItems'); var rliCreateAccess = app.acl.hasAccess('create', 'RevenueLineItems'); var oppCreateAccess = app.acl.hasAccess('create', 'Opportunities'); var oppConfig = app.metadata.getModule('Opportunities', 'config'); var rlisTurnedOn = oppConfig && oppConfig.opps_view_by === 'RevenueLineItems'; return pliViewAccess && rliCreateAccess && oppCreateAccess && rlisTurnedOn && this._super('hasAccess'); } }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.PurchasedLineItems.RecordView * @alias SUGAR.App.view.views.PurchasedLineItemsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc * @param options */ initialize: function(options) { // Adds this plugin to handle changes to Service and Purchase Name fields this.plugins = _.union(this.plugins || [], ['PurchaseAndServiceChangeHandler']); this._super('initialize', [options]); // Binds handlers for service and purchase_name changes this.bindDataChange(); }, }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.PurchasedLineItems.ActivityCardDetailView * @alias SUGAR.App.view.views.BasePurchasedLineItemsActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) /** * @inheritdoc */ formatDate: function(date) { return date.formatUser(true); }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.PurchasedLineItems.CreateView * @alias SUGAR.App.view.views.PurchasedLineItemsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Adds PurchaseAndServiceChangeHandler plugin * Manually sets default values for service related field * * @inheritdoc * @param options */ initialize: function(options) { // Adds this plugin to handle changes to Service and Purchase Name fields this.plugins = _.union(this.plugins || [], ['PurchaseAndServiceChangeHandler']); this._super('initialize', [options]); if (!_.isUndefined(this.model)) { var parent = !_.isUndefined(this.context.parent) ? this.context.parent : {}; var parentModule = !_.isEmpty(parent) ? parent.get('module') : ''; // If the create view drawer is opened from the Purchase module PLI subpanel // and Purchase and Product name are already populated if (parentModule === 'Purchases' && !_.isEmpty(this.model.get('purchase_name')) && !_.isEmpty(this.model.get('product_template_id'))) { this.handlePurchaseChange(); } // Setting service_duration_value field defaults here since it is coming from the sales_item vardefs this.model.setDefault('service_duration_value', 1); var modelFields = this.model.fields || {}; if (!_.isEmpty(modelFields) && !_.isUndefined(modelFields.service_start_date)) { // Service start date displays today's date by default modelFields.service_start_date.display_default = 'now'; } // This sets the service_duration_unit based on service field on first load this.handleServiceChange(); // Binds handlers for service and purchase_name changes this.bindDataChange(); } }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "MobileDevices":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "PushNotifications":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Escalations":{"fieldTemplates": {} , "views": { "base": { "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Escalations.ActivityCardContentView * @alias SUGAR.App.view.views.BaseEscalationsActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.formatDescriptionField(); }, /** * Formats the description field to account for line breaks */ formatDescriptionField: function() { if (this.activity) { var description = this.activity.get('description'); this.descriptionField = this.formatContent(description); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Escalations.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseEscalationsActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "DocumentTemplates":{"fieldTemplates": { "base": { "file": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocumentTemplates.FileField * @alias SUGAR.App.view.fields.BaseDocumentTemplatesFileField * @extends View.Fields.Base.BaseField */ ({ // File FieldTemplate (base) extendsFrom: 'BaseFileField', supportedFileExtensions: ['docx', 'xlsx', 'pptx',], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); app.error.errorName2Keys = app.error.errorName2Keys || {}; app.error.errorName2Keys.unsupportedExtension = 'ERROR_WRONG_EXTENSION'; }, /** * @inheritdoc */ _doValidateFile: function(fields, errors, callback) { const fieldName = this.name; const fileName = this.model.get(this.name); if (this.def.required && _.isEmpty(fileName)) { errors[fieldName] = errors[fieldName] || {}; errors[fieldName].required = true; callback(null, fields, errors); return; } if (_.isString(fileName)) { const extension = fileName.substr(fileName.lastIndexOf('.') + 1); if (!this.supportedFileExtensions.includes(extension)) { errors[this.name] = errors[this.name] || {}; errors[this.name].unsupportedExtension = true; callback(null, fields, errors); app.alert.show('file_ext_error', { level: 'error', messages: app.lang.getModString('LBL_ERROR_WRONG_EXTENSION', this.module), }); return; } } var fileInputEl = this.$(this.fieldTag); if (fileInputEl.length === 0) { callback(null, fields, errors); return; } var val = fileInputEl.val(); if (!_.isEmpty(val)) { this._uploadFile(fieldName, fileInputEl, fields, errors, callback); } else { callback(null, fields, errors); return; } } }) } }} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "DocumentMerges":{"fieldTemplates": { "base": { "multimerge-records": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocumentMerges.MultimergeRecordsField * @alias SUGAR.App.view.fields.BaseDocumentMergesMultimergeRecordsField * @extends View.Fields.Base.BaseField */ ({ // Multimerge-records FieldTemplate (base) /** * @inheritdoc */ format: function(value) { this._super('format', arguments); try { this.records = JSON.parse(value); } catch (error) { this.records = []; } return value; } }) } }} , "views": { "base": { "tag-builder-relationships": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Used to display relationships for a certain module in tag builder. * * @class View.Views.Base.DocumentMerges.TagBuilderRelationshipsView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderRelatiosnhipsView */ ({ // Tag-builder-relationships View (base) /** * @inheritdoc */ events: { 'change .relationshipSelect': 'setSelectedRelationship', 'click .removeRelationship': 'removeRelationship', 'click .relationshipOptions': 'showCollectionOptions', }, /** * keeps the selected relationships * * @var array */ relationshipStack: [], /** * keeps the order of the relationships selected modules * * @var array */ relationshipModuleStack: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.listenTo(this.context, 'tag-builder:reset-relationships', this._resetRelationships, this); this.listenTo(this.context, 'change:currentRelationshipsModule', this.addToRelationshipStack, this); }, /** * @inheritdoc */ render: function() { this._super('render'); let relationshipList = this.$('select.relationshipSelect'); relationshipList.select2({ placeholder: relationshipList.attr('placeholder'), }); }, /** * Set the selected value on the last relationship * * @param {Event} evt */ setSelectedRelationship: function(evt) { const relationship = evt.target.value; let lastRelationship = this.relationshipStack.pop(); //find the selected relationship and set the selected value on that item for (let rel of lastRelationship) { if (rel.name === relationship) { const relationshipMeta = app.metadata.getRelationship(rel.relationship); const relationshipModule = relationshipMeta.lhs_module !== (this.currentRelationshipsModule || this.context.get('currentModule')) ? relationshipMeta.lhs_module : relationshipMeta.rhs_module; rel.selected = true; this.currentRelationshipsModule = relationshipModule; } else { rel.selected = false; } } this.relationshipStack.push(lastRelationship); this.context.set({ currentRelationshipsModule: this.currentRelationshipsModule, currentModule: this.currentRelationshipsModule }); }, /** * Adds relatiosnhips to stack * * @param {app.Context} context * @param {string} module */ addToRelationshipStack: function(context, module) { if (_.isEmpty(module)) { this.relationshipStack = []; this.relationshipModuleStack = []; this.currentModule = module; this.currentRelationshipsModule = module; this.render(); return; } let relationships = this.filterRelationships(module); if (!_.isEmpty(relationships)) { this.relationshipStack.push(relationships); } this.relationshipModuleStack.push(module); this.secondLastRelationshipStackIndex = this.relationshipStack.length > 1 ? this.relationshipStack.length - 2 : 0; this.context.set('currentModule', module); this.render(); }, /** * Gets the relationship fields for a module * * @param {string} module * @return {Array} */ filterRelationships: function(module) { if (!module) { return []; } const moduleMeta = app.metadata.getModule(module) || {}; if (!moduleMeta.fields) { return []; } let filteredRels = _.filter(moduleMeta.fields, function(field) { return field.type === 'link' && field.link_type !== 'one'; }); // make sure the relationship has a module so it can translate labels return _.map(filteredRels, function(relationship) { if (!app.lang.getModString(relationship.vname, relationship.module)) { relationship.module = module; } return relationship; }); }, /** * Clear the select relationship form stack * * @param {Event} evt */ removeRelationship: function(evt) { if (!this.currentRelationshipsModule) { return; } const stackIndex = evt.target.getAttribute('stack-index'); this.removeFromRelationshipStack(parseInt(stackIndex)); if (this.relationshipModuleStack.length > 1) { this.relationshipModuleStack.pop(); this.currentRelationshipsModule = [...this.relationshipModuleStack].pop(); } else { //there's only one module in the stack this.currentRelationshipsModule = this.relationshipModuleStack[0]; } if (this.secondLastRelationshipStackIndex > 0) { this.secondLastRelationshipStackIndex--; } this.context.set('currentModule', this.currentRelationshipsModule); this.render(); }, /** * When removing from the stack, * we need to make sure there is at least one remaining relationship * * @param {number} stackIndex */ removeFromRelationshipStack: function(stackIndex) { if (this.relationshipStack.length > 1) { this.relationshipStack.splice(stackIndex + 1, 1); this.resetRelationshipsSelected(); this.currentRelationshipsModule = [...this.relationshipModuleStack].pop(); } else { this.resetRelationshipsSelected(); this.currentRelationshipsModule = null; } }, /** * reset the selected variable on the last relationship inside the stack */ resetRelationshipsSelected: function() { length = this.relationshipStack.length; for (let relationship of this.relationshipStack[length - 1]) { relationship.selected = false; } }, /** * Trigger collection options * * @param {Event} evt */ showCollectionOptions: function(evt) { if (!this.currentRelationshipsModule) { return; } const stackIndex = evt.target.getAttribute('stack-index'); const currentRelationships = this.relationshipStack[stackIndex]; let selectedRelationship = _.filter(currentRelationships, function(item) { return item.selected === true; })[0]; let currentRelationshipsModule = this.relationshipModuleStack[stackIndex]; selectedRelationship.currentModule = currentRelationshipsModule; if (selectedRelationship) { this.context.trigger('tag-builder-options:show', selectedRelationship); } }, /** * Whenever the module is changed we need to reset * all relationship information. * * @param {string} module */ _resetRelationships: function(module) { this.relationshipStack = []; this.relationshipModuleStack = [module]; this.currentRelationshipsModule = module; }, }) }, "merge-widget-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocumentMerges.MergeWidgetHeaderView * @alias SUGAR.App.view.views.BaseDocumentMergesMergeWidgetHeaderView * @extends View.View */ ({ // Merge-widget-header View (base) /** * @inheritdoc */ events: { 'click #template-assistant': 'openTemplateBuilder' }, /** * Opens the Template Builder help view * * @param {Event} evt */ openTemplateBuilder: function(evt) { window.open( '#DocumentMerges/layout/tag-builder', 'TemplateBuilder', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=415,height=800' ); }, }) }, "merge-widget-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocumentMerges.MergeWidgetListView * @alias SUGAR.App.view.views.BaseDocumentMergesMergeWidgetListView * @extends View.View */ ({ // Merge-widget-list View (base) events: { 'click .download': 'downloadDocument', 'click .remove-merge': 'removeMergeFromWidget', }, plugins: ['DocumentMergeActions'], /** * Merges to display inside the widget * @property array */ merges: [], /** * Completion levels of the merge * @property {Object} */ completion: { 'processing': 15, 'document_load': 30, 'tags_extract': 45, 'tags_validate': 60, 'data_retrieving': 70, 'serialize_document': 85, 'send_document': 95, 'success': 100, 'error': 100 }, /** * Icons of uploaded documents * @property {Object} * */ icons: { 'DOC': 'doc', 'PDF': 'pdf', 'PPT': 'ppt', 'XLS': 'excel' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); app.events.on('document_merge:poll_merge', this.pollMerge, this); this._fields = [ 'id', 'name', 'generated_document_id', 'template_id', 'parent_id', 'parent_type', 'parent_name', 'file_type', 'status', 'message', 'merge_type' ]; /** * trigger loading merges */ this.loadData(); }, /** * Load merge data */ loadData: function() { var url = app.api.buildURL(this.module, 'create', {}, { 'fields': this._fields, 'max_num': app.config.maxQueryResult, 'order_by': 'date_entered:desc', 'filter': [{seen: 0, assigned_user_id: app.user.id},], }); app.api.call('read', url, null, { success: _.bind(this._mergesRetrieved, this), error: function(error) { app.alert.show('merges-error', { level: 'error', autoClose: true, messages: error.message, }); } }); }, /** * Data retrieved. * Set the merge object and render. * * @param {Object} data */ _mergesRetrieved: function(data) { _.isArray(data.records) ? this.merges = data.records : this.merges = []; _.map(this.merges, function(merge) { merge.completion = this.completion[merge.status]; merge.isMultiMerge = this._isMultiMerge(merge.merge_type); let icon = this.icons[merge.file_type] || this.icons.DOC; merge.icon_path = `themes/default/images/icon_${icon}.svg`; }.bind(this)); this.render(); this.layout.reposition(); }, /** * @inheritdoc * * Render tooltips and the office icons */ _render: function() { this._super('_render', arguments); this._renderTooltips(); this._setMergeCompletion(); }, /** * render tooltips */ _renderTooltips: function() { this.$('#actions').tooltip({ selector: '[rel="tooltip"]', container: 'body', }); }, /** * Download the document * * @param {Event} evt */ downloadDocument: function(evt) { let mergeObject = _.find(this.merges, {id: this._getMergeId(evt)}); if (!mergeObject || mergeObject.status !== 'success') { return; } const documentId = evt.currentTarget.getAttribute('document-id'); const fileUrl = app.api.buildFileURL({ module: 'Documents', id: documentId, field: 'filename', }, { forceDownload: true, cleanCache: true, }); app.api.fileDownload( fileUrl, {} ); }, /** * poll the status and message of the DocumentMerge until we find success or error * * We stop polling only one of the following is true: * - the merge was succesfull * - the merge returned an error * - 3 minutes have passed * * @param {string} documentMergeId */ pollMerge: function(documentMergeId) { var timesRun = 0; var maxRun = 1200; //equivalent of running the timer for 10 minutes if we run it once at 2 seconds this._documentGeneratedEventTriggered = false; var timer = setInterval(function() { timesRun++; //if it takes longer than 10 minutes do not wait if (timesRun === maxRun) { clearInterval(timer); } app.data.createBean('DocumentMerges', {id: documentMergeId}).fetch({ fields: ['status', 'message', 'generated_document_id'], success: function(record) { if (record.get('status') === 'success') { clearInterval(timer); if (!this._documentGeneratedEventTriggered) { app.events.trigger('docmerge:document:generated', { id: record.get('generated_document_id') }); this._documentGeneratedEventTriggered = true; } app.alert.show('merge_success', { level: 'success', messages: app.lang.getModString('LBL_GENERATED_DOCUMENT', 'DocumentMerges'), }); } if (record.get('status') === 'error') { clearInterval(timer); app.alert.show('merge_error', { level: 'error', messages: record.get('message') }); } }.bind(this), error: function(error) { //Stop polling if the request failed clearInterval(timer); if (_.has(error, 'message')) { app.alert.show('merge_error', { level: 'error', messages: error.message }); } }, complete: function(response) { /** * Here we manage the completion level of the progress bar */ var record = response.xhr.responseJSON || {}; _.map(this.merges, _.bind(function(merge) { if (merge.id === record.id) { merge.completion = this.completion[record.status]; merge.message = record.message; merge.status = record.status; merge.generated_document_id = record.generated_document_id; } }, this)); this.render(); }.bind(this) }); }.bind(this), 500); }, /** * Removes the merge from the widget * * @param {Event} evt */ removeMergeFromWidget: function(evt) { evt.preventDefault(); const mergeId = evt.target.closest('.merge-row').getAttribute('merge-id'); if (mergeId) { const url = app.api.buildURL('DocumentMerges', mergeId); app.api.call('update', url, {'seen': true}); this.merges = _.filter(this.merges, function(merge) { return merge.id !== mergeId; }); this.render(); this.layout.reposition(); } }, /** * Checks if the merge is a multimerge * * @param {string} mergeType * @return {bool} */ _isMultiMerge: function(mergeType) { return mergeType === 'multimerge' || mergeType === 'multimerge_convert' || mergeType === 'labelsgenerate' || mergeType === 'labelsgenerate_convert'; }, /** * Update merge completion */ _setMergeCompletion: function() { _.each(this.merges, _.bind(function(merge) { this.$('[data-merge-id=' + merge.id + ']').css('width', merge.completion + '%'); }, this)); }, }) }, "tag-builder-directives": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The view used for directives builder. * * @class View.Views.Base.DocumentMerges.TagBuilderDirectivesView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderDirectivesView */ ({ // Tag-builder-directives View (base) /** * @inheritdoc */ events: { 'change .directivesList': 'changeDirective', 'change .dm-tag-attribute': 'applyAttribute', 'change .customDateOption': 'toggleDateOption', 'change .relationshipsModuleList': 'updateRelationshipFieldsList', 'change .tableRelationshipFieldsList': 'updateTableHeader', }, /** * List of predefined date formats. * @var array */ dateFormats: [ 'MM-DD-YYYY', 'MMM-YYYY', 'MMMM-YYYY', 'MM/DD/YYYY', 'MM-Do-YYYY', 'YYYY MMM', ], /** * List of directives. * @var array */ directives: [ { value: 'date', name: 'Date', }, { value: 'list', name: 'List', }, { value: 'table', name: 'Table', }, ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.listenTo(this.context, 'change:currentModule', this.updateRelationships, this); }, /** * @inheritdoc */ render: function() { this._super('render'); this.initializeDropDowns(); this.hideCustomDate(); this.initColorPicker(); }, /** * Initialize color picker */ initColorPicker: function() { var field = this.$('.hexvar[rel=colorpicker]'); var preview = this.$('.color-preview'); field.colorpicker(); field.on('blur', _.bind(function() { var value = field.val(); preview.css('backgroundColor', value); this.applyAttribute(field); }, this)); }, /** * Initializes the select2 dropdowns. */ initializeDropDowns: function() { let dropDownList = this.$('select'); for (let dropDown of dropDownList) { let placeholder = this.$(dropDown).attr('placeholder'); let type = this.$(dropDown).attr('multiple'); if (type === 'multiple') { this.$(dropDown).select2({ allowClear: true, placeholder: placeholder, containerCssClass: 'select2-choices-pills-close', closeOnSelect: false, width: '100%', }); } else { this.$(dropDown).select2({ allowClear: true, placeholder: placeholder, width: '100%', }); } } }, /** * Hides custom date. */ hideCustomDate: function() { this.$('.customDate').hide(); }, /** * Changes the directive view. * @param {Event} evt */ changeDirective: function(evt) { this.resetDirectiveValues(); this.initTag(); this.currentDirective = evt.target.value; for (let directive of this.directives) { if (directive.value === this.currentDirective) { directive.selected = true; } else { directive.selected = false; } } this.tag.setName(this.currentDirective); this.render(); }, /** * Initializes the tag. */ initTag: function() { let tagBuilderFactory = new App.utils.DocumentMerge.TagBuilderFactory(); let tagBuilder = tagBuilderFactory.getTagBuilder('directive'); let tag = tagBuilder.newTag().get(); this.tag = tag; }, /** * Applies the attribute to the tag. * * @param {Event} evt */ applyAttribute: function(evt) { let field = evt.target || evt[0]; const inputType = field.type; let inputValue = this.$(field).val(); const inputName = this.$(field).attr('name'); if (inputType === 'checkbox') { inputValue = field.checked; } if (inputName === 'sort') { let fieldToSortBy = this.$('select.sortByRelationshipFields').val(); let sortBy = this.$('select.sortBy').val(); inputValue = fieldToSortBy + ':' + sortBy; } option = {}; option[inputName] = inputValue; this.tag.setAttribute(option); if (!inputValue || inputValue.length === 0) { this.tag.removeAttribute(inputName); } let tagValue = this.tag.compile().getTagValue(); this.$('.preview').text(tagValue); }, /** * Changes the custom date input visibility. * @param {Event} evt */ toggleDateOption: function(evt) { let checkedStatus = evt.currentTarget.checked; let customDate = this.$('.customDate'); if (checkedStatus) { customDate.show(); this.toggleFormatSelects(true); } else { customDate.hide(); this.toggleFormatSelects(false); this.clearCustomValues(); } }, /** * When choosing the custom option, we disable the format selects for date * * @param {bool} status */ toggleFormatSelects(status) { this.$('.dateFormatSelect').attr('disabled', status); }, /** * Clears the custom date input. */ clearCustomValues: function() { let customValues = this.$('.customDateValue'); customValues.val(''); customValues.trigger('change'); }, /** * Updates the relationships dropdown when the module gets changed. * @param {app.Context} context * @param {string} module */ updateRelationships: function(context, module) { if (!module) { return []; } this.currentModule = module; const moduleMeta = app.metadata.getModule(module) || {}; const relationships = _.filter(moduleMeta.fields, function(field) { return field.type === 'link' && field.link_type !== 'one'; }); this.moduleRelationships = _.map(relationships, _.bind(function(relationship) { return { name: relationship.name, relationshipName: relationship.relationship, module: relationship.module || this.getRelationshipModule(relationship.relationship, module), moduleLabelTranslation: app.lang.getModuleName( relationship.module || this.getRelationshipModule(relationship.relationship, module)), }; }, this)); this.render(); }, /** * Gets the relationship's module when it's not defined in the relationship. * @param {string} relationship * @param {string} module */ getRelationshipModule: function(relationship, module) { let relationshipMeta = app.metadata.getRelationship(relationship); if (!relationshipMeta) { return null; } return relationshipMeta.rhs_module !== module ? relationshipMeta.rhs_module : relationshipMeta.lhs_module; }, /** * Updates the relationship fields list. * * @param {Event} evt */ updateRelationshipFieldsList: function(evt) { this.resetDirectiveValues(); let fields = []; const module = this.$(evt.target).find(':selected').attr('module'); const moduleMeta = app.metadata.getModule(module); _.find(this.moduleRelationships, _.bind(function(relationship) { if (relationship.selected === true) { relationship.selected = false; } if (relationship.module === module) { relationship.selected = true; } }), this); if (moduleMeta) { let mappedFields = _.map(moduleMeta.fields, function(field) { let label = app.lang.get(field.vname, module); return { name: field.name, label: label || field.name, type: field.type, module: field.type === 'relate' ? field.module : module }; }); fields = _.filter(mappedFields, function(field) { return field.type !== 'link' && field.type !== 'id' && field.name && typeof field.name === 'string' && field.name.length > 0; }); } this.relationshipFields = fields; this.render(); }, /** * Resets the relationship fields when changing directive. */ resetDirectiveValues: function() { this.headerFields = []; this.resetSelected(); this.relationshipFields = []; }, /** * Updates the table header. * @param {Event} evt */ updateTableHeader: function(evt) { this.cocatRelationshipFields = ''; this.$('.tableHeader').attr('readonly', false); if (evt.added) { this.addRelationshipField(evt.added); } if (evt.removed) { this.removeRelationshipField(evt.removed); } _.each(this.headerFields, _.bind(function(field) { if (_.isEmptyValue(this.cocatRelationshipFields)) { this.cocatRelationshipFields = field.label; } else { this.cocatRelationshipFields = this.cocatRelationshipFields.concat(',', field.label); } }, this)); let tableHeader = this.$('.tableHeader'); let tableHeaderName = tableHeader.attr('name'); tableHeader.html(this.cocatRelationshipFields); if (this.cocatRelationshipFields.length === 0) { this.tag.removeAttribute(tableHeaderName); } else { let option = {}; option[tableHeaderName] = this.cocatRelationshipFields; this.tag.setAttribute(option); } let tagValue = this.tag.compile().getTagValue(); this.$('.preview').html(tagValue); }, /** * Adds the field label to the header. * @param {Object} field */ addRelationshipField: function(field) { let fieldLabel = this.$(field.element).attr('label'); let addedField = { id: field.id, label: fieldLabel }; this.headerFields.push(addedField); }, /** * Remove the field label from the header. * @param {Object} field */ removeRelationshipField: function(field) { this.headerFields = _.filter(this.headerFields, function(headerField) { return headerField.id !== field.id; }); }, /** * Resets the selected relationship when changing directives. */ resetSelected: function() { _.find(this.moduleRelationships, function(relationship) { relationship.selected = false; }); } }) }, "tag-builder-conditionals": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The conditonal tag builder. * * @class View.Views.Base.DocumentMerges.TagBuilderConditionalsView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderConditionalsView */ ({ // Tag-builder-conditionals View (base) /** * The condition part of the if statement. * @var string */ condition: '', /** * The result part of the if statement. * @var string */ conditionResult: '', /** * The array of multiple else if conditions. * @var array */ elses: [ { condition: '', result: '', }, ], /** * The condition part of the else if statement. * @var string */ elseCondition: '', /** * The result part of the else if statement. * @var string */ elseConditionResult: '', /** * The object containing the full statement. * @var Object */ conditionalObject: { if: { condition: '', result: '', }, elseifs: [], else: { condition: '', result: '' } }, /** * The tag preview. * @var string */ preview: '', /** * @inheritdoc */ events: { 'change [name=condition]': 'setConditionalTagValues', 'change [name=conditionResult]': 'setConditionalTagValues', 'change [name=elseConditionResult]': 'setConditionalTagValues', 'change [name=elseIfCondition]': 'setElseIfConditionalTagValues', 'change [name=elseIfConditionResult]': 'setElseIfConditionalTagValues', 'click .addElse': 'addElse', 'click .removeElse': 'removeElse', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); const conditionBuilder = new app.utils.DocumentMerge.TagBuilderFactory().getTagBuilder('conditional'); this.conditionalTag = conditionBuilder.newTag(); this.conditionalTag.setName('conditional'); }, /** * Sets the elseif conditional values on the tag. * @param {Event} evt */ setElseIfConditionalTagValues: function(evt) { const block = evt.target.getAttribute('block'); const statement = evt.target.getAttribute('statement'); const index = evt.target.getAttribute('data-index'); this.elses[index][block] = evt.target.value; if (!this.conditionalObject.elseifs[index]) { this.conditionalObject.elseifs[index] = {}; } this.conditionalObject[statement][index] = this.elses[index]; this.updateTag(); }, /** * Sets the conditional values on the tag. * @param {Event} evt */ setConditionalTagValues: function(evt) { const conditionName = evt.target.getAttribute('name'); const block = evt.target.getAttribute('block'); const statement = evt.target.getAttribute('statement'); this[conditionName] = evt.target.value; this.conditionalObject[statement][block] = this[conditionName]; this.updateTag(); }, /** * Updates the tag. */ updateTag: function() { let tag = this.conditionalTag.setAttributes(this.conditionalObject).get(); this.preview = tag.compile().getTagValue(); this.render(); }, /** * Adds an additional else block. * @param {Event} evt */ addElse: function(evt) { this.elses.push({ condition: '', result: '', }); this.render(); }, /** * Remove the selected ifelse statement. * @param {Event} evt */ removeElse: function(evt) { let elseIndex = evt.currentTarget.getAttribute('elseIndex'); this.elses.splice(elseIndex, 1); this.render(); }, /** * Set the value of the hidden input * so we can copy to clipboard * * @param {Event} evt */ setFieldCopyTargetValue: function(evt) { let selectedOption = evt.target.options[evt.target.selectedIndex]; this.$('#moduleFieldsList').attr('value', selectedOption.value); } }) }, "tag-builder-module": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The modules view in tag-builder. * * @class View.Views.Base.DocumentMerges.TagBuilderModulesView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderModulesView */ ({ // Tag-builder-module View (base) /** * @inheritdoc */ events: { 'change .dm-tag-builder-modules': 'setCurrentModule', }, /** * List of modules to be excluded from tag builder * * @var array */ denyModules: [ 'Login', 'Home', 'WebLogicHooks', 'UpgradeWizard', 'Styleguide', 'Activities', 'Administration', 'Audit', 'Calendar', 'MergeRecords', 'Quotas', 'Teams', 'TeamNotices', 'TimePeriods', 'Schedulers', 'Campaigns', 'CampaignLog', 'CampaignTrackers', 'Documents', 'DocumentRevisions', 'Connectors', 'ReportMaker', 'DataSets', 'CustomQueries', 'WorkFlow', 'EAPM', 'Users', 'ACLRoles', 'InboundEmail', 'Releases', 'EmailMarketing', 'EmailTemplates', 'SNIP', 'SavedSearch', 'Trackers', 'TrackerPerfs', 'TrackerSessions', 'TrackerQueries', 'SugarFavorites', 'OAuthKeys', 'OAuthTokens', 'EmailAddresses', 'Sugar_Favorites', 'VisualPipeline', 'ConsoleConfiguration', 'SugarLive', 'iFrames', 'Sync', 'DataArchiver', 'MobileDevices', 'PushNotifications', 'PdfManager', 'Dashboards', 'Expressions', 'DataSet_Attribute', 'EmailParticipants', 'Library', 'Words', 'EmbeddedFiles', 'DataPrivacy', 'CustomFields', 'ArchiveRuns', 'KBDocuments', 'KBArticles', 'FAQ', 'Subscriptions', 'ForecastManagerWorksheets', 'ForecastWorksheets', 'pmse_Business_Rules', 'pmse_Project', 'pmse_Inbox', 'pmse_Emails_Templates', ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.getModules(); }, /** * @inheritdoc */ render: function() { this._super('render', arguments); this.initializeDropDown(); // trigger this, so the options will hide this.context.set('currentModule', null); }, /** * apply select2 to all selects */ initializeDropDown: function() { let dropDown = this.$('.select2'); dropDown.select2({ allowClear: true, placeholder: dropDown.attr('placeholder') }); }, /** * Returns a list of modules * * @return array */ getModules: function() { const url = app.api.buildURL('DocumentMerge', 'mergeModules'); app.api.call('read', url, null, { success: _.bind(function(response) { this.modules = _.map(response, function(value, key) { return { moduleName: key, moduleDisplay: value, }; }); this.render(); }, this), error: function() { app.alert.show('merges-error', { level: 'error', autoClose: true, messages: app.lang.getModString('LBL_DOCUMENT_MERGE_COULD_NOT_RETRIEVE_MODULES', this.module), }); } }); }, /** * Sets the current module on the context * * @param {Event} evt */ setCurrentModule: function(evt) { const module = this.$('select.dm-tag-builder-modules').val(); this.context.trigger('tag-builder:reset-relationships', module); this.context.set({ currentModule: module, currentRelationshipsModule: module }); }, }) }, "tag-builder-fields": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Used in tag builder for showing the current's module fields * * @class View.Views.Base.DocumentMerges.TagBuilderFieldsView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderFieldsView */ ({ // Tag-builder-fields View (base) /** * @inheritdoc */ events: { 'click .field-options': 'showFieldOptions', 'keyup #searhFields': 'search', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.listenTo(this.context, 'change:currentModule', this.refreshFields, this); }, /** * Triggers the event that makes the field options visible. * @param {Event} evt */ showFieldOptions: function(evt) { evt.preventDefault(); let fieldName = $(evt.currentTarget.closest('td')).attr('attr-name'); let field = _.find(this.fieldsMeta, function(_field) { return _field.name === fieldName; }); this.context.trigger('tag-builder-options:show', field); }, /** * retrieves fields for a module * * @param {app.Context} context * @param {string} module */ refreshFields: function(context, currentModule) { this.currentModule = currentModule; // reset the fields if the module is unselected if (_.isEmpty(this.currentModule)) { this.fieldsMeta = []; this.render(); return; } const fields = app.metadata.getModule(this.currentModule) ? app.metadata.getModule(this.currentModule).fields : []; if (!_.isEmpty(fields)) { this.fieldsMeta = _.filter(fields, function(field) { return field.type != 'link' && field.type != 'id' && field.name && typeof field.name == 'string' && field.name.length > 0; }); // set the initial tag on all fields for (let fieldIndex in this.fieldsMeta) { let field = this.fieldsMeta[fieldIndex]; field.tag = `{${field.name}}`; field.translatedLabel = app.lang.get(field.vname, this.currentModule); // if the label cannot be translated then just use the field name field.translatedLabel = field.translatedLabel === field.vname ? field.name : field.translatedLabel; } } else { this.fieldsMeta = []; } this.render(); }, /** * Search the table * * @param {Event} evt */ search: function(evt) { let searchTerm = evt.target.value.toLowerCase(); this.$('.fieldsList tr').filter(_.bind(function(index, element) { if (index === 0) { // don't hide the search input return; } const tr = this.$(element).children()[0]; const label = tr.getAttribute('label'); const hide = label ? label.toLowerCase().indexOf(searchTerm) > -1 : false; this.$(element).toggle(hide); }, this)); }, }) }, "tag-builder-options": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Used to display different options for * different types of fields in the tag builder. * * @class View.Views.Base.DocumentMerges.TagBuilderOptionsView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderOptionsView */ ({ // Tag-builder-options View (base) /** * Field types with special options * @var array */ specialFieldTypes: ['bool', 'date', 'datetime', 'datetimecombo', 'image', 'multienum', 'relate', 'link',], paddingOnlyFieldTypes: ['int', 'currency', 'decimal', 'float', 'phone'], /** * List of predefined date formats. * @var array */ dateFormats: [ 'MM-DD-YYYY', 'MMM-YYYY', 'MMMM-YYYY', 'MM/DD/YYYY', 'MM-Do-YYYY', 'YYYY MMM', 'dddd, MMMM Do YYYY', 'dddd, MMMM Do YYYY, h:mm:ss a', ], /** * @inheritdoc */ events: { 'click [name=back]': 'backToFields', 'change .dm-relate-field': 'setRelateTagName', 'change .dm-tag-attribute': 'applyAttribute', 'click .customOption': 'toggleCustomOption', 'click .barcode': 'enableBarcode', 'change [name=customStateOne]': 'customBoolOptionChange', 'change [name=customStateTwo]': 'customBoolOptionChange', 'click [name=copyTable]': 'copyTable', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.listenTo(this.context, 'tag-builder-options:show', this.showOptions, this); }, /** * @inheritdoc */ render: function(options) { this._super('render', arguments); this.initializeDropDowns(); this.hideCustomOptions(); }, /** * Create select2 components from the available select HTML element */ initializeDropDowns: function() { let dropDownList = this.$('select'); for (let dropDown of dropDownList) { let $dropdown = this.$(dropDown); let placeholder = $dropdown.attr('placeholder'); let type = $dropdown.attr('multiple'); if (type === 'multiple') { $dropdown.select2({ allowClear: true, placeholder: placeholder, containerCssClass: 'select2-choices-pills-close', closeOnSelect: false, width: '100%', }); } else { $dropdown.select2({ allowClear: true, placeholder: placeholder, width: '100%', }); } } }, /** * Shows the options to add to the tags. * @param {field} field */ showOptions: function(field) { this.type = field.type; this.tableAssistant = {}; if (this.paddingOnlyFieldTypes.includes(this.type)) { this.paddingOnly = true; } if (!this.specialFieldTypes.includes(this.type)) { this.type = 'string'; this.barcode = true; } if (this.type === 'relate') { this.relateModule = field.module; this.relateFields = app.metadata.getModule(this.relateModule).fields; } if (this.type === 'link') { this.type = 'collection'; let relationship = app.metadata.getRelationship(field.relationship); this.collectionModule = relationship.lhs_module !== field.currentModule ? relationship.lhs_module : relationship.rhs_module; this.collectionFields = app.metadata.getModule(this.collectionModule).fields; } this.initTag(); this.tag.setName(field.name).setAttributes({}); let tagValue = this.tag.compile().getTagValue(); this.preview = tagValue; this.render(); }, /** * Initializes the tag. */ initTag: function() { let type = this.type === 'collection' ? 'collection' : 'base'; let tagBuilderFactory = new app.utils.DocumentMerge.TagBuilderFactory(); let tagBuilder = tagBuilderFactory.getTagBuilder(type); let tag = tagBuilder.newTag().get(); this.tag = tag; }, /** * Returns from the attributes view. */ backToFields: function() { this.paddingOnly = false; this.context.trigger('tag-builder-options:hide'); }, /** * Applies the attribute to the current tag. * @param {Event} evt */ applyAttribute: function(evt) { let option; const inputType = evt.target.type; let inputValue = evt.target.value; const inputName = evt.target.name; if (inputType === 'checkbox') { inputValue = evt.target.checked; } if (inputType === 'select-multiple') { // this returns an array if the input is a multiple select inputValue = this.$(evt.target).val(); } if (inputName === 'sort') { let fieldToSortBy = this.$('select.sortByRelationshipFields').val(); let sortDirection = this.$('select[data-action=sortDirection]').val(); inputValue = fieldToSortBy ? fieldToSortBy + ':' + sortDirection : sortDirection; } //last chance to modify the inputValue const forcedInputValue = evt.target.getAttribute('format-value'); inputValue = forcedInputValue || inputValue; if (inputName === 'fields') { this.tag.setFields(inputValue); this.updateTableFields(inputValue); } else { option = {}; option[inputName] = inputValue; this.tag.setAttribute(option); } // if the barcode checkbox was unchecked if (forcedInputValue && inputType === 'checkbox' && !evt.target.checked) { this.tag.removeAttribute(inputName); // we also need to delete all the other barcode attributes this._removeBarcodeAttributes(); } if (!inputValue) { this.tag.removeAttribute(inputName); } this.tag.compile(); this.tableAssistant = this.tag.getCollectionTags(); this.createCopyTable(inputName, inputValue); let tagValue = this.tag.compile().getTagValue(); this.$('.preview').text(tagValue); }, /** * Set the tag name for the relate fields. * @param {Event} evt */ setRelateTagName: function(evt) { let inputValue = this.$(evt.target).val(); let baseName = this.tag.getName().split('.')[0]; this.tag.setName(baseName, inputValue); let tagValue = this.tag.compile().getTagValue(); for (let fieldName in this.relateFields) { let field = this.relateFields[fieldName]; if (field.name === inputValue) { this.currentRelateType = field.type; if (!this.specialFieldTypes.includes(this.currentRelateType)) { this.currentRelateType = 'string'; } field.selected = true; } else { field.selected = false; } } this.preview = tagValue; this.render(); }, /** * Hides custom options. */ hideCustomOptions: function() { const customOptions = this.$('.customOptions'); for (let customOption of customOptions) { this.$(customOption).hide(); } }, /** * Toggles the visibility of the custom option. * @param {Event} evt */ toggleCustomOption: function(evt) { let checkedStatus = evt.currentTarget.checked; let customOption = this.$('.customOptions'); if (checkedStatus) { customOption.show(); this.toggleFormatSelects(true); } else { customOption.hide(); this.toggleFormatSelects(false); this.clearCustomValues(); } }, /** * Clears the custom options values. */ clearCustomValues: function() { let customValues = this.$('.customValue'); customValues.val(''); customValues.trigger('change'); }, /** * When choosing the custom option, we disable the * format selects for date and bool * * @param {bool} status */ toggleFormatSelects(status) { this.$('.boolFormatSelect').attr('disabled', status); this.$('.dateFormatSelect').attr('disabled', status); }, /** * One of the custom bool options is activated * * @param {Event} evt */ customBoolOptionChange: function(evt) { const customOptionOne = this.$('[name=customStateOne]').val() || ''; const customOptionTwo = this.$('[name=customStateTwo]').val() || ''; let option = { 'format': `${customOptionOne}/${customOptionTwo}`, }; this.tag.setAttribute(option); let tagValue = this.tag.compile().getTagValue(); this.$('.preview').text(tagValue); }, /** * If the barcode checkbox is unchecked, * we should be removing the other barcode attributes */ _removeBarcodeAttributes: function() { for (const key in this.tag.attributes) { if (key.includes('barcode')) { this.tag.removeAttribute(key); } } }, /** * Creates the table to be copied. */ createCopyTable: function() { this.$('.dm-table-assistant').replaceWith(app.template.getView( 'tag-builder-options.table', 'DocumentMerges' )(this)); }, /** * Updates the field list for the table. * * @param {Array} list */ updateTableFields: function(list) { this.tag.resetCollectionTableFields(); _.each(this.collectionFields, _.bind(function setCollectionTableFields(field) { if (list.includes(field.name)) { this.tag.setCollectionTableFields(field.name, field.translatedLabel); } }, this)); }, /** * Copies the table, while validating the input * * @param {Event} evt */ copyTable: function(evt) { if (_.isEmpty(this.tableAssistant.fields)) { evt.stopImmediatePropagation(); app.alert.show('no_fields_error', { level: 'error', messages: app.lang.get('LBL_NO_SELECTED_FIELDS', this.module), }); } }, }) }, "tag-builder-formulas": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The formula builder used in tag builder. * * @class View.Views.Base.DocumentMerges.TagBuilderFormulasView * @alias SUGAR.App.view.views.BaseDocumentMergesTagBuilderFormulasView */ ({ // Tag-builder-formulas View (base) /** * The preview string * * @var string */ preview: '', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.initTag(); this.listenTo(this.context, 'change:currentModule', this._render, this); }, initTag: function() { let tagBuilderFactory = new App.utils.DocumentMerge.TagBuilderFactory(); let tagBuilder = tagBuilderFactory.getTagBuilder('directive'); let tag = tagBuilder.newTag().setName('formula').get(); this.tag = tag; }, /** * @inheritdoc */ _render: function(view, selectedModule) { this._super('_render'); this._buildFormulaBuilderField(selectedModule); }, /** * creates a new formula builder field and appends it to the current element * * @param {string} module */ _buildFormulaBuilderField: function(module) { this.formulaField = app.view.createField({ view: this, viewName: 'edit', targetModule: module || this.module, callback: _.bind(this.formulaChanged, this), formula: '', def: { type: 'formula-builder', name: 'formula-builder', }, }); this.formulaField.render(); this.$('.row-fluid.builder').append(this.formulaField.$el); }, /** * Update the preview each time the formula changes * * @param {string} formula */ formulaChanged: function(formula) { this.tag.setAttribute({'value': formula}); let tagValue = this.tag.compile().getTagValue(); this.$('.preview').html(tagValue); this.$('.preview').attr('value', tagValue); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "tag-builder": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The base layout for the tag builder component. * * @class View.Layouts.Base.DocumentMerges.TagBuilderLayout * @alias SUGAR.App.view.layouts.BaseDocumentMergesTagBuilderLayout */ ({ // Tag-builder Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.hideApplicationHeaderAndSidebar(); this.listenTo(this.context, 'change:currentModule', this.hideOptions, this); this.listenTo(this.context, 'tag-builder-options:show', this.showOptions, this); this.listenTo(this.context, 'tag-builder-options:hide', this.hideOptions, this); }, /** * Hide application header and sidebar nav */ hideApplicationHeaderAndSidebar: function() { $('.navbar').remove(); $('#sidebar-nav').remove(); $('#content').css({ 'top': '0px', 'left': '0px', 'height': '100%', 'overflow-x': 'hidden' }); }, /** * Whenever the module changes, hide the options * * @param {app.Context} context * @param {string} module */ hideOptions: function(context, module) { let tabs = this.getComponent('tag-builder-tabs'); tabs.getComponent('tag-builder-options').hide(); tabs.getComponent('tag-builder-relationships').show(); tabs.getComponent('tag-builder-fields').show(); }, /** * Show field options * * @param {app.Context} context * @param {string} module */ showOptions: function(context, module) { let tabs = this.getComponent('tag-builder-tabs'); tabs.getComponent('tag-builder-fields').hide(); tabs.getComponent('tag-builder-relationships').hide(); tabs.getComponent('tag-builder-options').show(); } }) }, "sidebar-merge-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DocumentMerges.SidebarMergeWidgetLayout * @alias SUGAR.App.view.layouts.BaseDocumentMergesSidebarMergeWidgetLayout * @extends View.Layout */ ({ // Sidebar-merge-widget Layout (base) /** * @inheritdoc */ bindDataChange: function() { this.listenTo(this.layout, 'reload', this.reload); this.listenTo(app.events, 'document:merge', this._mergeDocument, this); }, /** * Reload the list of merges */ reload: function() { this.getComponent('merge-widget-list').loadData(); this.render(); }, /** * Merge a document template * * @param {Object} options */ _mergeDocument: function(options) { const recordId = options.currentRecordId; const recordModule = options.currentRecordModule; const templateId = options.templateId; const templateName = options.templateName; const isPdf = options.isPdf; const requestType = 'read'; const apiPath = 'DocumentTemplates'; const requestMeta = { fields: [ 'name', 'file_ext', 'use_revisions', ], }; const apiCallbacks = { success: _.bind(function createTemplate(result) { const fileExt = result.file_ext; const useRevision = result.use_revisions; const mergeType = this._getMergeType(fileExt, isPdf); const mergeOptions = { recordId, recordModule, templateId, templateName, useRevision, mergeType, }; this._startDocumentMerge(mergeOptions); }, this) }; const apiUrl = app.api.buildURL(apiPath, templateId, null, requestMeta); app.api.call(requestType, apiUrl, null, null, apiCallbacks); }, /** * Start document merging * * @param {Object} payload */ _startDocumentMerge: function(payload) { const requestType = 'create'; const apiPath = 'DocumentMerge'; const apiPathDocumentType = 'merge'; const apiCallbacks = { success: function createTemplate(documentMergeId) { //open widget in order to show the currently merging document app.events.trigger('document_merge:show_widget'); //start polling for changes on the merge request app.events.trigger('document_merge:poll_merge', documentMergeId); }, error: function(errorMessage) { app.alert.show('merge_error', { level: 'error', messages: errorMessage, }); } }; const apiUrl = app.api.buildURL(apiPath, apiPathDocumentType); app.api.call(requestType, apiUrl, payload, null, apiCallbacks); }, /** * Sets the correct merge type based on the template extension * * @param {string} extension file extension * @param {bool} isPdf check if the document should be converted to pdf * @private * * @return {string} Merge type. */ _getMergeType: function(extension, isPdf) { switch (extension) { case 'pptx': if (isPdf) { return 'presentation_convert'; } return 'presentation'; case 'xlsx': if (isPdf) { return 'excel_convert'; } return 'excel'; default: if (isPdf) { return 'convert'; } return 'merge'; } }, /** * Triggers the parent layout to reposition the widget */ reposition: function() { this.layout.trigger('reposition'); } }) }, "tag-builder-tabs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The layout for the tag builder tabs component. * * @class View.Layouts.Base.DocumentMerges.TagBuilderTabsLayout * @alias SUGAR.App.view.layouts.BaseDocumentMergesTagBuilderTabsLayout */ ({ // Tag-builder-tabs Layout (base) /** * @inheritdoc */ events: { 'click [data-toggle=tab]': 'switchTab', }, /** * @inheritdoc */ render: function() { this._super('render'); this._renderTooltips(); }, /** * Switch Tabs * * @param {Event} evt */ switchTab: function(evt) { evt.preventDefault(); const dataTarget = evt.target.dataset.target; this.$('.tab-content .tab-pane').hide(); this.$('.tab-content #' + dataTarget).show(); }, /** * Render tooltips associated to this layout */ _renderTooltips: function() { this.$('[rel=tooltip]').tooltip(); }, }) }, "merge-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The base layout for the document merge widget component. * * @class View.Layouts.Base.DocumentMerges.MergeWidgetLayout * @alias SUGAR.App.view.layouts.BaseDocumentMergesMergeWidgetLayout * @extends View.Layouts.Base.HelpLayout */ ({ // Merge-widget Layout (base) extendsFrom: 'HelpLayout', /** * @property {string} */ _module: 'DocumentMerges', /** * Use this if the popover is not fully initialized */ _popoverDefaultWidth: 400, /** * Leave 25px of space between rhs edge of popover and the screen. */ _popoverLeftOffset: 25, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); /** * The internal state of this layout. * By default this layout is closed ({@link #toggle} will call render). * * FIXME TY-1798/TY-1800 This is needed due to the bad popover plugin. * * @type {boolean} * @private */ this._isOpen = false; /** * This is the Help button in the footer. * Needed to render the modal by calling `popover` on the button. * * @type {jQuery} */ this.button = options.button; app.events.on('document:merge', this._mergeDocument, this); $(window).on('resize.' + this.cid, _.bind(function() { this.reposition(); }, this)); }, /** * Merge a document template * * @param {Object} options */ _mergeDocument: function(options) { const recordId = options.currentRecordId; const recordModule = options.currentRecordModule; const templateId = options.templateId; const templateName = options.templateName; const isPdf = options.isPdf; const requestType = 'read'; const apiPath = 'DocumentTemplates'; const requestMeta = { fields: [ 'name', 'file_ext', 'use_revisions', ], }; const apiCallbacks = { success: _.bind(function createTemplate(result) { const fileExt = result.file_ext; const useRevision = result.use_revisions; const mergeType = this._getMergeType(fileExt, isPdf); const mergeOptions = { recordId, recordModule, templateId, templateName, useRevision, mergeType, }; this._startDocumentMerge(mergeOptions); }, this) }; const apiUrl = app.api.buildURL(apiPath, templateId, null, requestMeta); app.api.call(requestType, apiUrl, null, null, apiCallbacks); }, /** * Start document merging * * @param {Object} payload */ _startDocumentMerge: function(payload) { const requestType = 'create'; const apiPath = 'DocumentMerge'; const apiPathDocumentType = 'merge'; const apiCallbacks = { success: function createTemplate(documentMergeId) { //open widget in order to show the currently merging document app.events.trigger('document_merge:show_widget'); //start polling for changes on the merge request app.events.trigger('document_merge:poll_merge', documentMergeId); }, error: function(errorMessage) { app.alert.show('merge_error', { level: 'error', messages: errorMessage, }); } }; const apiUrl = app.api.buildURL(apiPath, apiPathDocumentType); app.api.call(requestType, apiUrl, payload, null, apiCallbacks); }, /** * Sets the correct merge type based on the template extension * * @param {string} extension file extension * @param {bool} isPdf check if the document should be converted to pdf * @private * * @return {string} Merge type. */ _getMergeType: function(extension, isPdf) { switch (extension) { case 'pptx': if (isPdf) { return 'presentation_convert'; } return 'presentation'; case 'xlsx': if (isPdf) { return 'excel_convert'; } return 'excel'; default: if (isPdf) { return 'convert'; } return 'merge'; } }, /** * Initializes the popover plugin for the button given. * * @param {jQuery} button The jQuery button. * @private */ _initPopover: function(button) { button.popover({ title: this._getTitle('LBL_DOCUMENT_MERGE_FOOTER'), content: _.bind(function() { return this.$el; }, this), html: true, placement: 'top', template: '<div class="popover footer-modal feedback document-merge-widget" data-modal="document-merge">' + '<div class="arrow"></div><h3 class="popover-title dm-popover-title"></h3>' + '<div class="popover-content"></div></div>' }); }, /** * Fetches the title of the widget modal. * If none exists, returns a default help title. * * @param {string} titleKey The modal title label. * @return {string} The converted title. * @private */ _getTitle: function(titleKey) { var title = app.lang.get(titleKey, this._module, app.controller.context); return title === titleKey ? app.lang.get('LBL_DOCUMENT_MERGES') : title; }, /** * Toggle this view (by re-rendering). * * @param {boolean} [show] `true` to show, `false` to hide, `undefined` * to toggle the current state. */ toggle: function(show) { if (!this.button) { return; } if (_.isUndefined(show)) { this._isOpen = !this._isOpen; } else { this._isOpen = show; } if (this._isOpen) { this.render(); this._initPopover(this.button); this.button.popover('show'); this.bindOutsideClick(); } else { this.button.popover('hide'); } this.trigger(this._isOpen ? 'show' : 'hide', this, this._isOpen); }, /** * Closes the widget modal if event target is outside of the DocumentMerge widget modal. * * @param {Object} evt jQuery event. */ closeOnOutsideClick: function(evt) { if ($(evt.target).closest('.document-merge-widget').length !== 0) { //if click inside the widget do not close return; } if ($(evt.target).closest('.merge-row').length !== 0) { //if click on the widget action buttons return; } if ($(evt.target).closest('[data-modal=document-merge]').length === 0) { //if not click on the button this.toggle(false); } }, /** * Reload all the merges */ reload: function() { this.getComponent('merge-widget-list').loadData(); this.render(); }, /** * Reposition the layout so we can set the top of the layout. */ reposition: function() { const $popoverContainer = this.button.data()['bs.popover']; if (!$popoverContainer) { return; } const $tip = $popoverContainer.tip(); const height = $tip.height(); const top = 0 - height; let left; if (app.lang.direction === 'rtl') { // Leave 25px of space between lhs edge of popover and the screen. left = this._popoverLeftOffset; } else { let popoverWidth = $popoverContainer.width ? $popoverContainer.width() : this._popoverDefaultWidth; // Leave 25px of space between rhs edge of popover and the screen. left = $(window).width() - popoverWidth - this._popoverLeftOffset; } $tip.css({top: top, left: left}); }, _dispose: function() { $(window).off('resize.' + this.cid); this._super('_dispose'); } }) } }} , "datas": {} }, "CloudDrivePaths":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "WorkFlow":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "EAPM":{"fieldTemplates": {} , "views": { "base": { "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.EAPM.CreateView * @alias SUGAR.App.view.views.EAPMCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this._beforeInit(); this._super('initialize', [options]); this._registerEvents(); }, /** * Before init properties handling */ _beforeInit: function() { this.fullAPIList = {}; this.messageListenersToBeRemoved = []; this._getExternApiList(); }, /** * Fetch the external api list */ _getExternApiList: function() { app.alert.show('eapm-loading-list', { level: 'process', title: app.lang.get('LBL_LOADING'), }); const url = app.api.buildURL(this.options.module, 'list'); app.api.call('read', url, null, { success: _.bind(function(data) { if (this.disposed) { return; } this.fullAPIList = data; this.setupFields(); }, this), complete: _.bind(function() { app.alert.dismiss('eapm-loading-list'); }, this), }); }, /** * Register related events */ _registerEvents: function() { this.listenTo(this.model, 'change:application', this.setupFields, this); }, /** * show or hide a field * * @param {string} fieldName * @param {bool} fieldName */ _showHideField: function(fieldName, show) { const field = this.getField(fieldName); if (!field) { return; } const fieldEl = field.getFieldElement(); if (!fieldEl) { return; } const fieldHolder = fieldEl.closest('.record-cell'); show ? fieldHolder.show() : fieldHolder.hide(); }, /** * set if a field is required * * @param {string} fieldName * @param {bool} fieldName */ _setFieldRequired: function(fieldName, required) { const field = this.getField(fieldName); if (!field) { return; } field.def.required = !!required; field.render(); }, /** * Setup the fields */ setupFields: function() { if (_.isEmpty(this.fullAPIList)) { return; } const applicationType = this.model.get('application'); const applicationMeta = this.fullAPIList[applicationType]; this._setFieldRequired('application', true); if (!applicationMeta) { //here we don't have an application type selected in the UI const showFields = true; this._showHideField('url', showFields); this._showHideField('name', showFields); this._showHideField('password', showFields); this._setFieldRequired('url', showFields); this._setFieldRequired('name', showFields); this._setFieldRequired('password', showFields); return; } const needsUrl = applicationMeta.needsUrl ? true : false; const isAuthMethPassword = applicationMeta.authMethod === 'password'; this._showHideField('url', needsUrl); this._showHideField('name', isAuthMethPassword); this._showHideField('password', isAuthMethPassword); this._setFieldRequired('url', needsUrl); this._setFieldRequired('name', isAuthMethPassword); this._setFieldRequired('password', isAuthMethPassword); }, /** * Start the authorization * * @inheritdoc */ save: function() { this.disableButtons(); async.waterfall( [ _.bind(this.validateModelWaterfall, this), _.bind(this._startAuthProcess, this), ], _.bind(function(error) { this.enableButtons(); if (error && error.status === 412 && !error.request.metadataRetry) { this.handleMetadataSyncError(error); } }, this), this ); }, /** * Start the oauth process */ _startAuthProcess: function() { const applicationType = this.model.get('application'); const applicationMeta = this.fullAPIList[applicationType]; const isPassword = applicationMeta.authMethod === 'password'; if (isPassword) { //add logic for webex } else { this._startOauthProcess(); } }, /** * Start the oauth logging process * */ _startOauthProcess: function() { const applicationType = this.model.get('application'); app.alert.show('eapm-loading-client', { level: 'process', title: app.lang.get('LBL_LOADING'), }); const url = app.api.buildURL('EAPM', 'auth', null, {application: applicationType}); app.api.call('read', url, null, { success: _.bind(function(data) { if (this.disposed) { return; } if (!_.has(data, 'auth_url')) { this._showFailedAlert('LBL_AUTH_UNSUPPORTED'); this.enableButtons(); return; } this._startAuth2(data); }, this), error: _.bind(function(data) { app.alert.dismiss('eapm-loading-client'); this._showFailedAlert(); this.enableButtons(); }, this), complete: _.bind(function() { app.alert.dismiss('eapm-loading-client'); }, this), }); }, /** * Start the oauth process * * @param {Object} data */ _startAuth2: function(data) { const authorizationListener = _.bind(function(e) { if (this) { this.handleAuthorizeComplete(e); } window.removeEventListener('message', authorizationListener); }, this); window.addEventListener('message', authorizationListener); this.messageListenersToBeRemoved.push(authorizationListener); this._openAuthWindow(data); }, /** * Open the auth window * * @param {Object} data */ _openAuthWindow: function(data) { const urlData = { height: 600, width: 600, left: (screen.width - 600) / 2, top: (screen.height - 600) / 4, resizable: 1, }; const urlParams = Object.entries(urlData).map(([key, value]) => `${key}=${value}`).join(','); const submitWindow = window.open('/', '_blank', urlParams); const dataKey = 'auth_url'; submitWindow.location.href = 'about:blank'; submitWindow.location.href = data[dataKey]; //we have to re-enable the cancel/connect buttons. //since the opened window will be on a different origin //we can't add a listener like beforeunload //so we have to use an workaround here const checkChildWindow = setInterval(_.bind(function() { if (submitWindow.closed) { clearInterval(checkChildWindow); if (this.disposed) { return; } this.enableButtons(); } }, this), 500); }, /** * Handles the oauth completion event. * Note that the EAPM bean has already been saved at this point. * * @param {Object} e * @param {string} smtpType * @return {boolean} True if success, otherwise false */ handleAuthorizeComplete: function(e) { if (!e) { this._showFailedAlert(); this.enableButtons(); return; } if (!e.data) { this._showFailedAlert(); this.enableButtons(); return; } try { const response = JSON.parse(e.data); if (!response || !response.result || response.result !== true) { this._showFailedAlert(); this.enableButtons(); return; } this._showSuccessAlert(); this._closeDrawer(); } catch (e) { this._showFailedAlert(); this.enableButtons(); return; } }, /** * Display a success alery * */ _showSuccessAlert: function() { app.alert.show('success', { level: 'success', messages: app.lang.get('LBL_CONNECTED', this.module), autoClose: true }); }, /** * Display a failed message * * @param {string} label */ _showFailedAlert: function(label = 'LBL_AUTH_ERROR') { app.alert.show('error', { level: 'error', messages: app.lang.get(label, this.module) }); }, /** * Close the drawer */ _closeDrawer: function() { if (this.closestComponent('drawer')) { app.drawer.close(this.context, this.model); } else { app.navigate(this.context, this.model); } }, /** * @inheritdoc */ _dispose: function() { this._super('_dispose'); _.each(this.messageListenersToBeRemoved, function(listener) { window.removeEventListener('message', listener); }, this); this.messageListenersToBeRemoved = []; this.stopListening(); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Worksheet":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Users":{"fieldTemplates": { "base": { "downloads": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.DownloadsField * @alias SUGAR.App.view.fields.BaseUsersDownloadsField * @extends View.Fields.Base.BaseField */ ({ // Downloads FieldTemplate (base) extendsFrom: 'BaseField', showNoData: false, /** * @inheritdoc * @param options */ initialize: function(options) { this._super('initialize', [options]); this.fetchPlugins(); }, /** * Retrieves a list of available plugins from the server and * renders them in the field */ fetchPlugins: function() { this.loading = true; let pluginsUrl = app.api.buildURL('me/plugins'); app.api.call('read', pluginsUrl, null, { success: (result) => { this._pluginCategories = result; }, complete: () => { this.loading = false; this.render(); } }); } }) }, "email-credentials": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.EmailCredentialsField * @alias SUGAR.App.view.fields.BaseUsersEmailCredentialsField * @extends View.Fields.Base.BaseField */ ({ // Email-credentials FieldTemplate (base) extendsFrom: 'BaseField', events: { 'click button.change-password': '_changePasswordClicked', 'click button.authorize': '_authorizeClicked', 'click button.test-email': '_testEmailClicked', 'click button.test-email-cancel': '_cancelTestEmailClicked', 'click button.test-email-send': '_sendTestEmailClicked', }, /** * Stores dynamic window message listener to clear it on dispose */ messageListeners: [], /** * Stores Authorized Email Account status */ status: undefined, /** * Stores Connected status */ isConnected: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.oauth2Types = { google_oauth2: { application: 'GoogleEmail', dataSource: 'googleEmailRedirect' }, exchange_online: { application: 'MicrosoftEmail', dataSource: 'microsoftEmailRedirect' } }; // Load connector information this.connectorsLoaded = false; this._loadOauth2TypeInformation(() => { this.connectorsLoaded = true; this.render(); }); }, /** * @inheritdoc * */ bindDataChange: function() { this._super('bindDataChange'); if (this.model) { this.listenTo(this.model, 'change:mail_credentials', () => { let mailCredentials = this.model.get('mail_credentials') || {}; let smtpType = mailCredentials.mail_smtptype || ''; let eapmId = mailCredentials.eapm_id || ''; this._testConnection(smtpType, eapmId); }); this.listenTo(this.view.context, 'change:connectionStatus', function() { this.status = this.view.context.get('connectionStatus'); this.isConnected = this.view.context.get('isConnected'); this.render(); }, this); } }, /** * @inheritdoc */ bindDomChange: function() { if (!(this.model instanceof Backbone.Model)) { return; } let el = this.$el.find(this.fieldTag); el.on('change', (event) => { let $target = $(event.currentTarget); let changedName = $target.data('name'); let changedValue = $target.val(); let modelValue = this.model.get(this.name) || {}; modelValue[changedName] = changedValue; this.model.set(this.name, modelValue); }); }, /** * Sets the change_smtp_password flag when the "Change Password" button * is clicked * * @private */ _changePasswordClicked: function() { let modelValue = this.model.get(this.name) || {}; modelValue.mail_smtppass_change = true; this.model.set(this.name, modelValue); this.render(); }, /** * Initializes the authorization information for the OAuth2 tabs * * @param {Function} callback the callback to run after information is fetched * @private */ _loadOauth2TypeInformation: function(callback) { _.each(this.oauth2Types, function(properties, smtpType) { if (!_.isUndefined(properties.auth_url)) { return; } var url = app.api.buildURL('EAPM', 'auth', {}, {application: properties.application}); var callbacks = { success: (data) => { if (data) { this.oauth2Types[smtpType].auth_url = data.auth_url || false; } }, error: () => { this.oauth2Types[smtpType].auth_url = false; }, complete: () => { callback.call(this); } }; var options = { showAlerts: false, bulk: 'loadOauth2TypeInformation', }; app.api.call('read', url, {}, callbacks, options); }, this); app.api.triggerBulkCall('loadOauth2TypeInformation'); }, /** * Handles auth when the Authorize button is clicked. */ _authorizeClicked: function() { let smtpType = this.credentials.mail_smtptype; if (this.oauth2Types[smtpType] && this.oauth2Types[smtpType].auth_url) { let authorizationListener = (message) => { if (this) { this.handleAuthorizeComplete(message, smtpType); } window.removeEventListener('message', authorizationListener); }; window.addEventListener('message', authorizationListener); this.messageListeners.push(authorizationListener); let width = 600; let height = 600; let left = (screen.width - width) / 2; let top = (screen.height - height) / 4; window.open( this.oauth2Types[smtpType].auth_url, '_blank', `width=${width},height=${height},left=${left},top=${top},resizable=1` ); } }, /** * Handles the oauth completion event. * Note that the EAPM bean has already been saved at this point. * * @param {Object} message the message data received from the auth window * @param {string} smtpType the SMTP type authorized with * @return {boolean} True if success, otherwise false */ handleAuthorizeComplete: function(message, smtpType) { let data = JSON.parse(message.data) || {}; if (!data.dataSource || !this.oauth2Types[smtpType] || data.dataSource !== this.oauth2Types[smtpType].dataSource) { return false; } if (data.eapmId && data.emailAddress && data.userName) { let modelValue = this.model.get(this.name) || {}; modelValue.eapm_id = data.eapmId; modelValue.authorized_account = data.emailAddress; modelValue.mail_smtpuser = data.userName; } else { app.alert.show('error', { level: 'error', messages: app.lang.get('LBL_EMAIL_AUTH_FAILURE', this.module) }); } this.render(); return true; }, /** * Shows the test email modal when the Send Test Email button is clicked * * @private */ _testEmailClicked: function() { this.$('.test-email-dialog').removeClass('hide'); }, /** * Hides the test email modal when the Cancel button in it is clicked * * @private */ _cancelTestEmailClicked: function() { this.$('.test-email-dialog').addClass('hide'); }, /** * Handles when the Send button is clicked in the test email modal * * @private */ _sendTestEmailClicked: function() { // Hide the modal this.$('.test-email-dialog').addClass('hide'); // Build the arguments for the send test email API let settings = this.model.get(this.name); let args = { user_id: this.model.get('id'), name: this.model.get('full_name'), from_address: this._getFromAddress(), to_address: this.$('input.test-address').val(), mail_smtpuser: settings.mail_smtpuser, mail_smtppass: settings.mail_smtppass, eapm_id: settings.eapm_id, }; // Call the send test email API app.alert.show('send_test_email_in_progress', { level: 'process', messages: 'LBL_LOADING', autoClose: false }); let url = app.api.buildURL('OutboundEmail/testUserOverride'); app.api.call('create', url, args, { success: (result) => { app.alert.show('send_test_email_results', { level: result.status ? 'success' : 'error', messages: result.status ? 'LBL_EMAIL_TEST_NOTIFICATION_SENT' : result.errorMessage, autoClose: true, autoCloseDelay: 5000 }); }, complete: () => { app.alert.dismiss('send_test_email_in_progress'); } }); }, /** * Gets the from address for sending a test email (based on the User's * primary email address) if it is available * * @return {string|null} the User's primary email address if available, or null * @private */ _getFromAddress: function() { let emailAddrs = this.model.get('email'); if (emailAddrs) { let fromAddr = _.findWhere(emailAddrs, {primary_address: true}); if (fromAddr) { return fromAddr.email_address; } } return null; }, /** * @inheritdoc */ _render: function() { this.credentials = this.model.get(this.name); this._super('_render'); }, /** * Gets testConnection's result */ _testConnection: function(smtpType, eapmId) { if (!this.oauth2Types[smtpType] || !eapmId) { return; } let url = app.api.buildURL('EAPM', 'test', {}, { application: this.oauth2Types[smtpType].application, eapm_id: eapmId }); let callback = { success: (data) => { if (!data.isConnected) { app.alert.show('info-checking-mail-connection', { level: 'info', title: app.lang.get('LBL_ALERT_TITLE_NOTICE'), messages: [app.lang.get('LBL_EMAIL_RE_AUTHORIZE_ACCOUNT')] }); } this.view.context.set({ connectionStatus: app.lang.get(this._getStatus(eapmId, data.isConnected)), isConnected: data.isConnected }); }, error: (error) => { app.alert.show('error-checking-mail-connection', { level: 'error', messages: error }); } }; app.api.call('read', url, {}, callback); }, /** * Gets current status */ _getStatus: (eapmId, isConnected) => (eapmId && isConnected) ? 'LBL_MAIL_AUTHORIZED_ACCOUNT' : 'LBL_MAIL_NOT_AUTHORIZED_ACCOUNT', /** * @inheritdoc */ _dispose: function() { _.each(this.messageListeners, function(listener) { window.removeEventListener('message', listener); }, this); this.messageListeners = []; this._super('_dispose'); } }) }, "email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.EmailField * @alias SUGAR.App.view.fields.BaseUsersEmailField * @extends View.Fields.Base.EmailField */ ({ // Email FieldTemplate (base) extendsFrom: 'EmailField', /** * Get HTML for email input field. * override the parent method * @param {Object} email * @return {Object} * @private */ _buildEmailFieldHtml: function(email) { let editEmailFieldTemplate = app.template.getField( 'email', 'edit-email-field', 'Users' ); let emails = this.model.get(this.name); let index = _.indexOf(emails, email); //Get additional params (there are none in the parent method) let additionalParams = this._getAdditionalParams(email); return editEmailFieldTemplate({ max_length: this.def.len, index: index === -1 ? emails.length - 1 : index, email_address: email.email_address, primary_address: email.primary_address, opt_out: email.opt_out, invalid_email: email.invalid_email, reply_to_address: email.reply_to_address, disabledPrimary: additionalParams.disabledPrimary, disabled: additionalParams.disabled }); }, /** * Get additional params about disability of email fields * * @param {Object} email * @private */ _getAdditionalParams: function(email) { return { disabledPrimary: app.config.idmModeEnabled && !this.model.get('is_group') && !this.model.get('portal_only'), disabled: app.config.idmModeEnabled && email.primary_address && !this.model.get('is_group') && !this.model.get('portal_only') }; } }) }, "user-type": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.UserTypeField * @alias SUGAR.App.view.fields.BaseUsersUserTypeField * @extends View.Fields.Base.EnumField */ ({ // User-type FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.listenTo(this.model, `change:${this.name}`, this.render); }, /** * @inheritdoc * * Turns boolean model value into number template value */ format: function(value) { return +value; }, /** * @inheritdoc * * Turns number template value into boolean model value */ unformat: function(value) { return !!parseInt(value); }, /** * @inheritdoc */ _render: function() { this._super('_render'); // Add the info label for the user type let optionInfoDef = this.def.optionInfo || {}; let optionInfo = optionInfoDef[+this.model.get(this.name)] || ''; this.$el.append(app.lang.get(optionInfo, this.module)); }, /** * @inheritdoc */ _loadTemplate: function() { this.type = 'enum'; this._super('_loadTemplate'); this.type = this.def.type; }, /** * @inheritdoc */ _dispose: function() { this._super('_dispose'); this.stopListening(); } }) }, "available-modules": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.AvailableModulesField * @alias SUGAR.App.view.fields.BaseUsersAvailableModulesField * @extends View.Fields.Base.BaseField */ ({ // Available-modules FieldTemplate (base) extendsFrom: 'BaseField', events: { 'click .sicon-remove': '_removeClicked' }, /** * @inheritdoc */ bindDataChange: function() { this.listenTo(this.model, 'change:number_pinned_modules', this._updateDivider); this.listenTo(this.model, `change:${this.name}`, this.render); }, /** * @inheritdoc */ format: function(value) { return { display: value && value.display || [], hide: value && value.hide || [] }; }, /** * @inheritdoc */ unformat: function() { let newValue = app.utils.deepCopy(this.model.get(this.name)) || {}; let lists = this.$el.find('.sortable-list'); _.each(lists, function(list) { let $list = $(list); let listName = $list.data('name'); newValue[listName] = []; let items = $list.find('li:not(.ui-sortable-helper)'); _.each(items, function(item) { let $item = $(item); let moduleName = $item.data('name'); newValue[listName].push(moduleName); }, this); }, this); return newValue; }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._updateDivider(); if (this.action === 'edit') { this._initSortable(); } }, /** * Initializes the sortability of the item lists * * @private */ _initSortable: function() { this.$('.sortable-list').sortable({ connectWith: '.sortable-list', appendTo: 'body', classes: { 'ui-sortable-helper': 'list-none border-none' }, change: () => { this._updateDivider(); }, update: () => { this._updateDivider(); }, stop: (event, ui) => { this._updateRemoveButtonVisibility(ui.item); this._updateDivider(); this._updateModel(); } }); }, /** * Toggles the visiblity of a list item's remove button based on the list * it is contained in * * @param {jQuery} $item the jQuery object representing the list item * @private */ _updateRemoveButtonVisibility: function($item) { $item.find('i.sicon-remove').toggleClass('hide', $item.closest('ul').data('name') === 'hide'); }, /** * Updates which item in the list shows a bottom border to mark where * the pinned items end * * @private */ _updateDivider: function() { let borderClass = 'border-b-2'; this.$el.find(`li.${borderClass}`).removeClass(borderClass); let numberPinned = this.model.get('number_pinned_modules'); if (_.isNumber(numberPinned)) { let displayList = this._getListElementByName('display'); if (displayList) { $(displayList).find(`li:not(.ui-sortable-helper):nth-child(${numberPinned})`).addClass(borderClass); } } }, /** * Gets the DOM element for one of the lists by its name * * @param {string} listName the name of the list * @return {Element|undefined} the list element, or undefined it does not exist * @private */ _getListElementByName: function(listName) { let lists = this.$el.find('.sortable-list'); return _.find(lists, function(list) { let $list = $(list); return $list.data('name') === listName; }); }, /** * Updates the model's stored value based on the current state of the field * * @private */ _updateModel: function() { this.model.set(this.name, this.unformat()); }, /** * Handles when the remove button is clicked on a list item * * @param event the Javascript click event * @private */ _removeClicked: function(event) { // Get the list item that was clicked let $item = $(event.target).closest('li'); // Move the list item to the hide column let targetList = this._getListElementByName('hide'); if (targetList) { $item.detach().appendTo($(targetList)); this._updateRemoveButtonVisibility($item); this._updateDivider(); this._updateModel(); } }, /** * @inheritdoc */ _dispose: function() { this._super('_dispose'); this.stopListening(); } }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseUsersEditablelistbuttonField * @extends View.Fields.Base.EditablelistbuttonField */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * Flag to check if we should navigate to Reassign User Records page * {boolean} */ triggerReassignUserRecords: false, /** * @inheritdoc */ saveClicked: function(evt) { let changedAttributes = this.model.changedAttributes(); if (changedAttributes && changedAttributes.status && changedAttributes.status === 'Inactive') { this.triggerReassignUserRecords = true; } this._super('saveClicked', [evt]); }, /** * @inheritdoc */ onSaveComplete: function() { this._super('onSaveComplete'); if (this.triggerReassignUserRecords) { this.triggerReassignUserRecords = false; app.alert.show('reassign_records', { level: 'confirmation', messages: app.lang.get('LBL_REASS_CONFIRM_REASSIGN', this.module), onConfirm: _.bind(function() { let url = app.bwc.buildRoute('Users', this.model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, this), onCancel: function() { return; }, }, this); } }, /** * @inheritdoc */ _getNoAccessErrorMessage: function(error) { if (error.code === 'license_seats_needed' && _.isString(error.message)) { return error.message; } return this._super('_getNoAccessErrorMessage', [error]); } }) }, "portal-change-password": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Widget for changing a password of portal users. * * It does not require old password confirmation. * * @class View.Fields.Base.Users.PortalChangePasswordField * @extends View.Fields.Base.ChangePasswordField */ ({ // Portal-change-password FieldTemplate (base) extendsFrom: 'ChangePasswordField' }) }, "role_access": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.RoleAccessField * @alias SUGAR.App.view.fields.BaseUsersRoleAccessField * @extends View.Fields.Base.BaseField */ ({ // Role_access FieldTemplate (base) extendsFrom: 'BaseField', showNoData: false, /** * Array of operations in the access control matrix */ names: [], /** * Access control matrix data */ categories: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._getAccessData(); }, /** * Get the users role access data defined by the admin in Roles * * @private */ _getAccessData: function() { const url = app.api.buildURL('Users', 'userAccess', {id: this.model.id}); this.loading = true; app.api.call('GET', url, null, { success: (results) => { this.names = results.names; this.categories = results.categories; }, error: () => { app.logger.error('Unable to fetch the user role data.'); }, complete: () => { this.loading = false; this.render(); } }); } }) }, "hybrid-select": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Users.HybridSelectField * @alias SUGAR.App.view.fields.BaseUsersHybridSelectField * @extends View.Fields.Base.Field */ ({ // Hybrid-select FieldTemplate (base) /** * @inheritdoc */ events: { 'change select': 'updateSelect', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.selectModule = options.def.select_module; this.context.on('change:assigned_user', this.changeAssignedFilter, this); /** * select items */ this.items = [{id: '', text: '', element: new Option()}]; /** * selected items */ this.massCollection = app.data.createBeanCollection(this.selectModule); }, /** * @inheritdoc */ _render: function() { this._super('_render', arguments); this.setSelect(this.items); }, /** * setup the select as select2 */ setSelect: function() { const select = this.$('.hybrid select'); const placeholder = this.options.def.placeholder; select.select2({ allowClear: true, containerCssClass: 'select2-choices-pills-close', placeholder: app.lang.getModString(placeholder, this.module), }); // set empty options this.$('select').val(null).trigger('change'); //add options for (const item of this.items) { this.$('select').append(item.element); } this.$('select').trigger('change'); select.on('select2-opening', _.bind(this.openDrawer, this)); }, /** * Change event for assigned filter * * @param {Event} evt */ changeAssignedFilter: function(evt) { let filterPopulate = { assigned_user_id: this.context.get('assigned_user'), }; let initialFilterLabel = 'LBL_FILTER_UTILS_SELECT'; // if the module is Filters, we should filter by 'created by' if (this.selectModule === 'Filters') { filterPopulate = { created_by: this.context.get('assigned_user'), }; initialFilterLabel = 'LBL_FILTER_UTILS_CREATED'; } this.filterOptions = new app.utils.FilterOptions() .config({ initial_filter: 'utils-select', initial_filter_label: initialFilterLabel, filter_populate: filterPopulate, }).format(); }, /** * Open multi select drawer * * @param {Event} evt */ openDrawer: function(evt) { evt.preventDefault(); evt.stopPropagation(); /** * Remove focus from input because we don't want accidental typing events triggered */ this.$('.select2-search input, :focus,input').prop('focus', false).blur(); if (this.context.get('assigned_user') && _.isUndefined(this.filterOptions)) { this.changeAssignedFilter(); } let context = { module: this.selectModule, isMultiSelect: true, mass_collection: this.massCollection, }; if (this.filterOptions) { _.extend(context, {filterOptions: this.filterOptions}); } // open the selection drawer app.drawer.open({ context: context, layout: 'multi-selection-list', }, _.bind(function(data) { // give focus back to the input this.$('.select2-search input, :focus,input').prop('focus', true).blur(); if (_.isArray(data) && !_.isEmpty(data)) { const hasName = _.first(data).name; if (hasName) { this.setFieldValue(data); } else { // make sure we have sth to display into the field // so we need to retrieve records names this.enhanceData(data, this.setFieldValue); } } }, this)); }, /** * Call api in order to retrieve the name field for each selected record * * @param {Array} data * @param {Function} callback */ enhanceData: function(data, callback) { app.alert.show('utils-loading', { level: 'process', title: app.lang.getModString('LBL_LOADING_ITEMS', this.module), }); let fields = ['id', 'name',]; if (this.selectModule === 'Dashboards') { fields.push('dashboard_module'); } const ids = _.map(data, function mapFilter(item) { return item.id; }); const filter = [{id: {$in: ids}}]; var url = app.api.buildURL(this.selectModule, 'filter'); app.api.call('create', url, { fields: fields, filter: filter, max_num: -1, }, { success: _.bind(callback, this), error: function(error) { app.alert.show('data-error', { level: 'error', messages: app.lange.getModString('LBL_DATA_NOT_RETRIEVED', this.module), }); }, complete: function() { app.alert.dismiss('utils-loading'); } }); }, /** * Set data in order to be displayed in the field input * * @param {Array} data */ setFieldValue: function(data) { data = data.records || data; // set the selected items this.massCollection = new app.data.createBeanCollection(this.selectModule); for (const item of data) { this.massCollection.push(new app.data.createBean(this.selectModule, item)); } // this will be used at select2 init this.items = _.map(data, _.bind(function(item) { let itemName = item.name; if (this.massCollection.module === 'Dashboards') { itemName = app.lang.get(item.name, item.dashboard_module); } return { id: item.id, text: itemName, element: new Option(itemName, item.id, true, true) }; }, this)); this.render(); }, /** * whenever we change data on select, update the massCollection also * * @param {Event} evt */ updateSelect: function(evt) { const removed = evt.removed; if (!removed) { return; } _.each(this.massCollection.models, _.bind(function massCollectionRemove(item) { if (item.id === removed.id) { this.massCollection.remove(item); } }, this)); }, /** * Retrieves the selected data and returns only their ids */ getSelected: function() { return _.map(this.massCollection.models, function mapItems(item) { return item.get('id'); }); }, }) } }} , "views": { "base": { "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.MassupdateView * @alias SUGAR.App.view.views.BaseUsersMassupdateView * @extends View.Views.Base.MassupdateView */ ({ // Massupdate View (base) extendsFrom: 'MassupdateView', /** * @inheritdoc * * Extends the parent function to also check for fields that are not * editable while in IDM mode */ checkFieldAvailability: function(field) { let available = this._super('checkFieldAvailability', [field]); let idmProtected = app.config.idmModeEnabled && field.idm_mode_disabled; let isPreferenceField = field.user_preference; return available && !idmProtected && !isPreferenceField; }, /** * @override */ getDeleteMessage: function() { return app.lang.get('LBL_DELETE_USER_CONFIRM', this.module); }, /** * @override */ deleteModelsSuccessCallback: function(data, response, options) { this.layout.trigger('list:records:deleted', this.lastSelectedModels); this.lastSelectedModels = null; if (options.status === 'done') { this.layout.context.reloadData({showAlerts: false}); } else if (options.status === 'queued') { app.alert.show('jobqueue_notice', { level: 'success', title: app.lang.get('LBL_MASS_UPDATE_JOB_QUEUED'), autoClose: true }); } this._modelsToDelete = null; let url = app.bwc.buildRoute('Users', null, 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, }) }, "copy-content-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.CopyContentButtonsView * @alias SUGAR.App.view.layouts.BaseUsersCopyContentButtonsView * @extends View.Layouts.Base.View */ ({ // Copy-content-buttons View (base) /** * @inheritdoc */ events: { 'click [name=copy_button]': 'copy', 'click [name=clear_button]': 'clear', 'click [name=cancel_button]': 'cancel', }, /** * Cloning name->label map * Used in alerts */ cloningTypes: { 'CloneFavoriteReports': 'LBL_FAVORITE_REPORTS', 'CloneSugarEmailClient': 'LBL_SUGAR_EMAIL_CLIENT', 'CloneScheduledReporting': 'LBL_SCHEDULED_REPORTING', 'CloneNotifyOnAssignment': 'LBL_NOTIFY_ON_ASSIGNMENT', 'CloneRemindersOptions': 'LBL_REMINDER_OPTIONS', 'CloneDefaultTeams': 'LBL_DEFAULT_TEAMS', 'CloneNavigationBar': 'LBL_NAVBAR_SELECTION', 'CloneDashboards': 'LBL_DASHBOARDS_UTILS', 'CopyDashboards': 'LBL_EXISTING_DASHBOARD', 'CloneFilters': 'LBL_FILTERS', 'CopyFilters': 'LBL_EXISTING_FILTERS', 'CloneUserSettings': 'LBL_USER_LOCALE', }, /** * Go back to the users list */ cancel: function() { app.router.navigate('#Users', {trigger: true}); }, /** * Re-renders the layout so all inputs are cleared */ clear: function() { this.layout.render(); if (this.layout.getComponent('copy-content-items') instanceof app.view.View) { this.layout.getComponent('copy-content-items').resetSection(); } }, /** * Copy settings to users */ copy: function() { const itemsView = this.layout.getComponent('copy-content-items'); const usersView = this.layout.getComponent('copy-content-users'); const destinationUsers = usersView.getField('users_select').getSelected(); const destinationTeams = usersView.getField('teams_select').getSelected(); const destinationRoles = usersView.getField('roles_select').getSelected(); const dashboards = itemsView.getField('dashboards_select').getSelected(); const filters = itemsView.getField('filters_select').getSelected(); // used later in the success message let modules = _.isEmpty(itemsView.getModulesForDashboards()) ? itemsView.getModulesForFilters() : itemsView.getModulesForDashboards(); this.context.set('cloneModules', modules); this.context.set('copyDashboards', dashboards); this.context.set('copyFilters', filters); const selectingSection = itemsView.selectingSection; const section = itemsView.section; const sourceUser = itemsView.sourceUser; switch (section) { case 'user_prefs': const types = itemsView.getSelectedPrefTypes(); let payload = []; for (const type of types) { payload.push({ type: type, sourceUser: sourceUser, destinationUsers: destinationUsers, destinationTeams: destinationTeams, destinationRoles: destinationRoles, }); } this.callCommand(payload); break; case 'dashboards': let dashboardsPayload = [{ sourceUser: sourceUser, destinationUsers: destinationUsers, destinationTeams: destinationTeams, destinationRoles: destinationRoles, dashboards: dashboards, modules: itemsView.getModulesForDashboards(), }]; _.first(dashboardsPayload).type = selectingSection === 'from_modules' ? 'CloneDashboards' : 'CopyDashboards'; this.callCommand(dashboardsPayload); break; case 'filters': let filtersPayload = [{ sourceUser: sourceUser, destinationUsers: destinationUsers, destinationTeams: destinationTeams, destinationRoles: destinationRoles, filters: filters, modules: itemsView.getModulesForFilters(), }]; _.first(filtersPayload).type = selectingSection === 'from_modules' ? 'CloneFilters' : 'CopyFilters'; this.callCommand(filtersPayload); break; } }, /** * Makes api call to UserUtilities. * * @param {Array} payload */ callCommand: function(payload) { const validatedPayload = this.validatePayload(payload); if (!validatedPayload) { return; } app.alert.show('utils-processing', { level: 'process', title: app.lang.getModString('LBL_IN_PROGRESS', this.module), }); const url = app.api.buildURL('userUtilities'); const commands = this.getCommandNames(payload); const messageLabel = this.getMessageLabel(payload); app.api.call('create', url, {'actions': payload}, { success: _.bind(function(commands, messageLabel) { let destinationList = this.context.get('destinationList'); if (destinationList.length >= 20) { const messageLabel = app.lang.getModString('LBL_UTILS_USER_TEAMS_ROLES', this.module); destinationList = destinationList.length + ' ' + messageLabel; } if (_.isArray(destinationList)) { destinationList = destinationList.join(', '); } let alertAttributes = { commands: commands.join(', '), destinationList: destinationList, }; if (messageLabel === 'LBL_COPY_CONTENT_COUNT_SUCCESS') { alertAttributes.count = this.context.get('copyDashboards').length || this.context.get('copyFilters').length; } if (messageLabel === 'LBL_COPY_CONTENT_CLONE_MODULES_SUCCESS') { alertAttributes.moduleList = this.context.get('cloneModules').join(', '); } app.alert.show('utils-success', { level: 'success', messages: app.lang.getModString(messageLabel, this.module, alertAttributes), autoClose: false }); this.clear(); }, this, commands, messageLabel), error: function(error) { app.alert.show('utils-error', { level: 'error', messages: error.message, }); }, complete: function() { app.alert.dismiss('utils-processing'); } }); }, /** * Finds the right message to display in case of success * * @param {Array} payload */ getMessageLabel: function(payload) { for (const command of payload) { if (command.type === 'CloneDashboards' || command.type === 'CloneFilters') { return 'LBL_COPY_CONTENT_CLONE_MODULES_SUCCESS'; } if (command.type === 'CopyDashboards' || command.type === 'CopyFilters') { return 'LBL_COPY_CONTENT_COUNT_SUCCESS'; } } return 'LBL_COPY_CONTENT_SUCCESS'; }, /** * Returns an array of the commands given * * @param {Array} payload */ getCommandNames: function(payload) { let commandNames = []; for (const command of payload) { const commandName = this.parseCommandType(command.type); commandNames.push(commandName); } return commandNames; }, /** * Returns the name of the command action * * @param {string} commandType */ parseCommandType: function(commandType) { return app.lang.getModString(this.cloningTypes[commandType], this.module); }, /** * Validates the command payload * * @param {Array} payload */ validatePayload: function(payload) { const destinationList = this.context.get('destinationList'); const contentView = this.layout.name === 'copy-content' ? this.layout.getComponent('copy-content-items') : this.layout.getComponent('copy-content-locale'); const selectedActions = contentView.$('input:checked'); if (destinationList.length == 0) { app.alert.show('utils-error-no-users', { level: 'error', messages: app.lang.getModString('LBL_NO_DESTINATION', this.module), }); return false; } if (this.layout.name === 'copy-content' && selectedActions.length === 0) { app.alert.show('utils-error-no-prefs', { level: 'error', messages: app.lang.getModString('LBL_NO_USER_PREFERENCES', this.module), }); return false; } for (const command of payload) { if (command.type === 'CopyDashboards' && command.dashboards.length === 0) { app.alert.show('utils-error-no-dashboards', { level: 'error', messages: app.lang.getModString('LBL_NO_DASHBOARD', this.module), }); return false; } if ((command.type === 'CloneDashboards' || command.type === 'CloneFilters') && command.modules.length === 0) { app.alert.show('utils-error-no-modules', { level: 'error', messages: app.lang.getModString('LBL_NO_MODULES', this.module), }); return false; } if (command.type === 'CopyFilters' && command.filters.length === 0) { app.alert.show('utils-error-no-filters', { level: 'error', messages: app.lang.getModString('LBL_NO_FILTERS', this.module), }); return false; } if (this.layout.name === 'copy-user-settings' && selectedActions.length === 0) { app.alert.show('utils-error-no-settings', { level: 'error', messages: app.lang.getModString('LBL_NO_USER_SETTINGS', this.module), }); return false; } } return true; } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.RecordlistView * @alias SUGAR.App.view.views.BaseUsersRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * Extend the parent function to add editability checking for IDM */ parseFields: function() { _.each(this.meta.panels, function(panel) { app.utils.setUsersEditableFields(panel.fields, 'recordlist'); }, this); return this._super('parseFields'); }, /** * @override */ getDeleteMessages: function(model) { let messages = this._super('getDeleteMessages', [model]); messages.confirmation = app.lang.get('LBL_DELETE_USER_CONFIRM', this.module); return messages; }, /** * @override */ deleteModelSuccessCallback: function(model) { this._modelToDelete = null; this.collection.remove(model, {silent: true}); app.events.trigger('preview:close'); this.layout.trigger('list:record:deleted', model); let url = app.bwc.buildRoute('Users', model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, /** * @inheritdoc * * Handles IDM alert messaging */ editClicked: function(model, field) { this._super('editClicked', [model, field]); if (app.config.idmModeEnabled) { let message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_REGULAR_USER', this.module); // Admin users should see a link to the SugarIdentity user list page if (app.user.get('type') === 'admin') { message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_ADMIN_USER', this.module); message = message.replace('%s', app.config.cloudConsoleUsersListUrl); } app.alert.show('edit-user-record', { level: 'info', autoClose: false, messages: app.lang.get(message) }); } }, }) }, "dashablerecord": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.DashablerecordView * @alias SUGAR.App.view.views.BaseUsersDashablerecordView * @extends View.Views.Base.DashablerecordView */ ({ // Dashablerecord View (base) extendsFrom: 'DashablerecordView', /** * Flag to check if we should navigate to Reassign User Records page * {boolean} */ triggerReassignUserRecords: false, /** * @inheritdoc */ _setReadonlyFields: function() { this._super('_setReadonlyFields'); _.each(this.meta.panels, function(panel) { app.utils.setUsersEditableFields(panel.fields, 'dashablerecord'); }); }, /** * @inheritdoc */ completeCallback: function() { this._super('completeCallback'); if (this.triggerReassignUserRecords) { this.triggerReassignUserRecords = false; app.alert.show('reassign_records', { level: 'confirmation', messages: app.lang.get('LBL_REASS_CONFIRM_REASSIGN', this.module), onConfirm: _.bind(function() { let url = app.bwc.buildRoute('Users', this.model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, this), onCancel: function() { return; }, }, this); } }, /** * @inheritdoc */ handleSave: function() { let changedAttributes = this.model.changedAttributes(); if (changedAttributes && changedAttributes.status && changedAttributes.status === 'Inactive') { this.triggerReassignUserRecords = true; } this._super('handleSave'); }, /** * @inheritdoc * * Handles IDM alert messaging */ editRecord: function() { this._super('editRecord'); if (app.config.idmModeEnabled) { let message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_REGULAR_USER', this.module); // Admin users should see a link to the SugarIdentity user edit page if (app.user.get('type') === 'admin') { message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_ADMIN_USER', this.module); message = message.replace('%s', this.model.get('cloud_console_user_link')); } app.alert.show('edit-user-record', { level: 'info', autoClose: false, messages: app.lang.get(message) }); } } }) }, "copy-content-locale": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.CopyContentLocaleView * @alias SUGAR.App.view.layouts.BaseUsersCopyContentLocaleView * @extends View.Views.Base.View */ ({ // Copy-content-locale View (base) /** * @inheritdoc */ events: { 'change .fromUser': 'loadSettings', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.currentUserId = app.user.id; this.retrieveUsers(); this.getData(); }, /** * @inheritdoc */ _render: function() { this._super('_render', arguments); this.renderDropdowns(); }, /** * Loads the settings for the selected user * * @param {Event} evt */ loadSettings: function(evt) { const userData = this.$(evt.currentTarget).find(':selected').data(); if (_.isEmpty(userData)) { return; } this.currentUserId = userData.id; this.getData(); }, /** * retrieve the users in order to display them in the user select field */ retrieveUsers: function() { const usersUrl = app.api.buildURL('Users', null, null, { filter: [{ status: { $equals: 'Active' } }], max_num: -1, order_by: 'first_name:asc', }); app.api.call('read', usersUrl, null, { success: _.bind(function(data) { this.users = data.records; this.render(); }, this), error: _.bind(function(error) { app.alert.show('user-utils-error', { level: 'error', messages: app.lang.getModString('LBL_USER_UTILS_DATA_ERROR', this.module), }); }, this), }); }, /** * Creates the select2 dropdowns. */ renderDropdowns: function() { this.$('select').select2({ allowClear: true, containerCss: 'select2-choices-pills-close', }); }, /** * Gets the locales data for the current user. */ getData: function() { const currentUser = this.currentUserId; const localeUrl = app.api.buildURL('userUtilities', `getLocaleData/${currentUser}`); app.api.call('read', localeUrl, null, { success: _.bind(function(data) { this.locales = data; this.render(); }, this), error: function(error) { app.alert.show('user-utils-error', { level: 'error', messages: app.lang.getModString('LBL_USER_UTILS_DATA_ERROR', this.module), }); } }); }, }) }, "module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Module menu provides a reusable and easy render of a module Menu. * * This also helps doing customization of the menu per module and provides more * metadata driven features. * * @class View.Views.Base.Users.ModuleMenuView * @alias SUGAR.App.view.views.BaseUsersModuleMenuView * @extends View.Views.Base.ModuleMenuView */ ({ // Module-menu View (base) extendsFrom: 'ModuleMenuView', _renderHtml: function() { if (app.config.idmModeEnabled) { var meta = app.metadata.getModule(this.module) || {}; this.actions = this.filterByAccess(meta.menu && meta.menu.header && meta.menu.header.meta); this.actions.forEach(_.bind(function(menuItem, key) { if (menuItem.label === 'LNK_NEW_USER' && -1 === menuItem.route.indexOf('user_hint')) { this.actions[key].route = this.getCloudConsoleLink(); } }, this)); } this._super('_renderHtml'); if (!this.meta.short) { this.$el.addClass('btn-group'); } }, handleRouteEvent: function(event) { if (App.config.idmModeEnabled && (event.target.getAttribute('data-navbar-menu-item') == 'LNK_NEW_USER')) { App.alert.show('idm_create_user', { level: 'info', messages: App.lang .get('ERR_CREATE_USER_FOR_IDM_MODE', 'Users') .replace('{0}', this.getCloudConsoleLink()) }); } this._super('handleRouteEvent', [event]); }, getCloudConsoleLink: function() { return this.meta.cloudConsoleLink + '&user_hint=' + app.utils.createUserSrn(app.user.id); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.RecordView * @alias SUGAR.App.view.views.BaseUsersRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * Flag to check if we should navigate to Reassign User Records page * {boolean} */ triggerReassignUserRecords: false, /** * Extend the parent function to add editability checking for IDM * * @param {Array} options */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this._super('initialize', [options]); _.each(this.meta.panels, function(panel) { app.utils.setUsersEditableFields(panel.fields, 'record'); }); this._initUserTypeViews(); }, /** * @inheritdoc */ _afterInit: function() { this._super('_afterInit'); // Get a list of names of all the user preference fields on the view let userPreferenceFields = []; let viewFields = _.flatten(_.pluck(this.meta.panels, 'fields')); _.each(viewFields, function(field) { if (field.name) { let fieldDef = this.model.fields[field.name]; if (fieldDef && fieldDef.user_preference) { userPreferenceFields.push(field.name); } } }, this); // Make sure all user preference fields are added to the options of // fields to fetch let contextFields = this.context.get('fields') || []; contextFields = contextFields.concat(userPreferenceFields); this.context.set('fields', _.uniq(contextFields)); }, /** * @inheritdoc */ bindDataChange: function() { this.listenTo(this.context, 'button:reset_preferences:click', this.resetPreferencesClicked); this.listenTo(this.context, 'button:reset_password:click', this.resetPasswordClicked); this._super('bindDataChange'); }, /** * @override */ getDeleteMessages: function() { let messages = this._super('getDeleteMessages'); messages.confirmation = app.lang.get('LBL_DELETE_USER_CONFIRM', this.module); return messages; }, /** * @override */ deleteModelSuccessCallback: function() { this.context.trigger('record:deleted', this._modelToDelete); this._modelToDelete = false; let url = app.bwc.buildRoute('Users', this.model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, /** * @inheritdoc */ _getNoAccessErrorMessage: function(error) { if (error.code === 'license_seats_needed' && _.isString(error.message)) { return error.message; } return this._super('_getNoAccessErrorMessage', [error]); }, /** * @inheritdoc * * Handles IDM alert messaging */ editClicked: function() { this._super('editClicked'); if (app.config.idmModeEnabled && !this.model.get('is_group') && !this.model.get('portal_only')) { let message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_REGULAR_USER', this.module); // Admin users should see a link to the SugarIdentity user edit page if (app.user.get('type') === 'admin') { message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_ADMIN_USER', this.module); message = message.replace('%s', this.model.get('cloud_console_user_link')); } app.alert.show('edit-user-record', { level: 'info', autoClose: false, messages: app.lang.get(message) }); } }, /** * @inheritdoc */ _saveModelCompleteCallback: function() { this._super('_saveModelCompleteCallback'); if (this.triggerReassignUserRecords) { this.triggerReassignUserRecords = false; app.alert.show('reassign_records', { level: 'confirmation', messages: app.lang.get('LBL_REASS_CONFIRM_REASSIGN', this.module), onConfirm: _.bind(function() { let url = app.bwc.buildRoute('Users', this.model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, this), onCancel: function() { return; }, }, this); } }, /** * @inheritdoc */ handleSave: function() { let changedAttributes = this.model.changedAttributes(); if (changedAttributes && changedAttributes.status && changedAttributes.status === 'Inactive') { this.triggerReassignUserRecords = true; } this._super('handleSave'); }, /** * Reset all preferences for this user */ resetPreferencesClicked: function() { app.alert.show('reset_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_RESET_PREFERENCES_WARNING_USER', this.module), onConfirm: _.bind(function() { let url = app.api.buildURL(this.module, `${this.model.get('id')}/resetPreferences`); app.api.call('update', url, null, { success: _.bind(function() { this.context.reloadData(); app.alert.show('reset_success', { level: 'success', messages: app.lang.get('LBL_RESET_PREFERENCES_SUCCESS_USER', this.module), autoClose: true, }); }, this), }); }, this), onCancel: function() { return; } }); }, /** * Reset password for this user */ resetPasswordClicked: function() { app.alert.show('reset_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_RESET_PASSWORD_WARNING_USER', 'Users'), onConfirm: _.bind(function() { let params = { userId: this.model.get('id') }; let url = app.api.buildURL('password/adminRequest'); app.api.call('create', url, params, { success: function() { app.alert.show('reset_success', { level: 'success', messages: app.lang.get('LBL_NEW_USER_PASSWORD_RESET', 'Users'), autoClose: true, }); }, error: function(err) { app.logger.error('Failed to trigger a password reset for a user : ' + JSON.stringify(err)); app.alert.show('reset_error', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: err.message || app.lang.get('EXCEPTION_UNKNOWN_EXCEPTION') }); } }); }, this), onCancel: function() { return; } }); }, /** * Sets up functionality to support special views based on the User type * * @private */ _initUserTypeViews() { // Always fetch is_group and portal_only so we can determine if we need // to show their special views let contextFields = this.context.get('fields') || []; contextFields.push('is_group', 'portal_only'); this.context.set('fields', _.uniq(contextFields)); this._checkUserType(); this.listenTo(this.model, 'change:is_group change:portal_only', this._checkUserType); this.listenTo(this.model, 'sync', this._checkAbilityPasswordReset); }, /** * Fetches new metadata and re-renders to show special views based on the * User type if necessary * * @private */ _checkUserType: function() { let viewType = this.model.get('is_group') ? 'group' : this.model.get('portal_only') ? 'portalapi' : false; if (['group', 'portalapi'].includes(viewType)) { this.meta = _.extend({}, app.metadata.getView(null, 'record'), app.metadata.getView(this.module, `record-${viewType}`)); this.render(); this.handleActiveTab(); } }, /** * Checks the ability of user to trigger a password reset and lefts this item in menu * if it is possible * * @private */ _checkAbilityPasswordReset: function() { _.each(this.meta.buttons, function(buttonMeta) { if (buttonMeta.name === 'main_dropdown' && !this._accessToResetPassword()) { buttonMeta.buttons = _.filter(buttonMeta.buttons, function(button) { return button.name !== 'reset_password'; }); this.render(); } }, this); }, /** * Checks if user can reset password * * @return {boolean} true if user can reset password * @private */ _accessToResetPassword: function() { if (_.isUndefined(this.model.get('user_name'))) { return true; } return app.acl.hasAccess('admin', 'Users') && app.user.get('user_name') !== this.model.get('user_name') && this.model.get('status') === 'Active' && !['SugarCustomerSupportPortalUser', 'SNIPuser'].includes(this.model.get('user_name')); }, /** * @inheritdoc * * Bypasses IDM mode restrictions for creating group and portal type users */ getCustomSaveOptions: function(options) { if (app.config.idmModeEnabled && (this.model.get('is_group') || this.model.get('portal_only'))) { options = options || {}; options.params = options.params || {}; options.params.skip_idm_mode_restrictions = true; } return options; }, /** * @inheritdoc */ _dispose: function() { this._super('_dispose'); this.stopListening(); } }) }, "sidebar-nav-flyout-actions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.SidebarNavFlyoutMenuView * @alias SUGAR.App.view.views.BaseUsersSidebarNavFlyoutMenuView * @extends View.Views.Base.SidebarNavFlyoutMenuView */ ({ // Sidebar-nav-flyout-actions View (base) extendsFrom: 'SidebarNavFlyoutMenuView', /** * @inheritdoc */ _handleRouteItemClick: function(event) { let menuItem = event.target.closest('[data-navbar-menu-item=LNK_NEW_USER]'); if (menuItem && app.config.idmModeEnabled) { app.alert.show('idm_create_user', { level: 'info', messages: app.lang .get('ERR_CREATE_USER_FOR_IDM_MODE', 'Users') .replace('{0}', menuItem.getAttribute('data-route')) }); } this._super('_handleRouteItemClick', [event]); }, }) }, "copy-user-settings-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.CopyUserSettingsButtonsView * @alias SUGAR.App.view.layouts.BaseUsersCopyUserSettingsButtonsView * @extends View.Layouts.Base.View */ ({ // Copy-user-settings-buttons View (base) extendsFrom: 'UsersCopyContentButtonsView', /** * @inheritdoc */ copy: function() { let userSettings = {}; const destinationUsers = this.context.get('destinationUsers'); const destinationTeams = this.context.get('destinationTeams'); const destinationRoles = this.context.get('destinationRoles'); if (_.isEmpty(destinationRoles) && _.isEmpty(destinationTeams) && _.isEmpty(destinationUsers)) { app.alert.show('user-utils-error', { level: 'error', messages: app.lang.getModString('LBL_USER_UTILITIES_DESTINATION_USER_ERROR', this.module), }); return; } const settingsView = this.layout.getComponent('copy-content-locale'); const selectedSettings = settingsView.$('input:checked'); for (const setting of selectedSettings) { settingName = setting.name; userSettings[settingName] = this._getUserSetting(settingName, settingsView); } const payload = [{ type: 'CloneUserSettings', userSettings: userSettings, destinationUsers: destinationUsers, destinationTeams: destinationTeams, destinationRoles: destinationRoles, }]; this.callCommand(payload); }, /** * Gets the value for the setting * * @param {string} settingName */ _getUserSetting: function(settingName, settingsView) { let setting = settingsView.$(`select[name=${settingName}]`).val(); if (!setting) { setting = settingsView.$(`input[name=${settingName}]:not([type=checkbox])`).val(); } return setting; } }) }, "copy-content-items": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.CopyContentItemsView * @alias SUGAR.App.view.layouts.BaseUsersCopyContentItemsView * @extends View.Views.Base.View */ ({ // Copy-content-items View (base) /** * Modules which can't be used to copy/clone dashboards/filters from */ denyModules: [ 'Login', 'Sync', 'Connectors', 'CustomQueries', 'EAPM', 'FAQ', 'OAuthKeys', 'OAuthTokens', 'SNIP', 'Styleguide', 'SugarLive', 'Trackers', 'TrackerQueries', 'TrackerSessions', 'TrackerPerfs', 'UpgradeWizard', 'WebLogicHooks', 'iFrames', 'TimePeriods', 'TeamNotices', 'DataSets', 'SugarFavorites', 'SavedSearch', 'PdfManager', 'Teams', 'ACLRoles', 'Releases', ], /** * Default selected section */ section: 'user_prefs', /** * default subsection */ selectingSection: 'from_modules', /** * @inheritdoc */ events: { 'change .ut-pref-choice': 'changeSection', 'change .fromUser': 'changeFromUser', 'change [name=selection]': 'changeSelection', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); let modulesMeta = app.metadata.getModules({ filter: 'display_tab', access: true, }); this.modules = Object.keys(modulesMeta) .filter(key => !this.denyModules.includes(key)) .reduce((obj, key) => { obj[key] = modulesMeta[key]; return obj; }, {}); this.currentUserId = app.user.id; this.sourceUser = app.user.id; this.retrieveUsers(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._renderDropdowns(); this.updateSelectionFilters(); }, /** * retrieve the users in order to display them in the user select field */ retrieveUsers: function() { const usersUrl = app.api.buildURL('Users', null, null, { filter: [{ status: { $equals: 'Active' } }], max_num: -1, order_by: 'first_name:asc', }); app.api.call('read', usersUrl, null, { success: _.bind(function(data) { this.users = data.records; this.render(); }, this), error: function(error) { app.alert.show('user-utils-error', { level: 'error', messages: app.lang.getModString('LBL_USER_UTILS_DATA_ERROR', this.module), }); }, }); }, /** * When the section dropdown changes * * @param {Event} evt */ changeSection: function(evt) { this.section = evt.target.value; switch (this.section) { case 'user_prefs': this.$('.user-prefs-section').removeClass('hide'); this.$('.dashboards-section').addClass('hide'); this.$('.filters-section').addClass('hide'); break; case 'dashboards': this.$('.user-prefs-section').addClass('hide'); this.$('.dashboards-section').removeClass('hide'); this.$('.filters-section').addClass('hide'); this.selectingSection = 'from_modules'; this._setDefaultSelection(this.section); break; case 'filters': this.$('.user-prefs-section').addClass('hide'); this.$('.dashboards-section').addClass('hide'); this.$('.filters-section').removeClass('hide'); this.selectingSection = 'from_modules'; this._setDefaultSelection(this.section); break; } }, /** * Sets the default selection for modules * * @param {string} section */ _setDefaultSelection: function(section) { this.$(`.${section}-section [name="selection"][value="from_modules"]`).prop('checked', true); this.$(`.${section}-section [name="selection"][value="from_modules"]`).trigger('change'); }, /** * show selects as select2 */ _renderDropdowns: function() { this.$('.select-modules').select2({ allowClear: true, containerCssClass: 'select2-choices-pills-close', }); }, /** * event for changing the "from" user * * @param {Event} evt */ changeFromUser: function(evt) { const target = evt.target; this.sourceUser = target.options[target.selectedIndex].dataset.id; this.updateSelectionFilters(); }, /** * Update the dashboards and filters hybrid select initial filter */ updateSelectionFilters: function() { this.getField('dashboards_select').context.set('assigned_user', this.sourceUser); this.getField('filters_select').context.set('assigned_user', this.sourceUser); }, /** * When changing sections make sure the other sections are hidden * * @param {Event} evt */ changeSelection: function(evt) { this.selectingSection = evt.target.value; switch (this.selectingSection) { case 'existing_dashboards': this.$('.existing-dashboards').removeClass('hide'); this.$('.from-modules').addClass('hide'); this.$('.existing-filters').addClass('hide'); break; case 'existing_filters': this.$('.existing-dashboards').addClass('hide'); this.$('.from-modules').addClass('hide'); this.$('.existing-filters').removeClass('hide'); break; case 'from_modules': this.$('.existing-dashboards').addClass('hide'); this.$('.from-modules').removeClass('hide'); this.$('.existing-filters').addClass('hide'); break; } }, /** * Return the selected preferences */ getSelectedPrefTypes: function() { const selectedTypes = []; const favoriteReports = this.$('[name=favorite_reports]').prop('checked') && selectedTypes.push('CloneFavoriteReports'); const sugarEmailClient = this.$('[name=sugar_email_client]').prop('checked') && selectedTypes.push('CloneSugarEmailClient'); const scheduledReporting = this.$('[name=scheduled_reporting]').prop('checked') && selectedTypes.push('CloneScheduledReporting'); const notifyOnAssignement = this.$('[name=notify_on_assignment]').prop('checked') && selectedTypes.push('CloneNotifyOnAssignment'); const reminderOptions = this.$('[name=reminder_options]').prop('checked') && selectedTypes.push('CloneRemindersOptions'); const defaultTeams = this.$('[name=default_teams]').prop('checked') && selectedTypes.push('CloneDefaultTeams'); const navigationBar = this.$('[name=navigation_bar]').prop('checked') && selectedTypes.push('CloneNavigationBar'); return selectedTypes; }, /** * Get modules for filter cloning */ getModulesForFilters: function() { return this.$('.filters-section select.select-modules').val(); }, /** * Get modules for dashboard cloning */ getModulesForDashboards: function() { return this.$('.dashboards-section select.select-modules').val(); }, /** * Reset the current section to user_prefs */ resetSection: function() { this.section = 'user_prefs'; this.selectingSection = 'from_modules'; } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.CreateView * @alias SUGAR.App.view.views.BaseUsersCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc * * Sets correct metadata if the user being created is a special type */ initialize: function(options) { let userType = options.context.get('userType'); if (['group', 'portalapi'].includes(userType)) { options.meta = _.extend({}, app.metadata.getView(options.module, `record-${userType}`), options.meta); this.showExtraMeta = false; } this._super('initialize', [options]); this._setConfigBasedDefaults(); }, /** * Sets default values for fields whose defaults are based on system configuration * * @private */ _setConfigBasedDefaults: function() { let defaults = {}; let configDefaultFields = _.filter(app.metadata.getModule('Users', 'fields'), (field) => { return _.has(field, 'defaultFromConfig'); }); _.each(configDefaultFields, function(configDefaultField) { if (_.has(app.config, configDefaultField.defaultFromConfig)) { defaults[configDefaultField.name] = app.config[configDefaultField.defaultFromConfig]; } }, this); if (!_.isEmpty(defaults)) { this.model.setDefault(defaults); } }, /** * @inheritdoc */ _getNoAccessErrorMessage: function(error) { if (error.code === 'license_seats_needed' && _.isString(error.message)) { return error.message; } return this._super('_getNoAccessErrorMessage', [error]); }, /** * @inheritdoc * * Bypasses IDM mode restrictions for creating group and portal type users */ getCustomSaveOptions: function(options) { if (app.config.idmModeEnabled && ['group', 'portalapi'].includes(this.context.get('userType'))) { options = options || {}; options.params = options.params || {}; options.params.skip_idm_mode_restrictions = true; } return options; }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Users.PreviewView * @alias SUGAR.App.view.views.BaseUsersPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * Flag to check if we should navigate to Reassign User Records page * {boolean} */ triggerReassignUserRecords: false, /** * Extend the parent function to add editability checking for IDM * * @param {Array} options */ initialize: function(options) { this._super('initialize', [options]); this.originalMeta = this.meta; _.each(this.meta.panels, function(panel) { app.utils.setUsersEditableFields(panel.fields, 'record'); }); // Always fetch is_group and portal_only so we can determine if we need // to show their special views let contextFields = this.context.get('fields') || []; contextFields.push('is_group', 'portal_only'); this.context.set('fields', _.uniq(contextFields)); }, /** * @inheritdoc */ _render: function() { // Set up the special view metadata for Group and Portal API type Users let viewType = this.model.get('is_group') ? 'group' : this.model.get('portal_only') ? 'portalapi' : false; if (['group', 'portalapi'].includes(viewType)) { let desiredMeta = _.extend({}, this.meta, app.metadata.getView('Users', `record-${viewType}`)); this.meta = this._previewifyMetadata(desiredMeta); } else { this.meta = this.originalMeta; } // Watch for special User types so that we render the correct metadata this.stopListening(this.model, 'change:is_group change:portal_only', this.render); this.listenTo(this.model, 'change:is_group change:portal_only', this.render); this._super('_render'); }, /** * @inheritdoc */ _previewifyMetadata: function(meta) { let formattedMeta = this._super('_previewifyMetadata', [meta]); if (formattedMeta && formattedMeta.panels) { formattedMeta.panels = formattedMeta.panels.filter((item) => !['downloads_tab_panel', 'access_tab_user_role_panel'].includes(item.name)); } return formattedMeta; }, /** * @inheritdoc */ _getNoAccessErrorMessage: function(error) { if (error.code === 'license_seats_needed' && _.isString(error.message)) { return error.message; } return this._super('_getNoAccessErrorMessage', [error]); }, /** * @inheritdoc */ _renderHtml: function() { this.meta = this._previewifyMetadata(this.meta); this._super('_renderHtml'); }, /** * @inheritdoc * * Handles IDM alert messaging */ handleEdit: function() { this._super('handleEdit'); if (app.config.idmModeEnabled) { let message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_REGULAR_USER', this.module); // Admin users should see a link to the SugarIdentity user edit page if (app.user.get('type') === 'admin') { message = app.lang.get('LBL_IDM_MODE_NON_EDITABLE_FIELDS_FOR_ADMIN_USER', this.module); message = message.replace('%s', this.model.get('cloud_console_user_link')); } app.alert.show('edit-user-record', { level: 'info', autoClose: false, messages: app.lang.get(message) }); } }, /** * @inheritdoc */ handleSave: function() { let changedAttributes = this.model.changedAttributes(); if (changedAttributes && changedAttributes.status && changedAttributes.status === 'Inactive') { this.triggerReassignUserRecords = true; } this._super('handleSave'); }, /** * @inheritdoc */ completeCallback: function() { this._super('completeCallback'); if (this.triggerReassignUserRecords) { this.triggerReassignUserRecords = false; app.alert.show('reassign_records', { level: 'confirmation', messages: app.lang.get('LBL_REASS_CONFIRM_REASSIGN', this.module), onConfirm: _.bind(function() { let url = app.bwc.buildRoute('Users', this.model.get('id'), 'reassignUserRecords'); app.router.navigate(url, {trigger: true}); }, this), onCancel: function() { return; }, }, this); } } }) }, "copy-content-users": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.UsersCopyContentUsers * @alias SUGAR.App.view.layouts.BaseUsersCopyContentUsersView * @extends View.Views.Base.View */ ({ // Copy-content-users View (base) /** * @inheritdoc */ events: { 'change .destinationSelect': 'updateDestination', }, /** * Updates destination users, teams. roles. * */ updateDestination: function() { let users = this.getField('users_select').items; let teams = this.getField('teams_select').items; let roles = this.getField('roles_select').items; let destinationList = this.mergeDestinations(users, teams, roles); this.context.set({ 'destinationUsers': this.getDestinationIds(users), 'destinationTeams': this.getDestinationIds(teams), 'destinationRoles': this.getDestinationIds(roles), 'destinationList': destinationList, }); }, /** * Returns array of destination names * * @param {Array} users * @param {Array} teams * @param {Array} roles */ mergeDestinations: function(users, teams, roles) { let userNames = this.getDestinationNames(users); let teamNames = this.getDestinationNames(teams); let roleNames = this.getDestinationNames(roles); let destinationNames = [...userNames, ...teamNames, ...roleNames]; return destinationNames; }, /** * Filters the names from the destination list * * @param {Array} destinationList */ getDestinationNames: function(destinationList) { let list = _.filter(destinationList, function(item) { return !_.isEmpty(item.id); }); return _.map(list, function(destination) { return destination.text; }); }, /** * Filters the ids from the destination list * * @param {Array} destinationList */ getDestinationIds: function(destinationList) { return _.map(destinationList, function(destination) { return destination.id; }); }, }) } }} , "layouts": { "base": { "sidebar-nav-flyout-module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Users.SidebarNavFlyoutModuleMenuLayout * @alias SUGAR.App.view.layouts.BaseUsersSidebarNavFlyoutModuleMenuLayout * @extends View.Layouts.Base.SidebarNavFlyoutModuleMenuLayout */ ({ // Sidebar-nav-flyout-module-menu Layout (base) extendsFrom: 'SidebarNavFlyoutModuleMenuLayout', /** * @override */ _getMenuActions: function() { let actions = this._super('_getMenuActions'); let cloudConsoleLink = this._getCloudConsoleLink(); if (app.config.idmModeEnabled) { actions.forEach(_.bind(function(menuItem, key) { if (menuItem.label === 'LNK_NEW_USER' && -1 === menuItem.route.indexOf('user_hint')) { actions[key].route = `${cloudConsoleLink}&user_hint=${app.utils.createUserSrn(app.user.id)}`; } }, this)); } return actions; }, /** * Returns cloud console link in case of an IDM instance * * @return {string} * @private */ _getCloudConsoleLink: function() { return app.utils.deepCopy(this.meta && this.meta.cloudConsoleLink || ''); }, }) }, "selection-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Users.SelectionListLayout * @alias SUGAR.App.view.layouts.BaseUsersSelectionListLayout * @extends View.Layouts.Base.SelectionListLayout */ ({ // Selection-list Layout (base) extendsFrom: 'BaseSelectionListLayout', /** * @inheritdoc */ loadData: function(options) { this.setUsersFilters(); this._super('loadData', [options]); }, /** * Sets flags on the collection parameters to filter out certain users */ setUsersFilters: function() { let params = this.collection.getOption('params') || {}; params.filterInactive = true; params.filterPortal = true; this.collection.setOption('params', params); } }) } }} , "datas": {} }, "Employees":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Administration":{"fieldTemplates": { "base": { "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationModuleIconField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ getSelect2Options: function(optionsKeys) { let select2Options = {}; select2Options = this._super('getSelect2Options', [optionsKeys]); if (_.contains(['color', 'icon'], this.def.formatOptions)) { select2Options.formatResult = _.bind(this.formatIconsOrColors, this); select2Options.formatSelection = _.bind(this.formatIconsOrColors, this); } return select2Options; }, /** * Format options to show the icon or color associated with the value * @param opt * @return {string|*|jQuery|HTMLElement} */ formatIconsOrColors: function(opt) { if (!opt.id) { return opt.text.toUpperCase(); } return $( `<span class="flex flex-row items-center"> <i class="h-3.5 inline-block mr-1.5 rounded-sm sicon ${_.escape(opt.id)} w-3.5"></i> ${_.escape(opt.text)} </span>` ); }, }) }, "modules": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationModuleSelectField * @extends View.Fields.Base.EnumField */ ({ // Modules FieldTemplate (base) extendsFrom: 'EnumField', waitingForRender: false, moduleFields: {}, render: function(options) { if (this.waitingForRender) { return; } this.waitingForRender = true; var self = this; this.load(function() { self._super('render', [options]); self.waitingForRender = false; }); }, load: function(callback) { var self = this; this.setEnabled(false); var options = { success: _.bind(function(data) { self.items = {}; self.moduleFields = {}; _.each(data, function(value, key) { self.items[key] = key; self.moduleFields[key] = value; }); }, this), error: function() { self.items = {}; }, complete: function() { if (callback) { callback(); } self.setEnabled(true); self.model.trigger('change:modules'); } }; app.api.call( 'get', app.api.buildURL(this.module, 'denormalization/configuration'), [], options, {context: this} ); }, getFieldsForModule: function(module) { return this.moduleFields[module] || []; }, /** * @inheritdoc */ getSelect2Options: function() { var options = this._super('getSelect2Options'); options.escapeMarkup = function(m) { return m; }; return options; }, /** * @inheritdoc */ _filterOptions: function(options) { var self = this; options = this._super('_filterOptions', [options]); var filtered = {}; _.each(options, function(item, key) { if (!_.isEmpty(self.view.getFieldsForModule(key))) { filtered[key] = item; } }); return filtered; }, _sortResults: function(results) { results = this._super('_sortResults', [results]); var self = this; var updated = []; var fields; var df; var nf; _.each(results, function(item) { fields = self.view.getFieldsForModule(item.id); df = nf = 0; _.each(fields, function(field) { if (field.is_denormalized) { df += 1; } else { nf += 1; } }); if (df > 0) { item.text = '<span style="font-weight: bold">' + item.text + '</span>' + '<small style="margin-left: 10px">' + nf + ' / <b style="color: #54cb14">' + df + '</b></small>'; } else { item.text += '<small style="margin-left: 10px">' + nf + '</small>'; } updated.push(item); }); return updated; }, setEnabled: function(flag) { var cmd = flag ? 'enable' : 'disable'; this.$(this.fieldTag).select2(cmd); } }) }, "module-select": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationModuleSelectField * @extends View.Fields.Base.EnumField */ ({ // Module-select FieldTemplate (base) extendsFrom: 'EnumField', waitingForRender: false, moduleFields: {}, render: function(options) { if (this.waitingForRender) { return; } this.waitingForRender = true; var self = this; this.load(function() { self._super('render', [options]); self.waitingForRender = false; }); }, load: function(callback) { var self = this; this.setEnabled(false); var options = { success: _.bind(function(data) { self.items = {}; self.moduleFields = {}; _.each(data, function(value, key) { self.items[key] = key; self.moduleFields[key] = value; }); }, this), error: function() { self.items = {}; }, complete: function() { if (callback) { callback(); } self.setEnabled(true); } }; app.api.call( 'get', app.api.buildURL(this.module, 'denormalization/configuration'), [], options, {context: this} ); }, getFieldsForModule: function(module) { return this.moduleFields[module] || []; }, /** * @inheritdoc */ getSelect2Options: function() { var options = this._super('getSelect2Options'); options.escapeMarkup = function(m) { return m; }; return options; }, /** * @inheritdoc */ _filterOptions: function(options) { var self = this; options = this._super('_filterOptions', [options]); var filtered = {}; _.each(options, function(item, key) { if (!_.isEmpty(self.view.getFieldsForModule(key))) { filtered[key] = item; } }); return filtered; }, _sortResults: function(results) { results = this._super('_sortResults', [results]); var self = this; var updated = []; var fields; var df; var nf; _.each(results, function(item) { fields = self.view.getFieldsForModule(item.id); df = nf = 0; _.each(fields, function(field) { if (field.is_denormalized) { df += 1; } else { nf += 1; } }); if (df > 0) { item.text = '<span style="font-weight: bold">' + item.text + '</span>' + '<small style="margin-left: 10px">' + nf + ' / <b style="color: #54cb14">' + df + '</b></small>'; } else { item.text += '<small style="margin-left: 10px">' + nf + '</small>'; } updated.push(item); }); return updated; }, setEnabled: function(flag) { var cmd = flag ? 'enable' : 'disable'; this.$(this.fieldTag).select2(cmd); } }) }, "linked-lists": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationLinkedListsField */ ({ // Linked-lists FieldTemplate (base) selectorListDefault: '#df-fields-default', selectorListDenorm: '#df-fields-denorm', selectorListDenormId: 'df-fields-denorm', render: function(options) { this._super('render', [options]); this.initSortable(); }, initSortable: function() { var self = this; this.getElementFieldListDefault().sortable({ connectWith: this.selectorListDenorm, cursor: 'grabbing', placeholder: 'label-warning', revert: true, items: 'li:not(.muted)', dropOnEmpty: true }).disableSelection(); this.getElementFieldListDenormalized().sortable({ connectWith: this.selectorListDefault, cursor: 'grabbing', placeholder: 'label-warning', revert: true, update: self.onUpdate.bind(this), items: 'li:not(.muted)', dropOnEmpty: true }).disableSelection(); }, getElementFieldListDefault: function() { return this.$(this.selectorListDefault); }, getElementFieldListDenormalized: function() { return this.$(this.selectorListDenorm); }, getLiTemplate: function(field, isDenormalized) { return '<li data-name="' + field.name + '" data-is-denormalized="' + (isDenormalized ? '1' : '') + '" style="cursor: grab; border-bottom: 1px solid #e4e4e4">' + field.name + '<span class="pull-right">' + field.type + '</span></li>'; }, refresh: function(fields, denormalizedFieldList) { var self = this; denormalizedFieldList = denormalizedFieldList || []; this.clearFieldLists(); var fieldListDefault = this.getElementFieldListDefault(); var fieldListDenormalized = this.getElementFieldListDenormalized(); _.each(fields, function(el) { var isDenormalized = !!denormalizedFieldList[el.name]; var fieldList = isDenormalized ? fieldListDenormalized : fieldListDefault; fieldList.append(self.getLiTemplate(el, isDenormalized)); }); this.update(); }, clearFieldLists: function() { this.getElementFieldListDefault().find('li').remove(); this.getElementFieldListDenormalized().find('li').remove(); }, onUpdate: function(event, ui) { var isDenormList = !!ui.sender; var isDenormField = ui.item.data('is-denormalized') === 1; var listTo = isDenormList ? this.getElementFieldListDenormalized() : this.getElementFieldListDefault(); var listFrom = !isDenormList ? this.getElementFieldListDenormalized() : this.getElementFieldListDefault(); if (isDenormField !== isDenormList) { listFrom.find('li').addClass('muted'); listTo.find('li').addClass('muted'); ui.item.removeClass('muted').addClass('text-success'); this.context.trigger('field-lists:pending'); } else { listFrom.find('li').removeClass('muted text-success'); listTo.find('li').removeClass('muted text-success'); this.context.trigger('field-lists:initial'); } this.getElementFieldListDefault().sortable('destroy'); this.getElementFieldListDenormalized().sortable('destroy'); this.initSortable(); this.update(); }, update: function() { var data = {'not_denormalized': [], 'denormalized': []}; this.getElementFieldListDefault().find('li').each(function() { data.not_denormalized.push($(this).data('name')); }); this.getElementFieldListDenormalized().find('li').each(function(el) { data.denormalized.push($(this).data('name')); }); this.model.set('field-lists', data, {silent: true}); } }) }, "sub-title": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Administration.SubTitleField * @alias SUGAR.App.view.fields.BaseAdministrationSubTitleField * @extends View.Fields.Base.LabelField */ ({ // Sub-title FieldTemplate (base) extendsFrom: 'LabelField', /** * @inheritdoc */ format: function(value) { value = app.lang.get(this.def.text, this.module); return value; } }) }, "url": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Administration.UrlField * @alias SUGAR.App.view.fields.BaseAdministrationUrlField * @extends View.Fields.Base.UrlField */ ({ // Url FieldTemplate (base) extendsFrom: 'UrlField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.model.addValidationTask('url_' + this.cid, _.bind(this._doValidateUrl, this)); }, /** * URL validation. * * @param {Object} fields The list of field names and their definitions. * @param {Object} errors The list of field names and their errors. * @param {Function} callback Async.js waterfall callback. * */ _doValidateUrl: function(fields, errors, callback) { var value = this.model.get(this.name); if (value && !/^https?:\/\/[^\s\/$.?#]+\.[^\s]+$/.test(this.format(value))) { errors[this.name] = errors[this.name] || {}; errors[this.name].url = true; } callback(null, fields, errors); }, /** * @inheritdoc */ unformat: function(value) { if (value && !value.match(/^([a-zA-Z]+):/)) { value = 'http://' + value; } return this._super('unformat', [value]); }, /** * @inheritdoc */ _dispose: function() { this.model.removeValidationTask('url_' + this.cid); this._super('_dispose'); }, }) }, "job-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationJobListField */ ({ // Job-list FieldTemplate (base) initialize: function(options) { this._super('initialize', [options]); this.on('render', function() { this.loadData(); }, this); this.events = _.extend({}, this.events, { 'click [data-action=refreshList]': 'loadData', 'click [data-action=removeJob]': 'removeJob' }); }, /** * @inheritdoc */ loadData: function() { var options = {}; options.success = _.bind(function(data) { this.buildList(data); }, this); app.api.call( 'read', app.api.buildURL(this.module, 'denormalization/status'), [], options, {context: this} ); }, buildList: function(data) { data.dataParsed = data.data ? JSON.parse(data.data) : {}; data.dataPretty = JSON.stringify(data.dataParsed, null, 2); data.module = this.module; this.$el.html(this.template(data)); }, removeJob: function() { var self = this; app.alert.show('relate-denormalization-rm-job-warning', { level: 'confirmation', title: app.lang.get('LBL_WARNING'), messages: app.lang.get('LBL_ALERT_CONFIRM_DELETE'), onConfirm: function() { var options = { success: function() { self.loadData(); } }; app.api.call( 'create', app.api.buildURL(self.module, 'denormalization/abort'), [], options, {context: self} ); } }); } }) } }} , "views": { "base": { "actionbutton-document-merge": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Document Merge action configuration view * * @class View.Views.Base.AdministrationActionbuttonDocumentMergeView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonDocumentMergeView * @extends View.View */ ({ // Actionbutton-document-merge View (base) events: { 'change input[type=checkbox][data-fieldname=pdf]': 'pdfChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this.isSellServe = app.user.hasSellServeLicense(); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._buttonId = options.buttonId; this._actionId = options.actionId; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { id: '', name: '', pdf: false, }; } this._properties.pdf = app.utils.isTruthy(this._properties.pdf); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSelection(); }, /** * Handle update of selected PDF Template change * * @param {UIEvent} e * */ pdfChanged: function(e) { this._properties.pdf = e.currentTarget.checked; this._updateActionProperties(); this.render(); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { if (!this._properties.id) { app.alert.show('alert_actionbutton_document_merge_nodata', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_INVALID_DATA'), messages: app.lang.get('LBL_ACTIONBUTTON_SELECT_TEMPLATE'), autoClose: true, autoCloseDelay: 5000, }); return false; } return true; }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Update action properties & UI based on selection * * @param {Object} selection * */ setValue: function(selection) { if (selection) { this._properties = { id: selection.id, name: selection.name, pdf: this._properties.pdf, }; this._updateSelect2View(); this._updateActionProperties(); } }, /** * Update Select2 selection with configured action * */ _updateSelect2View: function() { if (this.disposed) { return; } this.$('[name="dm_template_name"]').select2('data', { id: this._properties.id, text: this._properties.name }); }, /** * Update action properties in context * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Create relate field against Users module * */ _createSelection: function() { this.disposeTemplateSelectField(); var moduleName = 'DocumentTemplates'; this.model.set({ dm_template_name: this._properties.name, dm_template_id: this._properties.id, name: this._properties.name }); this._templateSelectionField = app.view.createField({ def: { type: 'relate', module: moduleName, name: 'dm_template_name', rname: 'name', id_name: 'dm_template_id', initial_filter_label: 'LBL_ACTIONBUTTON_DOCUMENT_MERGE', filter_populate: { 'template_module': { '$in': [this.model.get('module')] } }, }, view: this, viewName: 'edit', }); this._templateSelectionField.render(); this._templateSelectionField.setValue = _.bind(this.setValue, this); var templateContainer = this.$('[data-fieldname="dm_template_name"]'); templateContainer.empty(); templateContainer.append(this._templateSelectionField.$el); if (this._properties.id) { this._updateSelect2View(); } }, /** * Dipose the sidecar relate field created for template selection * */ disposeTemplateSelectField: function() { if (this._templateSelectionField) { this._templateSelectionField.dispose(); this._templateSelectionField = null; } }, /** * @inheritdoc */ _dispose: function() { this.disposeTemplateSelectField(); this._super('_dispose'); }, }) }, "actionbutton-display-settings-action-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button configuration settings view * * @class View.Views.Base.AdministrationActionbuttonDisplaySettingsActionMenuView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonDisplaySettingsActionMenuView * @extends View.View */ ({ // Actionbutton-display-settings-action-menu View (base) events: { 'change [data-fieldname=orderNumber]': 'orderNumberChanged', 'change [data-fieldname=listView]': 'changeListView', 'change [data-fieldname=recordView]': 'changeRecordView', 'change [data-fieldname=recordViewDashlet]': 'changeRecordViewDashlet', 'change [data-fieldname=subpanels]': 'changeSubpanels', 'change [data-fieldname=focusDashboardHeader]': 'changeSetting', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._updateActionMenuSettings(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { const model = options.context.get('model'); this._actionMenu = model.get('data').actionMenu; this._orderData = []; let actionViewButtonsNo = 0; const moduleMeta = app.metadata.getModule(model.get('module')); if (moduleMeta) { _.each(moduleMeta.fields, function getButton(button) { if (button.type === 'actionbutton') { this._orderData.push(this._orderData.length + 1); if (!_.isEmpty(JSON.parse(button.options).actionMenu)) { actionViewButtonsNo = actionViewButtonsNo + 1; } } }, this); } if (_.isEmpty(this._actionMenu)) { this._actionMenu = { orderNumber: actionViewButtonsNo + 1, listView: false, recordView: false, recordViewDashlet: false, focusDashboardHeader: false, subpanels: false }; } if (actionViewButtonsNo === 0) { this._orderData.push(this._orderData.length + 1); } }, /** * Updates configuration and re-renders preview * */ _updateActionMenuSettings: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.actionMenu = this._actionMenu; this.context.trigger('update-buttons-preview', buttonsData); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('.ab-admin-order').select2(); }, /** * Handle order number change event * * @param {UIEvent} e */ orderNumberChanged: function(e) { this._actionMenu.orderNumber = e.currentTarget.value; this._updateActionMenuSettings(); }, /** * Handle list view visibility property * * @param {UIEvent} e */ changeListView: function(e) { this._actionMenu.listView = e.currentTarget.checked; this._updateActionMenuSettings(); }, /** * Update record view visibility property * * @param {UIEvent} e */ changeRecordView: function(e) { this._actionMenu.recordView = e.currentTarget.checked; this._updateActionMenuSettings(); }, /** * Update record view dashlet visibility property * * @param {UIEvent} e */ changeRecordViewDashlet: function(e) { this._actionMenu.recordViewDashlet = e.currentTarget.checked; this._updateActionMenuSettings(); }, /** * Update setting. * * @param {UIEvent} e */ changeSetting: function(e) { let settingName = $(e.currentTarget).data('fieldname'); this._actionMenu[settingName] = e.currentTarget.checked; this._updateActionMenuSettings(); }, /** * Update subpanels visibility property * * @param {UIEvent} e */ changeSubpanels: function(e) { this._actionMenu.subpanels = e.currentTarget.checked; this._updateActionMenuSettings(); }, }) }, "actionbutton-tabs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button tab view * * @class View.Views.Base.AdministrationActionbuttonTabsView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonTabsView * @extends View.View */ ({ // Actionbutton-tabs View (base) events: { 'click a[data-tabId]': 'tabButtonClicked', 'click a[data-action="add"]': 'addButton', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Property initialization * */ _initProperties: function() { this.buttons = this.context.get('model').get('data').buttons; this._canDeleteButton = true; this._uiButtons = []; if (Object.keys(this.buttons).length === 0) { this._createButton(); } }, /** * Context event handling * */ _registerEvents: function() { this.listenTo(this.context.get('model'), 'refresh:ui', this.render, this); }, /** * @inheritdoc */ _render: function() { this._buildButtonsUIData(); this._super('_render'); var removeTabIcon = this.$('[data-action="remove-tab"]'); removeTabIcon.on('click', _.bind(this.deleteButton, this)); if (Object.keys(this.buttons).length <= 1) { removeTabIcon.hide(); } this._makeButtonsSortable(); }, /** * Handler for tab selection * * @param {UIEvent} e * */ tabButtonClicked: function(e) { if (this._isTabValid()) { var buttonId = e.currentTarget.dataset.tabid; this._activateTabs(false); this.buttons[buttonId].active = true; this._updateActionButtons(); this.context.get('model').trigger('update:button:view', buttonId); this.render(); } else { e.stopImmediatePropagation(); } }, /** * Handler for new button creation event * */ addButton: function() { if (this._isTabValid()) { this._createButton(); this.render(); } }, /** * Handler for button removal event * * @param {UIEvent} e * */ deleteButton: function(e) { if (this._canDeleteButton && Object.keys(this.buttons).length > 1) { app.alert.show('alert-actionbutton-delete', { level: 'confirmation', messages: app.lang.get('LBL_ACTIONBUTTON_DELETE_BUTTON'), autoClose: false, onConfirm: _.bind(function deletebutton() { var buttonId = $(e.currentTarget).data('id'); this._activateTabs(false); delete this.buttons[buttonId]; var firstButtonKey = Object.keys(this.buttons)[0]; this.buttons[firstButtonKey].active = true; this._updateActionButtons(); this.context.get('model').trigger('update:button:view', firstButtonKey); // rerender so that the tab view also goes away this.render(); }, this), }); } }, /** * Validation for button configuration * * @return {bool} */ _isTabValid: function() { var headerPane = this.layout.layout.getComponent('actionbutton-headerpane'); if (headerPane) { var isValid = headerPane.canSaveConfig(headerPane.layout); return isValid; } return false; }, /** * Builds an array that will get itterated over in hbs */ _buildButtonsUIData: function() { this._uiButtons = _.sortBy(this.buttons, 'orderNumber'); }, /** * Create a default button configuration for a new button * */ _createButton: function() { var newButtonId = app.utils.generateUUID(); this._activateTabs(false); this.buttons[newButtonId] = { active: true, buttonId: newButtonId, orderNumber: Object.keys(this.buttons).length, properties: { label: app.lang.get('LBL_ACTIONBUTTON_BUTTON'), description: '', showLabel: true, showIcon: true, colorScheme: 'primary', icon: 'sicon-settings', isDependent: false, stopOnError: false, formula: '', }, actions: {}, }; this._updateActionButtons(); this.context.get('model').trigger('update:button:view', newButtonId); }, /** * Adds the sortability feature to created tabs */ _makeButtonsSortable: function() { this.$('.nav-tabs').sortable({ revert: true, cancel: '.ab-tab-add', items: '> li:not(.ab-tab-add)', start: _.bind(function blockRemoval(event, ui) { // if we drag buttons we need to block the delete functions this._canDeleteButton = false; var initialIndex = ui.item.index(); ui.item.data('initialIndex', initialIndex); }, this), stop: _.bind(function allowRemoval(event, ui) { // when we release the button we can remove buttons once again this._canDeleteButton = true; this._reorderButtons(ui.item.data('initialIndex'), ui.item.index()); this._updateActionButtons(); }, this) }); }, /** * Reorders buttons list * * @param {number} initialOrderNumber * @param {number} finalOrderNumber */ _reorderButtons: function(initialOrderNumber, finalOrderNumber) { this._unsetButtonOrder(initialOrderNumber); _.each(this.buttons, function orderButtons(buttonData) { if (buttonData.orderNumber !== -1) { if ( initialOrderNumber > finalOrderNumber && buttonData.orderNumber >= finalOrderNumber && buttonData.orderNumber <= initialOrderNumber ) { buttonData.orderNumber = buttonData.orderNumber + 1; } if ( initialOrderNumber < finalOrderNumber && buttonData.orderNumber >= initialOrderNumber && buttonData.orderNumber <= finalOrderNumber ) { buttonData.orderNumber = buttonData.orderNumber - 1; } } }); _.each(this.buttons, function orderButtons(buttonData) { if (buttonData.orderNumber === -1) { buttonData.orderNumber = finalOrderNumber; } }); }, /** * Sets the orderNumber to -1 * * @param {number} orderNumber */ _unsetButtonOrder: function(orderNumber) { _.each(this.buttons, function unsetOrder(buttonData) { if (buttonData.orderNumber === orderNumber) { buttonData.orderNumber = -1; } }); }, /** * Toggles button active flag * * @param {bool} active * */ _activateTabs: function(active) { _.each(this.buttons, function clearTab(buttonData, buttonId) { this.buttons[buttonId].active = active; }, this); }, /** * Update context action button configuration * */ _updateActionButtons: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons = this.buttons; // update button data into the main data container ctxModel.set('data', buttonsData); this.context.trigger('update-buttons-preview', buttonsData); }, }) }, "searchbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationSearchbarView * @alias SUGAR.App.view.views.BaseAdministrationSearchbarView * @extends View.Views.Base.SearchbarView */ ({ // Searchbar View (base) /** * @inheritdoc */ className: 'admin-searchbar', /** * @inheritdoc */ extendsFrom: 'SearchbarView', /** * @inheritdoc * * @private */ _initProperties: function() { this.module = 'Administration'; this.greeting = app.lang.get('LBL_ADMIN_SEARCHBAR_GREETING', this.module); if (this.layout) { this.layout.on('admin:panel-defs:fetched', function() { this.sourceDataReady(); }, this); } }, /** * @inheritdoc * * @private */ _populateLibrary: function() { this.library = []; var defs = this.layout.getAdminPanelDefs(); _.each(defs, function(category) { _.each(category.options, function(item) { var action = { name: app.lang.get(item.label, this.module), description: app.lang.get(item.description, this.module), href: item.link }; this.library.push(action); }, this); }, this); } }) }, "package-builder": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderView * @extends View.View */ ({ // Package-builder View (base) /** * Tab views */ tabsView: [], /** * Customizations */ customizations: [], /** * Active tab */ activeTab: 'configuration', /** * Tab list */ tabList: [ { 'value': 'configuration', 'label': 'LBL_PACKAGE_BUILDER_TAB_CONFIG', 'tooltip': 'LBL_PACKAGE_BUILDER_TAB_CONFIG_HELP', 'active': true, 'disabled': false }, { 'value': 'customizations', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS', 'tooltip': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_HELP', 'active': false, 'disabled': true }, { 'value': 'packages', 'label': 'LBL_PACKAGE_BUILDER_TAB_PACKAGES', 'tooltip': 'LBL_PACKAGE_BUILDER_TAB_PACKAGES_HELP', 'active': false, 'disabled': false }, ], /** * Event handlers */ events: { 'click .configuration_tab': 'tabChanged', 'click .customizations_tab': 'tabChanged', 'click .packages_tab': 'tabChanged', 'click a[name="cancel_button"]': 'cancelClicked', }, /** * Alert */ alertId: 'pb-fetch-local-packages', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.checkIfRefetched(); this.context.set('installedPackagesFetched', false); }, /** * @inheritdoc */ render: function() { this._super('render'); this.showTabContent(); }, /** * If view was previously used and customizations were loaded, set dataRefetched true * this flag will reset the selected rows saved data */ checkIfRefetched: function() { if ('configuration' in this.tabsView && 'customizations' in this.tabsView) { this.tabsView.configuration.dataRefetched = true; } }, /** * Tab changed event * @param {Event} $el */ tabChanged: function($el) { let newTab = $el.target.name; if (newTab === 'packages' && !this.context.get('installedPackagesFetched')) { $el.stopPropagation(); this.fetchLocalPackages(); return; } this.activeTab = newTab; this.activeTabChanged(); // For the customization tab, remove the notification dot and animation if (this.activeTab === 'customizations') { this.$el.find('.' + this.activeTab + 'Dot').hide(); this.$el.find('.' + this.activeTab + '_tab').removeClass('start_animation'); } this.showTabContent(); }, /** * Change the active tab */ activeTabChanged: function() { let oldActiveTab = this.$el.find('.parentTabs .active')[0]; let newActiveTab = this.$el.find('.parentTabs .' + this.activeTab + '_tab')[0]; if (_.isUndefined(oldActiveTab) === false && _.isUndefined(newActiveTab) === false && oldActiveTab !== newActiveTab) { oldActiveTab.classList.remove('active'); // Remove previous active tab class newActiveTab.classList.add('active'); // Add active class to the current active tab } }, /** * Show tab content */ showTabContent: function() { let tabContent = this.$el.find('.tab-content'); // Before we go to the Customizations tab, make sure the customizations are synced if (this.activeTab === 'customizations') { this.syncCustomizations(); if (this.tabsView.configuration.dataRefetched === true) { this.resetCheckedRowsAndEntries(); this.tabsView.configuration.dataRefetched = false; } } if (_.isUndefined(this.tabsView[this.activeTab])) { let tabView; if (this.activeTab === 'packages') { tabView = app.view.createView({ name: 'package-builder-' + this.activeTab + '-tab', }); } else { tabView = app.view.createView({ name: 'package-builder-' + this.activeTab + '-tab', customizations: this.customizations, }); } tabView.render(); tabContent.empty(); tabContent.append(tabView.$el); this.tabsView[this.activeTab] = tabView; } else { tabContent.empty(); tabContent.append(this.tabsView[this.activeTab].$el); this.tabsView[this.activeTab].customizations = this.customizations; this.tabsView[this.activeTab].render(); } }, /** * Sync customizations */ syncCustomizations: function() { // If customizations were fetched, set them tot this.customizations if (_.isUndefined(this.tabsView.configuration) === false && _.isUndefined(this.tabsView.configuration.customizations) === false) { this.customizations = this.tabsView.configuration.customizations; } }, /** * Fetch local packages */ fetchLocalPackages: function() { let url = app.api.buildURL('Administration/package/customizations'); let data = {elementsToFetch: ['installed_packages']}; let callback = { success: function(data) { if ('installed_packages' in data) { let installedPackages = {}; _.each(data.installed_packages, function(installedPackage) { installedPackages[installedPackage.id] = installedPackage; }); this.context.set('installedPackages', installedPackages); this.context.set('installedPackagesFetched', true); } }.bind(this), error: function(errorData) { let categoryLabel = app.lang.get('LBL_PACKAGE_BUILDER_TAB_PACKAGES_I_PACKAGES_T', 'Administration'); let message = app.lang.get( 'LBL_PACKAGE_BUILDER_CATEGORY_RETRIEVE_FAILED', 'Administration', {category: categoryLabel} ); app.alert.show('pb-error', { level: 'error', messages: message }); }.bind(this), complete: function() { app.alert.dismiss(this.alertId); // Check if local packages were extracted if (!_.isUndefined(this.context.get('installedPackages'))) { this.activeTab = 'packages'; this.activeTabChanged(); this.showTabContent(); } }.bind(this) }; // Create and display progress alert app.alert.show(this.alertId, { level: 'process', messages: app.lang.get('LBL_LOADING'), autoClose: false, }); app.api.call('create', url, data, callback); }, /** * Reset checked rows and entries */ resetCheckedRowsAndEntries: function() { if (_.isUndefined(this.tabsView.customizations) || _.isUndefined(this.tabsView.customizations.customizationsTabsView)) { return; } // each view reset checkedRows and entries e.g. Fields, Dropdowns.. etc _.each(this.tabsView.customizations.customizationsTabsView, function resetSelectedRows(subtabView) { subtabView.checkedRows = []; subtabView.entries = []; }); }, /** * Cancel button clicked */ cancelClicked: function() { app.router.navigate('#Administration', {trigger: true}); }, /** * @inheritdoc */ _dispose: function() { if (_.isObject(this.tabsView.packages)) { this.tabsView.packages.dispose(); delete this.tabsView.packages; } this._super('_dispose'); if (_.isUndefined(this.tabsView.customizations) === false) { // Reset existing customizations subtabs, when existing view is disposed this.tabsView.customizations.customizationsTabsView = {}; } }, }) }, "actionbutton-assign-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Assign Record action configuration view * * @class View.Views.Base.AdministrationActionbuttonAssignRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonAssignRecordView * @extends View.View */ ({ // Actionbutton-assign-record View (base) /** * Event listeners */ events: { 'change input[type=checkbox]': 'assignToCurrentUser', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._buttonId = options.buttonId; this._actionId = options.actionId; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { id: '', name: '', assignToCurrentUser: false, }; } }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSelection(); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { // Selection is not required // Unassign a record if no user is selected return true; }, /** * Event handler for checkbox * * @param {UIEvent} e * */ assignToCurrentUser: function(e) { this._properties.assignToCurrentUser = e.currentTarget.checked; this._updateActionProperties(); this._updateSelectVisibility(); }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Update action properties & UI based on selection * * @param {Object} selection * */ setValue: function(selection) { if (selection) { this._properties = { id: selection.id, name: selection.name, assignToCurrentUser: false, }; this._updateSelect2View(); this._updateActionProperties(); } }, /** * Show or hide the select2 dropdown */ _updateSelectVisibility: function() { const $select2 = this.$('[name="preset_user_name"]'); if (this._properties.assignToCurrentUser) { $select2.prop('disabled', true); } else { $select2.prop('disabled', false); } }, /** * Update Select2 selection with configured action * */ _updateSelect2View: function() { if (this.disposed) { return; } this.$('[name="preset_user_name"]').select2('data', { id: this._properties.id, text: this._properties.name }); }, /** * Update action properties in context * */ _updateActionProperties: function() { // update action data into the main data container var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; ctxModel.set('data', buttonsData); }, /** * Create relate field against Users module * */ _createSelection: function() { this._disposeUserSelectField(); this.model.set({ preset_user_name: this._properties.name, preset_user_id: this._properties.id, name: this._properties.name, }); this.userSelectField = app.view.createField({ def: { type: 'relate', module: 'Users', name: 'preset_user_name', rname: 'name', id_name: 'preset_user_id', }, view: this, viewName: 'edit', }); this.userSelectField.render(); this.userSelectField.setValue = _.bind(this.setValue, this); this.$('[data-container="field"]').append(this.userSelectField.$el); this._updateSelectVisibility(); }, /** * Dispose the relate sidecar field * */ _disposeUserSelectField: function() { if (this.userSelectField) { this.userSelectField.dispose(); this.userSelectField = null; } }, /** * @inheritdoc */ _dipose: function() { this._disposeUserSelectField(); this._super('_dispose'); }, }) }, "actionbutton-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button configuratoin headerpane view * * @class View.Views.Base.AdministrationActionbuttonHeaderpaneView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonHeaderpaneView * @extends View.View */ ({ // Actionbutton-headerpane View (base) plugins: [ 'Editable', ], events: { 'click [data-action=close]': 'closeDrawer', 'click [data-action=save]': 'saveSettings', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Property initialization * */ _initProperties: function() { this.actionButtonLabel = this.context.get('model').get('label'); this._initialState = app.utils.deepCopy(this.context.get('model').get('data')); }, /** * Close configuration drawer * */ closeDrawer: function() { this.context.get('cancelCallback').call(this); app.drawer.close(); }, /** * Validate configured actions * * @param {View.Layout} layout * * @return {bool} */ canSaveConfig: function(layout) { var canSave = true; // go throught all our components and their subcomponents and see if the have any canSave logic var allComponents = _.union(layout._components, layout._subComponents); _.each(allComponents, function getSave(component) { if (canSave === true) { if (component.canSave && component.canSave() === false) { canSave = false; } else { canSave = this.canSaveConfig(component); } } }, this); return canSave; }, /** * Save action button configuration * */ saveSettings: function() { if (this.canSaveConfig(this.layout) && this.isDropdownValid()) { this.context.get('saveCallback').call(this, this.context.get('model').get('data')); this.closeDrawer(); } }, /** * @inheritdoc */ hasUnsavedChanges: function() { let currentState = this.context.get('model').get('data'); return !_.isEqual(currentState, this._initialState); }, /** * If the display type for button is dropdown, we are ensuring at least two buttons are configured. * * @return {bool} */ isDropdownValid: function() { const data = this.model.get('data'); if (data.settings.type === 'dropdown' && Object.keys(data.buttons).length < 2) { app.alert.show('alert_ab_min_two_buttons', { level: 'error', messages: app.lang.get('LBL_ACTIONBUTTON_INVALID_DROPDOWN_ERROR'), autoClose: true, autoCloseDelay: 5000 }); return false; } return true; } }) }, "portaltheme-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPortalThemeConfigView * @alias SUGAR.App.view.views.BasePortalThemeConfigView * @extends View.Views.Base.AdministrationConfigView */ ({ // Portaltheme-config View (base) extendsFrom: 'AdministrationConfigView', enableHeaderButtons: false, enableHeaderPane: false, /** * Sell & Serve specific text blocks */ sellServeLicencedTextBlocks: [ 'portaltheme_open_aws_settings_link' ], events: { 'click .restore-defaults-btn': 'restoreClicked', 'click a.open-aws-settings-btn': 'openAwsSettingsChatTab' }, /** * @inheritdoc */ render: function() { this._super('render'); this.displaySellServeLicensedTextBlocks(); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change', this.handleModelChange, this); }, /** * @inheritdoc */ loadSettingsSuccessCallback: function(settings) { this._super('loadSettingsSuccessCallback', [settings]); this._bindFieldEvents(); // execute field change functions once on load with default or saved data this.handleBannerBackgroundStyleChange(); }, /** * Bind field events * * @private */ _bindFieldEvents: function() { this.model.on( 'change:portaltheme_banner_background_style', this.handleBannerBackgroundStyleChange, this ); }, /** * Handle the changing of portaltheme_banner_background_style field and * properly display associated fields */ handleBannerBackgroundStyleChange: function() { var backgroundStyleField = this.getField('portaltheme_banner_background_style'); if (!backgroundStyleField) { return; } var value = backgroundStyleField.getFormattedValue(); var shouldShowBackgroundColorField; var shouldShowBackgroundImageField; var backgroundColorField = this.getField('portaltheme_banner_background_color'); var backgroundImageField = this.getField('portaltheme_banner_background_image'); switch (value) { case 'color': shouldShowBackgroundColorField = true; shouldShowBackgroundImageField = false; break; case 'image': shouldShowBackgroundColorField = false; shouldShowBackgroundImageField = true; break; default: shouldShowBackgroundColorField = shouldShowBackgroundImageField = false; break; } this._toggleFieldVisibility(backgroundColorField, shouldShowBackgroundColorField); this._toggleFieldVisibility(backgroundImageField, shouldShowBackgroundImageField); }, /** * @inheritdoc * @override * * Always place labels on top */ getLabelPlacement: function() { return true; }, /** * If the user is licensed for Sell or Serve, remove the 'hide' class from specific text blocks */ displaySellServeLicensedTextBlocks: function() { if (!app.user.hasSellServeLicense()) { return; } _.each(this.sellServeLicencedTextBlocks, function(name) { var $text = this.$el.find('[data-name=' + name + ']'); $text.removeClass('hide'); }, this); }, /** * Set 'Sugar Portal Chat' tab as 'active' in * 'Amazon Connect Settings' Administration page */ openAwsSettingsChatTab: function() { app.user.lastState.set( app.user.lastState.key('activeTab', this), '#panel_2' ); }, /** * @inheritdoc */ toggleHeaderButton: function(state) { var header = this.layout.layout._components[0].getComponent(this.name + '-header'); if (header) { header.enableButton(state); } }, /** * Handles the change event from the fields */ handleModelChange: function() { _.each(this.model.changed, function(value, key) { var field = this.getField(key); var data = this.getPreviewContextData(field, value); // no preview definition defined for field if (_.isEmpty(data.preview_components)) { return; } this.triggerPreview(data); }, this); }, /** * Triggers 'portal:config:preview' on the layout context and let * the layout component react to this event * * @param data */ triggerPreview: function(data) { var context = this.layout.context; if (context && context.get('config-layout')) { context.trigger('portal:config:preview', data); } }, /** * Get the preview_components definition from field metadata * * @param field * @return [] */ getPreviewComponentsDef: function(field) { var def = []; if (field && field.def && field.def.preview_components) { def = field.def.preview_components; } return def; }, /** * Get the prepared context data * * @param field * @param value * @return {Object} the prepared context data */ getPreviewContextData: function(field, value) { return { preview_components: this.getPreviewComponentsDef(field), preview_data: value }; }, /** * Unbind field events * * @private */ _unbindFieldEvents: function() { this.model.off( 'change:portaltheme_banner_background_style', this.handleBannerBackgroundStyleChange, this ); }, /* * Check if the field is named and has a default value set. * * @param {Object} field A field from the current view. * @return {boolean} True or false. */ hasDefaultValue: function(field) { var value = field.def.default; var isDefined = !_.isUndefined(value) && !_.isNull(value) && !_.isNaN(value); return !!field.name && isDefined; }, /** * It will set the field to its default value from metadata. * Some field types require a render to display the value correctly. * * @param {Object} field A field from the current view. */ resetFieldToDefault: function(field) { if (field.name) { var defaultValue = ''; if (this.hasDefaultValue(field)) { switch (field.type) { case 'text': defaultValue = app.lang.get(field.def.default, field.module); break; default: defaultValue = field.def.default; } } field.model.set(field.name, defaultValue); if (field.type === 'image-url') { // the previous set generates the default css, // but we must clear the inputs too // this will not alter the css file field.model.set(field.name, ''); } field.render(); this.context.trigger('portal:config:preview', {preview_components: [field.def]}); } }, /** * Restore default settings click handler. */ restoreClicked: function() { app.alert.show('restore_default_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_RESTORE_DEFAULT_PORTAL_CONFIG_CONFIRM', 'Administration'), onConfirm: _.bind(function() { this.restoreFields(); }, this) }); }, /** * Restore all fields on portaltheme-config */ restoreFields: function() { _.each(this.fields, this.resetFieldToDefault, this); }, /** * @inheritdoc */ _dispose: function() { this._unbindFieldEvents(); this.model.off('change', this.handleModelChange, this); this._super('_dispose'); } }) }, "drive-path-select": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDrivePathSelectView * @alias SUGAR.App.view.views.BaseAdminstrationDrivePathSelectView * @extends View.Views.Base.View */ ({ // Drive-path-select View (base) /** * @inheritdoc */ events: { 'click .folder': 'intoFolder', 'click .setFolder': 'setFolder', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.context.on('change:sharedWithMe', this.updateCurrentFolderPaths, this); const sharedWithMe = this.layout.getComponent('drive-path-buttons').sharedWithMe; let rootName = app.lang.getAppString('LBL_MY_FILES'); let sharedName = app.lang.getAppString('LBL_SHARED'); this.currentPathFolders = sharedWithMe ? [ {name: sharedName, folderId: 'root', sharedWithMe: true}, ] : [ {name: rootName, folderId: 'root'}, ]; this.pathIds = ['root']; this.driveType = this.context.get('driveType'); if (this.driveType === 'sharepoint') { this.pathIds = []; const sitesName = app.lang.getAppString('LBL_SITES'); this.currentPathFolders = [ {name: sitesName, folderId: null, resourceType: 'site'}, ]; } this.loadFolders(); }, /** * reset the paths folders * * @param {Context} context */ updateCurrentFolderPaths: function(context) { if (context.get('sharedWithMe')) { this.currentPathFolders = [{name: 'Shared', folderId: 'root', sharedWithMe: true},]; } else { this.currentPathFolders = [{name: 'My files', folderId: 'root'},]; } }, /** * Load folders from the drive * * @param {string} currentFolderId * @param {boolean} sharedWithMe * @param {string} driveId Used for onedrive navigation */ loadFolders: function(currentFolderId, sharedWithMe, driveId) { this.currentFolderId = currentFolderId || this.context.get('parentId'); const limit = 100; const url = app.api.buildURL('CloudDrive', 'list/folders'); app.alert.show('folders-loading', { level: 'process' }); let parentId = this.currentFolderId; if (this.driveType === 'sharepoint' && (this.displayingDocumentDrives || this.displayingSites)) { parentId = null; } let options = { parentId: parentId, sharedWithMe: sharedWithMe || false, driveId: driveId, type: this.driveType, folderPath: this.currentPathFolders, limit: limit, siteId: this.siteId, }; app.api.call('create', url, options, { success: _.bind(function(result) { app.alert.dismiss('folders-loading'); this.folders = result.files; this.displayingSites = result.displayingSites; this.displayingDocumentDrives = result.displayingDocumentDrives; this.render(); }, this), error: function(error) { app.alert.show('drive-error', { level: 'error', messages: error.message, }); }, }); }, /** * Steps into a folder * * @param {Event} evt */ intoFolder: function(evt) { if (this.driveType === 'sharepoint') { this.intoSharepointFolder(evt); return; } let event = evt.target.dataset; const currentFolderId = event.id; const currentFolderName = event.name; const driveId = event.driveid || null; const sharedWithMe = this.layout.getComponent('drive-path-buttons').sharedWithMe; if (evt.target.classList.contains('back')) { this.currentPathFolders.pop(); this.pathIds.pop(); } else { this.currentPathFolders.push({name: currentFolderName, folderId: currentFolderId, driveId: driveId}); this.pathIds.push(currentFolderId); } this.driveId = driveId; this.currentFolderName = currentFolderName; this.parentId = this.pathIds[this.pathIds.length - 2]; this.loadFolders(currentFolderId, sharedWithMe, driveId); }, /** * Special handler for Sharepoint navigation * * @param {Event} evt */ intoSharepointFolder: function(evt) { let event = evt.target.dataset; let isSite = event.site; let isDocumentDrive = event.documentlibrary; let resourceId = event.id; const resourceName = event.name; const resourceType = this.getSharePointResourceType(isSite, isDocumentDrive); if (evt.target.classList.contains('back')) { this.currentPathFolders.pop(); let lastPath = this.currentPathFolders[this.currentPathFolders.length - 1]; if (lastPath) { if (lastPath.resourceType === 'site') { isSite = true; this.displayingSites = true; } if (lastPath.resourceType === 'drive') { isDocumentDrive = true; this.displayingDocumentDrives = true; } resourceId = lastPath.id; } else { isSite = true; isDocumentDrive = false; } } else { this.currentPathFolders.push({ name: resourceName, id: resourceId, resourceType: resourceType, }); } if (isSite) { this.siteId = resourceId; this.driveId = null; this.loadFolders(resourceId, null, null); } else if (isDocumentDrive) { this.driveId = resourceId; this.loadFolders(resourceId, null, this.driveId); } else { this.loadFolders(resourceId, null, this.driveId); } }, /** * Gets the resource type * * @param {bool} isSite * @param {bool} isDocumentDrive * @return string */ getSharePointResourceType: function(isSite, isDocumentDrive) { if (isSite) { return 'site'; } if (isDocumentDrive) { return 'drive'; } return 'folder'; }, /** * Sets a folder as the current folder * * @param {Event} evt */ setFolder: function(evt) { let folders = this.currentPathFolders; const folderId = evt.target.dataset.id; const folderName = evt.target.dataset.name; let driveId = evt.target.dataset.driveid; let isSite = evt.target.dataset.site; let isDocumentDrive = evt.target.dataset.documentlibrary; const resourceType = this.getSharePointResourceType(isSite, isDocumentDrive); if (_.isArray(folders)) { if (this.driveType === 'sharepoint') { if (isSite) { this.siteId = folderId; } if (isDocumentDrive) { this.driveId = folderId; driveId = this.driveId; } folders.push({ name: folderName, id: folderId, resourceType: resourceType, }); } else { folders.push({folderId: folderId, name: folderName, driveId: driveId,}); } } const url = app.api.buildURL('CloudDrive', 'path'); app.alert.show('path-processing', { level: 'process' }); app.api.call('create', url, { pathModule: this.context.get('pathModule'), isRoot: this.context.get('isRoot'), type: this.driveType, drivePath: JSON.stringify(folders), folderId: folderId, driveId: driveId, siteId: this.siteId, isShared: this.context.get('sharedWithMe'), pathId: this.context.get('pathId'), } , { success: _.bind(function() { app.alert.dismiss('path-processing'); app.drawer.close(); }), error: function(error) { app.alert.show('drive-error', { level: 'error', messages: error.message, }); } }); }, }) }, "actionbutton-run-report": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Run Report action configuration view * * @class View.Views.Base.AdministrationActionbuttonRunReportView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonRunReportView * @extends View.View */ ({ // Actionbutton-run-report View (base) /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._buttonId = options.buttonId; this._actionId = options.actionId; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { id: '', name: '' }; } }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSelection(); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { if (this._properties.id === '') { app.alert.show('alert_actionbutton_runreport_nodata', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_INVALID_DATA'), messages: app.lang.get('LBL_ACTIONBUTTON_SELECT_RECORD'), autoClose: true, autoCloseDelay: 5000 }); return false; } return true; }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Update report selection * * @param {Object} selection * */ setValue: function(selection) { if (selection) { this._properties = { id: selection.id, name: selection.name }; this._updateSelect2View(); this._updateActionProperties(); } }, /** * Update select2 selection * */ _updateSelect2View: function() { if (this.disposed) { return; } this.$('[name="report_name"]').select2('data', { id: this._properties.id, text: this._properties.name }); }, /** * Update action button configuration * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Create sidecar relate field for report selection * */ _createSelection: function() { this.model.set({ report_name: this._properties.name, report_id: this._properties.id, name: this._properties.name, }); this._reportSelectField = app.view.createField({ def: { type: 'relate', module: 'Reports', name: 'report_name', rname: 'name', id_name: 'report_id', }, view: this, viewName: 'edit', }); this._reportSelectField.setValue = _.bind(this.setValue, this); this._reportSelectField.render(); this.$('[data-container="field"]').append(this._reportSelectField.$el); this._updateSelect2View(); }, /** * Clean up the report select field * */ _disposeReportSelectField: function() { if (this._reportSelectField) { this._reportSelectField.dispose(); this._reportSelectField = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeReportSelectField(); this._super('_dispose'); }, }) }, "maps-logger-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsLoggerConfigView * @alias SUGAR.App.view.views.BaseAdministrationMapsLoggerConfigView * @extends View.Views.Base.AdministrationMapsConfigView */ ({ // Maps-logger-config View (base) extendsFrom: 'AdministrationMapsConfigView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.meta.label = app.lang.get('LBL_MAP_CONFIG_GEOCODE_LOG_VIEWER'); }, }) }, "helplet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * A `helplet` is a view similar to a dashlet thats lives in the help * component. * * @class View.Views.Base.AdministrationHelpletView * @alias SUGAR.App.view.views.BaseAdministrationHelpletView * @extends View.View.HelpletView */ ({ // Helplet View (base) extendsFrom: 'HelpletView', /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * @param {Object} options */ _beforeInit: function(options) { this._productPageUrl = 'https://www.sugarcrm.com/crm/product_doc.php?'; this._helpMeta = {}; }, /** * Build help meta depending on context */ _computeHelpMeta: function() { const targetContext = app.controller.context; const route = targetContext.get('layout'); if (route === 'maps-config' || route === 'maps-logger-config') { this._helpMeta.route = route; this._helpMeta.moduleName = 'MapsAdmin'; this._helpMeta.label = 'LBL_SUGAR_MAPS'; } else if (route === 'drive-path') { this._helpMeta.route = route; this._helpMeta.moduleName = 'CloudDriveAdmin'; this._helpMeta.label = 'LBL_CLOUD_DRIVE'; } else { this._helpMeta.route = route; this._helpMeta.label = null; this._helpMeta.moduleName = targetContext.get('module'); }; }, /** * @inheritdoc */ createHelpObject: function(langContext) { this._computeHelpMeta(); if (this._helpMeta.moduleName === 'Administration') { this._super('createHelpObject', [langContext]); } else { this._createFeatureHelpMeta(langContext); } }, /** * Create the help object for core applications */ _createFeatureHelpMeta: function(langContext) { const helpUrl = _.extend({ more_info_url: this._createMoreHelpLink(), more_info_url_close: '</a>' }, langContext); const moduleName = app.lang.get(this._helpMeta.label, 'Administration'); const ctx = this.context.parent || this.context; this.helpObject = app.help.get(moduleName, ctx.get('layout'), helpUrl); }, /** * @inheritdoc */ _createMoreHelpLink: function() { const serverInfo = app.metadata.getServerInfo(); const lang = app.lang.getLanguage(); const products = app.user.getProductCodes().join(','); const module = this._helpMeta.moduleName; let params = { edition: serverInfo.flavor, version: serverInfo.version, lang, module, products, }; this._productPageUrl += $.param(params); return '<a href="' + this._productPageUrl + '" target="_blank">'; }, }) }, "drive-path-root-path": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDrivePathRootPathView * @alias SUGAR.App.view.views.BaseAdminstrationDrivePathRootPathView * @extends View.Views.Base.View */ ({ // Drive-path-root-path View (base) /** * Initial root id */ rootId: 'root', /** * @inheritdoc */ events: { 'click .selectRootPath': 'selectRootPath', 'click .removeRootPath': 'removeRootPath', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.driveType = this.context.get('driveType'); this.driveTypeLabel = app.lang.getAppListStrings('drive_types')[this.driveType]; /** * Load from the database the root path */ this.loadRootPath(); }, /** * Root path loading */ loadRootPath: function() { app.alert.dismissAll(); const url = app.api.buildURL('CloudDrivePaths', null, null, { max_num: -1, filter: [ { type: { $equals: this.driveType }, is_root: { $equals: 1 } } ] }); app.alert.show('path-loading', { level: 'process' }); app.api.call('read', url, null, { success: _.bind(this._renderRootPath, this), error: function(error) { app.alert.show('drive-error', { level: 'error', messages: error.message, }); }, }); }, /** * Render root path * * @param {Array} data */ _renderRootPath: function(data) { app.alert.dismiss('path-loading'); this.rootPath = _.isArray(data.records) && data.records[0] ? data.records[0] : {path: ''}; try { if (!_.isUndefined(this.rootPath)) { this.rootPathDisplay = _.map(JSON.parse(this.rootPath.path), function(item) { return item.name; }).join('/'); } } catch (err) { this.rootPathDisplay = this.driveType === 'sharepoint' ? app.lang.get('LBL_SHAREPOINT_ROOT_PATH', this.module) : ''; } this.render(); }, /** * Opent the path selection drawer * * @param {Event} evt */ selectRootPath: function(evt) { evt.preventDefault(); evt.stopPropagation(); // open the selection drawer app.drawer.open({ context: { pathModule: null, isRoot: true, parentId: 'root', driveType: this.driveType, }, layout: 'drive-path-select', }, _.bind(function() { this.loadRootPath(); }, this)); }, /** * Removes the root path * * @param {Event} evt */ removeRootPath: function(evt) { const url = app.api.buildURL('CloudDrive', 'path'); app.api.call('delete', url, { pathId: this.rootPath.id, }, { success: _.bind(function() { app.alert.show('path-deleted', { level: 'success', messages: app.lang.get('LBL_ROOT_PATH_REMOVED', this.module), }); this.loadRootPath(); }, this), error: _.bind(function(error) { app.alert.show('path-delete-error', { level: 'error', messages: error.message, }); }, this), }); } }) }, "maps-logger-controls": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for maps configuration * * @class View.Layouts.Base.AdministrationMapsLoggerControlsView * @alias SUGAR.App.view.layouts.BaseAdministrationMapsLoggerControlsView */ ({ // Maps-logger-controls View (base) /** * Event listeners */ events: { 'change [data-fieldname=logger-level]': 'loggerLevelChanged', 'click [data-fieldname=enableModule]': 'clickEnableModuleLog', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Property initialization */ _initProperties: function() { this.availableModulesLoaded = false; this.modulesWidgets = []; this._availableModulesForCurrentLicense = []; this._select2Data = this._getSelect2Data(); }, /** * Check if default data is setup */ _initDefaultData: function() { if (!this.model.get('maps_loggerLevel')) { this.model.set('maps_loggerLevel', 'error'); } if (!this.model.get('maps_loggerStartdate')) { const defaultDate = app.date().subtract(1, 'days').format('YYYY-MM-DD'); this.model.set('maps_loggerStartdate', defaultDate); } const enabledModules = this.model.get('maps_enabled_modules'); if (!enabledModules) { this.model.set('maps_enabled_modules', []); } else if (enabledModules.length > 0 && !this.model.get('enabledLoggingModules')) { //always we show directly the logs only for the first module. //then the user can select whatever module this.model.set('enabledLoggingModules', [enabledModules[0]]); } if (!this.model.get('enabledLoggingModules')) { this.model.set('enabledLoggingModules', []); } this.setAvailableSugarModules(); }, /** * Register context event handlers * */ _registerEvents: function() { this.listenTo(this.context, 'retrived:maps:config', this.configRetrieved, this); }, /** * Called when config is being retrieved from DB * * @param {Object} data */ configRetrieved: function(data) { if (this.disposed) { return; } this._initDefaultData(); this._updateUI(data); }, /** * Update the UI elements from config * * @param {Object} data */ _updateUI: function(data) { this._updateGeneralSettingsUI(data); this._updateModulesWidgets(data); }, /** * Update the module widget * * @param {Object} data */ _updateModulesWidgets: function(data) { const availableModules = this.model.get('maps_enabled_modules'); this.availableModulesLoaded = true; this.render(); this.$('[data-widget=report-loading]').hide(); if (_.isEmpty(availableModules)) { this.$('.maps-missing-modules').show(); } this.notifyLoggerDisplay(); }, /** * Update Log Level and Measuremenet Unit from config * * @param {Object} data */ _updateGeneralSettingsUI: function(data) { this._updateSelect2El('logger-level', data); const loggerStartDate = this.model.get('maps_loggerStartdate'); this.$('[data-fieldname=logger-startdate]').datepicker('setValue', loggerStartDate); }, /** * Update select2 value * * @param {string} elId * @param {Object} data */ _updateSelect2El: function(elId, data) { const dataKey = app.utils.kebabToCamelCase(elId); if (_.has(data, dataKey)) { let id = data[dataKey]; let text = app.lang.getModString(this._getSelect2Label(dataKey, data[dataKey]), this.module); this.$('[data-fieldname=' + elId + ']').select2('data', { id: id, text: text }); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); let select2Options = this._getSelect2Options({ 'minimumResultsForSearch': -1, sortResults: function(results, container, query) { results = _.sortBy(results, 'text'); return results; } }); this.$('[data-fieldname=logger-level]').select2(select2Options); this._createDatePicker(); }, /** * Create date picker widget */ _createDatePicker: function() { const userDateFormat = 'Y-m-d'; const options = { format: app.date.toDatepickerFormat(userDateFormat), weekStart: parseInt(app.user.getPreference('first_day_of_week'), 10), }; const datePicker = this.$('[data-fieldname=logger-startdate]').datepicker(options); datePicker.on('keydown', function keyDown(e) { e.preventDefault(); }); datePicker.on('changeDate', _.bind(this.startDateChanged, this)); }, /** * Event handler for start date selection change * * @param {UIEvent} e */ startDateChanged: function(e) { const startDate = e.currentTarget.value; const key = 'maps_loggerStartdate'; this.model.set(key, startDate); this.notifyLoggerDisplay(); }, /** * Event handler for log level selection change * * @param {UIEvent} e */ loggerLevelChanged: function(e) { const logLevel = e.currentTarget.value; const key = 'maps_loggerLevel'; this.model.set(key, logLevel); this.notifyLoggerDisplay(); }, /** * Notify the logger display component to update the logs */ notifyLoggerDisplay: function() { this.model.set({ offset: 0, currentPage: 1 }); this.context.trigger('retrieved:maps:logs'); }, /** * Event handler for changing the modules to be used for logging * * @param {UIEvent} e */ clickEnableModuleLog: function(e) { const moduleName = e.currentTarget.getAttribute('data-modulename'); const isChecked = e.currentTarget.checked; let enabledLoggingModules = this.model.get('enabledLoggingModules'); if (!enabledLoggingModules) { return; } const indexOfFocusedModule = enabledLoggingModules.indexOf(moduleName); if (isChecked && indexOfFocusedModule < 0) { enabledLoggingModules.push(moduleName); } else if (!isChecked && indexOfFocusedModule > -1) { enabledLoggingModules.splice(indexOfFocusedModule, 1); } this.model.set('enabledLoggingModules', enabledLoggingModules); this.notifyLoggerDisplay(); }, /** * Create generic Select2 options object * * @return {Object} */ _getSelect2Options: function(additionalOptions) { var select2Options = {}; select2Options.placeholder = app.lang.get('LBL_MAPS_SELECT_NEW_MODULE_TO_GEOCODE', 'Administration'); select2Options.dropdownAutoWidth = true; select2Options = _.extend({}, additionalOptions); return select2Options; }, /** * Data for select2 * * @return {Object} */ _getSelect2Data: function() { const data = { 'logLevel': { 'error': 'LBL_MAPS_LOGGER_LOG_ERROR', 'success': 'LBL_MAPS_LOGGER_LOG_SUCCESS', 'all': 'LBL_MAPS_LOGGER_LOG_ALL_MESSAGES', }, 'availableModules': this._availableModules, }; return data; }, /** * Get dropdown label * * @param {string} select2Id * @param {string} key * @return {string} */ _getSelect2Label: function(select2Id, key) { return this._select2Data[select2Id][key]; }, /** * Create generic Select2 component or return a cached select2 element * * @param {string} fieldname * @param {string} queryFunc * @param {boolean} reset * @param {Function} callback */ select2: function(fieldname, queryFunc, reset, callback) { if (this._select2 && this._select2[fieldname]) { return this._select2[fieldname]; }; this._disposeSelect2(fieldname); let additionalOptions = {}; if (queryFunc && this[queryFunc]) { additionalOptions.query = _.bind(this[queryFunc], this); } var el = this.$('[data-fieldname=' + fieldname + ']') .select2(this._getSelect2Options(additionalOptions)) .data('select2'); this._select2 = this._select2 || {}; this._select2[fieldname] = el; if (reset) { el.onSelect = (function select(fn) { return function returnCallback(data, options) { if (callback) { callback(data); } if (arguments) { arguments[0] = { id: 'select', text: app.lang.get('LBL_MAPS_SELECT_NEW_MODULE_TO_GEOCODE', 'Administration') }; } return fn.apply(this, arguments); }; })(el.onSelect); } return el; }, /** * Get a list of available modules */ setAvailableSugarModules() { this._availableModules = {}; _.each(app.metadata.getModules(), function getAvailableModules(moduleData, moduleName) { if (!_.contains(this._deniedModules, moduleName)) { let moduleLabel = app.lang.getModString('LBL_MODULE_NAME', moduleName); if (!moduleLabel) { moduleLabel = app.lang.getModuleName(moduleName, { plural: true }); } this._availableModulesForCurrentLicense[moduleName] = moduleLabel; if (!_.contains(this.model.get('maps_enabled_modules'), moduleName)) { this._availableModules[moduleName] = moduleLabel; } } }, this); }, /** * Dispose a select2 element */ _disposeSelect2: function(name) { this.$('[data-fieldname=' + name + ']').select2('destroy'); }, /** * Dispose datepicker element */ _disposeDatePicker: function(name) { const loggerStartDateEl = this.$('[data-fieldname=' + name + ']'); const dataPicker = loggerStartDateEl.datepicker(); const datePickerData = loggerStartDateEl.data('datepicker'); if (datePickerData && !datePickerData.hidden) { //when SC-2395 gets implemented change this to 'remove' not 'hide' loggerStartDateEl.datepicker('hide'); } if (dataPicker && _.isFunction(dataPicker.off)) { dataPicker.off('changeDate'); dataPicker.off('keydown'); } }, /** * Dispose all select2 elements */ _disposeSelect2Elements: function() { this._disposeSelect2('logger-level'); }, /** * @inheritdoc */ _dispose: function() { this._disposeSelect2Elements(); this._disposeDatePicker('logger-startdate'); this._super('_dispose'); }, }) }, "package-builder-customizations-tab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderCustomizationsTabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderCustomizationsTabView * @extends View.View */ ({ // Package-builder-customizations-tab View (base) /** * Customizations tabs */ customizationsTabs: {}, /** * Customizations tabs view */ customizationsTabsView: {}, /** * Customizations data */ customizations: false, /** * Active customizations tab */ customizationsActiveTab: '', /** * Invalid characters for package name */ invalidCharacters: ['/', '@', '!', '#', '$', '%', '*', '.', '=', '<', '>', '?', '-', '|', '\\', '^', '(', ')', '[', ']', '{', '}', ';', '+', '§', '±', ','], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.customizations = options.customizations; this.customizationsTabsView = {}; }, /** * @inheritdoc */ render: function() { this.setDefaultValues(); this.buildTabs(); // Call super this._super('render'); this.initEvents(); this.displaySelectedHeaders(); this.showCustomizationsTabContent(); }, /** * Add event listeners */ initEvents: function() { // Tabs events const tabs = this.$el.find('.customizationsTabs'); _.each(tabs, function addEvClickOnTabs(tab) { tab.addEventListener('click', this.customizationsTabChanged.bind(this)); }.bind(this)); // Init create button event if (_.isUndefined(this.context._events) || _.isEmpty(this.context._events['button:download_package_button:click'])) { this.listenTo(this.context, 'button:download_package_button:click', this.openCreatePackageDialog.bind(this)); } // Init add to local packages button event if (_.isUndefined(this.context._events) || _.isEmpty(this.context._events['button:add_to_local_packages_button:click'])) { this.listenTo( this.context, 'button:add_to_local_packages_button:click', this.addToLocalPackagesDialog.bind(this) ); } }, /** * Build tabs */ buildTabs: function() { let isFirstCategory = true; _.each(this.customizations, function(categoryData, category) { // First element will be the active tab if (isFirstCategory) { // Set category as active tab this.customizationsTabs[category].active = true; this.customizationsActiveTab = category; isFirstCategory = false; } // If we have data for this category, enable category tab if (this.categoryHasContent(category)) { this.customizationsTabs[category].disabled = false; } }.bind(this)); }, /** * Check if category has content * @param {string} categoryName * @return {boolean} */ categoryHasContent: function(categoryName) { let hasData = false; const categoryData = this.customizations[categoryName]; if (categoryName === 'miscellaneous') { const scheduledJobs = (categoryData.scheduled_jobs.map.db.schedulers.length > 0); const displayModules = (categoryData.display_modules_and_subpanels.map.db.config.length > 0); const quickCreate = (categoryData.quick_create_bar.map.db.length > 0); hasData = (displayModules || scheduledJobs || quickCreate); } else { hasData = (categoryData.length > 0); } return hasData; }, /** * Show customizations tab content */ showCustomizationsTabContent: function() { let tabContent = this.$el.find('.customizations-tab-content'); // Get table data let tableData = this.getTableData(); if (_.isUndefined(this.customizationsTabsView[this.customizationsActiveTab])) { // Create the new view let tabView = app.view.createView({ name: 'package-builder-content-table', tableData: tableData, }); tabView.render(); tabContent.empty(); tabContent.append(tabView.$el); this.customizationsTabsView[this.customizationsActiveTab] = tabView; } else { tabContent.empty(); tabContent.append(this.customizationsTabsView[this.customizationsActiveTab].$el); // If there are no entries if (_.isEmpty(this.customizationsTabsView[this.customizationsActiveTab].entries)) { // Sync with tableData, entries might have been reset when customizations were refetched this.customizationsTabsView[this.customizationsActiveTab].entries = tableData.tableEntries; } this.customizationsTabsView[this.customizationsActiveTab].render(); } }, /** * Get table data * @return {Object} */ getTableData: function() { let tableHeaders = false; let viewEntries = []; // Get selected/active tab customizations data let data = _.find(this.customizations, function(data, category) { return category === this.customizationsActiveTab; }.bind(this)); let entries = data || []; _.each(entries, function(entry) { let elementData = entry.data; if (!tableHeaders) { tableHeaders = []; for (let headerName in elementData) { tableHeaders.push(headerName); } } let viewEntry = []; for (let i = 0; i < tableHeaders.length; i++) { viewEntry.push(elementData[tableHeaders[i]]); } viewEntries.push(viewEntry); }, this); if (this.customizationsActiveTab == 'miscellaneous') { tableHeaders = ['Category', 'Description']; viewEntries = []; for (let category in data) { let desc = data[category].data.Description; viewEntries.push([category, desc]); } } return { 'tableName': this.customizationsActiveTab, 'tableHeaders': tableHeaders, 'tableEntries': viewEntries, }; }, /** * Customizations tab changed * @param {Event} $el */ customizationsTabChanged: function($el) { this.customizationsActiveTab = $el.target.getAttribute('name'); this.activeSubtabChanged(); this.showCustomizationsTabContent(); }, /** * Active subtab changed */ activeSubtabChanged: function() { let oldActiveSubtab = this.$el.find('.customizations-tab-list .tab.active')[0]; let newActiveSubtab = this.$el.find('.customizations-tab-list .' + this.customizationsActiveTab + '_tab')[0]; if (_.isUndefined(oldActiveSubtab) === false && _.isUndefined(newActiveSubtab) === false && oldActiveSubtab !== newActiveSubtab ) { oldActiveSubtab.classList.remove('active'); // Remove previous active tab class newActiveSubtab.classList.add('active'); // Add active class to the current active tab } }, /** * Set default values */ setDefaultValues: function() { this.customizationsTabs = { 'acl': { 'value': 'acl', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_ACL_T', 'active': false, 'disabled': true }, 'advanced_workflows': { 'value': 'advanced_workflows', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_ADVANCEDWORKFLOWS_T', 'active': false, 'disabled': true }, 'dashboards': { 'value': 'dashboards', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_DASHBOARDS_T', 'active': false, 'disabled': true }, 'dropdowns': { 'value': 'dropdowns', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_DROPDOWNS_T', 'active': false, 'disabled': true }, 'fields': { 'value': 'fields', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_FIELDS_T', 'active': false, 'disabled': true }, 'language': { 'value': 'language', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_LANGUAGE_T', 'active': false, 'disabled': true }, 'layouts': { 'value': 'layouts', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_LAYOUTS_T', 'active': false, 'disabled': true }, 'miscellaneous': { 'value': 'miscellaneous', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_MISCELLANEOUS_T', 'active': false, 'disabled': true }, 'relationships': { 'value': 'relationships', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_RELATIONSHIPS_T', 'active': false, 'disabled': true }, 'reports': { 'value': 'reports', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_REPORTS_T', 'active': false, 'disabled': true }, 'search_layouts': { 'value': 'search_layouts', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_SEARCHL_T', 'active': false, 'disabled': true }, 'workflows': { 'value': 'workflows', 'label': 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_WORKFLOWS_T', 'active': false, 'disabled': true }, }; }, /** * Open create package dialog */ openCreatePackageDialog: function() { if (this.customizations === false) { App.alert.show('pb_alert', { level: 'info', messages: app.lang.get('LBL_PACKAGE_BUILDER_NEED_FETCH_CUSTOMIZATIONS', 'Administration'), autoClose: true }); return; } if (!this.hasSelections()) { App.alert.show('pb_alert', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_NEED_SELECT_CUSTOMIZATIONS', 'Administration'), autoClose: true }); return; } let modalDiv = '<div id="pb_modal" class="modal hide">' + '<div class="modal-dialog">' + '<div class="modal-content">' + '<div class="modal-header" style="padding:5px 15px 5px 15px;">' + '<h4 class="inline px-1">' + app.lang.get('LBL_PACKAGE_BUILDER_DOWNLOAD_PACKAGE', 'Administration') + '</h4>' + '</div>' + '<div class="modal-body !pb-10" style="margin-left:20px; margin-bottom:20px;">' + '<div class="mt-4 mb-3 text-sm">' + app.lang.get('LBL_PACKAGE_BUILDER_PACKAGE_SAVE_MESSAGE', this.module) + '</div>' + '<div id="additional_settings">' + '<div class="mb-4" style="margin-top:5px; margin-right:40px;">' + '<label class="flex mb-1">' + app.lang.get('LBL_PACKAGE_BUILDER_PACKAGE_NAME', 'Administration') + ':</label>' + '<input type="text" name="name" value="" maxlength="255" class="input-large" ' + 'aria-label="' + app.lang.get('LBL_PACKAGE_BUILDER_PACKAGE_NAME', 'Administration') + '"' + ' style="width:100%;">' + '</div>' + '</div>' + '</div>' + '<div class="modal-footer !py-2.5 !px-0 flex justify-end gap-2.5">' + '<button type="button" class="btn btn-secondary !text-gray-800 !border-gray-300 !leading-[1.375rem] ' + 'dark:!bg-gray-800 dark:!border-gray-600 dark:!text-gray-400 ' + 'dark:hover:!bg-gray-700 dark:hover:!border-gray-400"' + 'data-bs-dismiss="modal">' + app.lang.get('LBL_PACKAGE_BUILDER_CANCEL', 'Administration') + '</button>' + '<a id="create_package_modal" class="btn btn-primary !leading-[1.5rem] !m-0" data-bs-dismiss="modal">' + app.lang.get('LBL_PACKAGE_BUILDER_DOWNLOAD_PACKAGE', 'Administration') + '</a>' + '</div>' + '</div>' + '</div>' + '</div>'; if ($('#pb_modal').length != 0) { $('#pb_modal').remove(); } let result = $(modalDiv) .appendTo('body') .modal('show'); $('#pb_modal').find('input[name="name"]').val(this._getDefaultPackageName()); result.find('#create_package_modal').on( 'click', function clickHandler(event) { let name = $('#pb_modal').find('input[name="name"]').val(); // Check if zip name is valid let invalidCharactersFound = this.getInvalidCharacters(name); if (invalidCharactersFound.length > 0) { // Show invalid characters found let lbl = app.lang.get('LBL_PACKAGE_BUILDER_TAB_CREATE_PACKAGE_INVALID_NAME', 'Administration'); app.alert.show('pb_alert_invalid_package_name', { level: 'error', messages: lbl + invalidCharactersFound, autoClose: true }); event.stopPropagation(); // stop click event return; // stop package creation } let cb = function(content) { //eslint-disable-next-line no-undef saveAs(content, this.packageName); }; const successMessage = app.lang.get('LBL_PACKAGE_BUILDER_CREATE_PACKAGE_SUCCESS', this.module); this.getFilteredCustomizations(this.createPackage.bind(this, name, cb, successMessage)); }.bind(this) ); }, /** * Open create package dialog */ addToLocalPackagesDialog: function() { if (!this.hasSelections()) { App.alert.show('pb_alert', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_NEED_SELECT_CUSTOMIZATIONS', 'Administration'), autoClose: true }); return; } const modalDialog = app.template.getView(this.name + '.modal-dialog', this.module); const args = { packageName: this._getDefaultPackageName() }; const modalId = '#pb_modal_local_package'; if ($(modalId).length != 0) { $(modalId).remove(); } $(modalDialog(args)).appendTo('body').modal('show').find('#add_to_local').on( 'click', (event) => { let name = $(modalId).find('input[name="name"]').val(); if (!this.validatePackageName(name)) { event.stopPropagation(); // stop click event return; // stop package creation } this.getFilteredCustomizations( this.createPackage.bind( this, name, (content) => { const data = new FormData(); const file = new File([content], name + '.zip', {type: 'application/zip'}); data.append('upgrade_zip', file); const options = { 'skipMetadataHash': true, 'contentType': false, 'processData': false, success: function(data) { if (this.context && data.id) { let installedPackages = this.context.get('installedPackages') || {}; installedPackages[data.id] = data; this.context.set('installedPackages', installedPackages); } }.bind(this) }; if (file.size > app.config.uploadMaxsize) { const bytesToMB = 1000000; const maxSizeMB = Math.round(app.config.uploadMaxsize / bytesToMB); let message = app.lang.get( 'LBL_PACKAGE_BUILDER_TAB_PACKAGES_MAX_SIZE_ERR', 'Administration' ); message = message.replace('<max_size>', maxSizeMB); app.alert.dismiss('pb_alert'); app.alert.show('pb_alert', { level: 'error', messages: message, autoClose: true, autoCloseDelay: 5000 }); return; } app.api.call('create', app.api.buildURL('Administration/packages'), data, null, options); }, app.lang.get('LBL_PACKAGE_BUILDER_ADD_TO_LOCAL_SUCCESS_MESSAGE', this.module) ) ); } ); }, /** * Validate package name * @param {string} name * @return {boolean} */ validatePackageName: function(name) { let invalidCharactersFound = this.getInvalidCharacters(name); if (invalidCharactersFound.length > 0) { // Show invalid characters found let lbl = app.lang.get('LBL_PACKAGE_BUILDER_TAB_CREATE_PACKAGE_INVALID_NAME', 'Administration'); app.alert.show('pb_alert_invalid_package_name', { level: 'error', messages: lbl + invalidCharactersFound, autoClose: true }); return false; } return true; }, /** * Check if we have any selections * @return {boolean} */ hasSelections: function() { let selectedItems = []; let selectionsFound = false; // Check if we got any selection, if we found at least one selection retrun false _.each(this.customizationsTabsView, function(categoryView) { selectedItems = categoryView.getSelection(); if (selectedItems.length > 0) { selectionsFound = true; return; // we have selections, stop iteration } }, this); return selectionsFound; }, /** * Get invalid characters * @param {string} name * @return {string} */ getInvalidCharacters: function(name) { let result = ''; _.each(this.invalidCharacters, function(invalidCh) { // If invalid character found if (name.includes(invalidCh)) { // Append to result result = result + invalidCh + ' '; } }.bind(this)); return result; }, /** * Get filtered customizations * @param {Function} callback */ getFilteredCustomizations: function(callback) { this.filteredCustomizations = {}; this.summaryRestrictions = {}; _.each(this.customizationsTabsView, function(currentElement) { this.summaryRestrictions[currentElement.title] = currentElement.getSelection(); }, this); for (let category in this.summaryRestrictions) { this.filteredCustomizations[category] = {}; this.filteredCustomizations[category].map = {}; this.filteredCustomizations[category].map.files = []; this.filteredCustomizations[category].map.db = {}; if (category === 'miscellaneous') { for (let i = 0; i < this.summaryRestrictions[category].length; i++) { let subcategory = this.summaryRestrictions[category][i].Category; let categoryCustomizationsMap = this.customizations[category][subcategory].map; this.filteredCustomizations[subcategory] = {}; this.filteredCustomizations[subcategory].map = {}; this.filteredCustomizations[subcategory].map.files = []; this.filteredCustomizations[subcategory].map.db = {}; this.filteredCustomizations[subcategory].map.db = categoryCustomizationsMap.db; this.filteredCustomizations[subcategory].map.files = categoryCustomizationsMap.files; } } else if (category === 'fields') { this.getElementsDataForFields(this.summaryRestrictions[category]); } else if (category !== 'dashboards' && category !== 'advanced_workflows') { this.getElementsData(category, this.summaryRestrictions[category]); } } // Fetch db data for dashboards and advanced workflows if (!_.isEmpty(this.summaryRestrictions.dashboards) || !_.isEmpty(this.summaryRestrictions.advanced_workflows)) { // Fetch db data for dashboards and advanced workflows let elements = { dashboards: this.summaryRestrictions.dashboards || [], advanced_workflows: this.summaryRestrictions.advanced_workflows || [] }; this.fetchDbData(elements, callback); } else if (_.isFunction(callback)) { callback(); } }, /** * Create package * @param {string} packageName * @param {Function} callbackFunction * @param {string} successMessage */ createPackage: function(packageName, callbackFunction, successMessage) { let url = app.api.buildURL('Administration/package'); let data = { customizations: this.filteredCustomizations, packageName: packageName }; let callback = function(data) { if (data === false) { app.alert.dismiss('pb_loading'); app.alert.show('pb_alert', { level: 'warning', messages: app.lang.get('LBL_PACKAGE_BUILDER_TAB_CREATE_PACKAGE_OVER_LIMIT', 'Administration'), autoClose: false }); return; } this.packageName = data.package_info.packageName; let filesMapping = []; for (let category in data) { let filesMap = false; if (data[category] && data[category].map && data[category].map.files) { filesMap = data[category].map.files; } if (filesMap) { for (let index = 0; index < filesMap.length; index++) { let fileInfo = filesMap[index]; filesMapping.push({ path: fileInfo.path, content: fileInfo.content }); } } } let map = {}; for (let index1 = 0; index1 < filesMapping.length; index1++) { let currentPath = map; let path = filesMapping[index1].path; path = path.split('/'); for (let i = 0; i < path.length; i++) { let currentPathSection = path[i]; if (!currentPath[currentPathSection]) { // eslint-disable-next-line max-depth if (i == path.length - 1) { currentPath[currentPathSection] = { content: filesMapping[index1].content }; } else { currentPath[currentPathSection] = {}; } } currentPath = currentPath[currentPathSection]; } } /** * Recursive function that creates the archive * @param {Object} map the folder mapping * @param {Object} archiveFolder current folder of archive * @return {null} */ function createArchive(map, archiveFolder) { for (let i in map) { if (map[i].content) { archiveFolder.file(i, map[i].content, { base64: true }); } else { let folder = archiveFolder.folder(i); createArchive.call(this, map[i], folder); } } } //eslint-disable-next-line no-undef let packageZip = new JSZip(); createArchive.call(this, map, packageZip); packageZip.generateAsync({type: 'blob'}).then( function cb(content) { app.alert.dismiss('pb_loading'); app.alert.show('pb_alert', { level: 'success', messages: successMessage, autoClose: true }); if (callbackFunction) { callbackFunction.call(this, content, successMessage); } }.bind(this) ); }.bind(this); app.api.call('create', url, data, { success: callback }); app.alert.show('pb_loading', { level: 'process', title: app.lang.get('LBL_PACKAGE_BUILDER_CREATING_PACKAGE', 'Administration'), }); }, /** * Get elements data for fields * @param {Array} elements */ getElementsDataForFields: function(elements) { _.each(elements, function(elementData, pos, requestedElements) { let category = 'fields'; let result = _.find(this.customizations[category], function(value) { return value.data.Name === elementData.Name && value.data.Type === elementData.Type && value.data['Custom Module'] === elementData['Custom Module']; }); if (result) { let customizationElement = result.map; let elementFiles = customizationElement.files; _.each(elementFiles, function(fileData) { this.filteredCustomizations[category].map.files.push(fileData); }, this); /*eslint-disable*/ let dbData = customizationElement.db; if (dbData.length !== 0) { for (let tableName in dbData) { if (!this.filteredCustomizations[category].map.db[tableName]) { this.filteredCustomizations[category].map.db[tableName] = []; } let tableDBEntries = dbData[tableName]; _.each(tableDBEntries, function(dbRow) { this.filteredCustomizations[category].map.db[tableName].push(dbRow); }, this); } } switch (elementData.type) { case 'relate': //we need to include also the id_name data let fieldMetaData = result.map.db.fields_meta_data[0]; if (fieldMetaData && fieldMetaData.ext3) { this._addFieldToPackageIfNeeded( requestedElements, category, fieldMetaData.ext3, elementData ); } break; case 'currency': //we need to take also the related fields. Generally base_rate and currency_id if (_.isArray(result.map.related_fields)) { _.each(result.map.related_fields, function addCurrencyRelatedField(fieldName) { this._addFieldToPackageIfNeeded(requestedElements, category, fieldName, elementData); }, this); } break; } /*eslint-enable*/ } }, this); }, /** * Add field to package if needed * @param requestedElements {array} * @param category {String} * @param fieldName {String} * @param elementData {Object} * @private */ _addFieldToPackageIfNeeded: function(requestedElements, category, fieldName, elementData) { if (requestedElements.findIndex(function idIsAlreadySelected(el) { return el.name === fieldName; }) === -1 ) { let relatedField = this.customizations[category] .find(function findRelated(entry) { return entry.data.name === fieldName && entry.data.custom_module === elementData.custom_module; }); if (relatedField !== undefined) { this.getElementsDataForFields([relatedField.data]); } } }, /** * Get elements data * @param {string} category * @param {Array} elements */ getElementsData: function(category, elements) { _.each(elements, function(elementData) { let result; let customizationsEntries = this.customizations[category]; if (category === 'dashboards' || category === 'advanced_workflows') { result = elementData; } else { let elementValuesStringify = JSON.stringify(Object.values(elementData)); result = _.find(customizationsEntries, function findFunc(value) { return JSON.stringify(Object.values(value.data)) === elementValuesStringify; }); } if (result) { let customizationElement = result.map; let elementFiles = customizationElement.files; _.each(elementFiles, function(fileData) { this.filteredCustomizations[category].map.files.push(fileData); }, this); /*eslint-disable*/ let dbData = customizationElement.db; if (dbData.length !== 0) { for (let tableName in dbData) { if (!this.filteredCustomizations[category].map.db[tableName]) { this.filteredCustomizations[category].map.db[tableName] = []; } let tableDBEntries = dbData[tableName]; _.each(tableDBEntries, function(dbRow) { this.filteredCustomizations[category].map.db[tableName].push(dbRow); }, this); } } /*eslint-enable*/ } }, this); }, /** * Fetch db data * @param {Array} customizations * @param {Function} callback */ fetchDbData: function(customizations, callback) { let url = app.api.buildURL('Administration', 'package/data'); let self = this; let callbacks = { 'success': function(data, request) { if (self.disposed) { return; } _.each(data, function(categoryData, category) { self.getElementsData(category, categoryData); }); if (_.isFunction(callback)) { callback(); } }, 'error': function() { if (self.disposed) { return; } App.alert.show('pb_loading_data', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_ERROR_LOADING_DATA', 'Administratio'), autoClose: true }); }, }; let data = {}; _.each(customizations, function(elements, category) { data[category] = []; let customizationsEntries = self.customizations[category]; if (category === 'advanced_workflows' || category === 'dashboards') { _.each(elements, function(selectedEl, key) { let result = _.find(customizationsEntries, function(value) { // Compare just values as headers might differ for some tabs return JSON.stringify(Object.values(value.data)) === JSON.stringify(Object.values(selectedEl)); }); data[category].push(result); }); } }); app.api.call('create', url, data, callbacks); }, /** * Display selected headers */ displaySelectedHeaders: function() { _.each(this.customizationsTabsView, function(subtabView) { // In case of refetched data, the checked rows will be reset if (subtabView.checkedRows.length === 0) { return; } // Get selected rows length let selectedCount = subtabView.$el.find('tbody').find('.sicon-check-circle-lg').length; let displayText = ''; if (selectedCount > 0) { let totalCount = subtabView.entries.length; displayText = '(' + selectedCount + '/' + totalCount + ')'; } let classPath = '.' + subtabView.title + '_tab #selected_display'; // Tab header class path this.$el.find(classPath).html(displayText); // Set display text }.bind(this)); }, /** * Get default package name * @return {string} */ _getDefaultPackageName: function() { let customFormatDateNumbers = function(number) { let sliceNr = -2; return ('0' + number).slice(sliceNr); }; let today = new Date(); return 'PackageBuilderBundle_' + today.getFullYear() + '_' + customFormatDateNumbers(today.getMonth() + 1) + '_' + customFormatDateNumbers(today.getDate()) + '_' + customFormatDateNumbers(today.getHours()) + customFormatDateNumbers(today.getMinutes()) + customFormatDateNumbers(today.getSeconds()); } }) }, "maps-module-settings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsModuleSettingsView * @alias SUGAR.App.view.views.BaseAdministrationMapsModuleSettingsView */ ({ // Maps-module-settings View (base) /** * Event listeners */ events: { 'change [data-fieldname=autopopulate]': 'autopopulateChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { this._settings = { autopopulate: { 'false': app.lang.get('LBL_NO'), 'true': app.lang.get('LBL_YES'), } }; if (options.widgetModule) { this.widgetModule = options.widgetModule; } }, /** * Property initialization * */ _initProperties: function() { if (this.context.safeRetrieveModulesData) { const currentSettings = this.context.safeRetrieveModulesData(this.widgetModule); if (_.isEmpty(currentSettings[this.widgetModule].settings) || (!_.has(currentSettings[this.widgetModule].settings, 'autopopulate')) ) { this._notifyAutopopulateChanged(false); } } }, /** * @inheritdoc */ _render: function() { this._super('_render'); let select2Options = this._getSelect2Options(); this._disposeSelect2('autopopulate'); this.$('[data-fieldname=autopopulate]').select2(select2Options); this._updateUI(); }, /** * Update UI elements with saved data */ _updateUI: function() { const _settings = this._getSettings(); const autopopulate = app.utils.isTruthy(_settings.autopopulate) ? 'true' : 'false'; this.$('[data-fieldname=autopopulate]').select2('data', { id: autopopulate, text: this._settings.autopopulate[autopopulate] }); }, _getSettings: function() { const _modulesData = this.context.safeRetrieveModulesData(this.widgetModule); return _modulesData[this.widgetModule].settings; }, /** * Create generic Select2 options object * * @return {Object} */ _getSelect2Options: function() { var select2Options = { minimumResultsForSearch: -1 }; return select2Options; }, /** * Event handler for unit type selection change * * @param {UIEvent} e * */ autopopulateChanged: function(e) { const unitType = e.currentTarget.value; this._notifyAutopopulateChanged(unitType); }, /** * Dispose a select2 element */ _disposeSelect2: function(name) { this.$('[data-fieldname=' + name + ']').select2('destroy'); }, /** * Dispose all select2 elements */ _disposeSelect2Elements: function() { this._disposeSelect2('autopopulate'); }, /** * Notify parent about the change of the settings * * @param {string} unitType */ _notifyAutopopulateChanged: function(unitType) { const value = app.utils.isTruthy(unitType); const widgetModule = this.widgetModule; let modulesData = this.context.safeRetrieveModulesData(widgetModule); modulesData[widgetModule].settings.autopopulate = value; this.model.set('maps_modulesData', modulesData); }, /** * @inheritdoc */ _dispose: function() { this._disposeSelect2Elements(); this._super('_dispose'); }, }) }, "administration-errors": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationErrorsView * @alias SUGAR.App.view.views.BaseAdministrationErrorsView * @extends View.Views.Base.View */ ({ // Administration-errors View (base) /** * Errors * @property {Array} */ errors: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.fetchErrors(); }, /** * Fetch errors and render if successful */ fetchErrors: function() { this.errors = []; app.api.call('read', app.api.buildURL(this.module, 'errors'), [], { success: _.bind(function(errors) { if (this.disposed) { return; } this.errors = errors; this.render(); this.updateContentGridWrapperStyles(); }, this) }); }, /** * Update styles on the content-grid-wrapper */ updateContentGridWrapperStyles: function() { let contentGridWrapper = this.layout.getComponent('content-grid-wrapper'); if (!contentGridWrapper) { return; } if (!_.isEmpty(this.errors)) { contentGridWrapper.$el.removeClass('pt-8'); } else { contentGridWrapper.$el.addClass('pt-8'); } }, }) }, "module-names-and-icons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationModuleNamesAndIconsView * @alias SUGAR.App.view.views.BaseAdministrationModuleNamesAndIconsView * @extends View.Views.Base.ConfigPanelView */ ({ // Module-names-and-icons View (base) extendsFrom: 'ConfigPanelView', events: { 'click a[name="cancel_button"]': 'cancelConfig', 'click a[name="save_button"]:not(.disabled)': 'saveConfig' }, /** * Store fields that fail validation */ invalidFields: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.metaFields = this._getMetaFields(); this._currentUrl = Backbone.history.getFragment(); this.model.set({'language_selection': app.lang.getLanguage()}); this.fetchModules(); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); app.routing.before('route', this.beforeRouteChange, this); this.listenTo(this.model, 'change:language_selection', this.fetchModules); this.listenTo(this.collection, 'change:module_display_type', this._displayTypeChanged); }, /** * Gets the set of field definitions on the view from metadata * * @return {Object} the map of {field name} => {field def} from meta fields * @private */ _getMetaFields: function() { let metaFields = []; let panelFields = this.options.meta.panels ? _.first(this.options.meta.panels).fields : []; _.each(panelFields, function(panelField) { metaFields.push(panelField); if (panelField.fields) { _.each(panelField.fields, function(subfield) { metaFields.push(subfield); }, this); } }, this); return _.object(_.pluck(metaFields, 'name'), metaFields); }, /** * @inheritdoc * @return {boolean} */ beforeRouteChange: function() { let isDirty = _.some(this.collection.models, function(model) { return model.changedAttributes(); }); if (isDirty) { let targetUrl = Backbone.history.getFragment(); // Replace the url hash back to the current staying page app.router.navigate(this._currentUrl, {trigger: false, replace: true}); app.alert.show('leave_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_WARN_UNSAVED_CHANGES', this.module), onConfirm: _.bind(function() { this.collection.reset(); if (app.drawer.count()) { app.drawer.close(); } app.router.navigate(targetUrl, {trigger: true}); }, this), onCancel: $.noop }); return false; } return true; }, /** * @inheritdoc */ _render: function() { this._super('_render'); // For each model in the collection, adjust the display type column to // show/operate correctly based on the selected display type _.each(this.collection.models, function(model) { this._adjustModelDisplayTypeField(model); }, this); }, /** * Handles when the display type field changes on a model * * @param {Backbone.Model} model the module model that was changed * @private */ _displayTypeChanged: function(model) { // Revert any changes made to the last shown display type field let displayType = model.get('module_display_type'); let revertField = displayType === 'icon' ? 'module_abbreviation' : 'module_icon'; model.set(revertField, model._syncedAttributes[revertField] || ''); this._adjustModelDisplayTypeField(model); }, /** * Adjusts the Display Type column for the given model so the subfields * show/operate correctly based on the selected display type * * @param {Backbone.Model} model the model representing the module settings * @private */ _adjustModelDisplayTypeField: function(model) { let displayType = model.get('module_display_type'); let iconField = this.getField('module_icon', model); let abbreviationField = this.getField('module_abbreviation', model); if (iconField) { iconField.def.required = displayType === 'icon'; iconField.render(); iconField.$el.parent().toggle(displayType === 'icon'); } if (abbreviationField) { abbreviationField.def.required = displayType === 'abbreviation'; abbreviationField.render(); abbreviationField.$el.parent().toggle(displayType === 'abbreviation'); } }, /** * Function to do the api call to fetch the renamable module list with values */ fetchModules: function() { let options = { success: _.bind(function(modulesData) { let newModels = []; modulesData.forEach(function(moduleData) { let model = new Backbone.Model(moduleData); model.fields = app.utils.deepCopy(this.metaFields); model._syncedAttributes = app.utils.deepCopy(model.attributes); newModels.push(model); }, this); this.collection.reset(newModels); this.render(); }, this), error: _.bind(this._showErrorAlert, this), complete: _.bind(function() { app.alert.dismiss('module-names-and-icons-loading'); }, this), }; app.alert.show('module-names-and-icons-loading', { level: 'process', title: app.lang.get('LBL_LOADING'), }); app.api.call('read', this._getConfigURL(), [], options, {context: this}); }, /** * Click handler for the save button, triggers save event */ saveConfig: function() { if (!this.validateCollection()) { this._showErrorAlert({ message: 'ERR_RESOLVE_ERRORS' }); return; } if (this.triggerBefore('save')) { let saveButton = this.getField('save_button'); if (saveButton && _.isFunction(saveButton.setDisabled)) { saveButton.setDisabled(true); } this._saveConfig(); } }, /** * Validates each model in the collection */ validateCollection: function() { let isValid = true; _.each(this.collection.models, function(model) { isValid = this.validateModel(model) && isValid; }, this); return isValid; }, /** * Validates the fields of a given model. Applies error styling to the * field if errors are encountered * * @param {Backbone.Model} model The model that was changed */ validateModel: function(model) { let isValid = true; _.each(_.keys(model.fields), function(fieldName) { let field = this.getField(fieldName, model); if (!field) { return; } let fieldEl = field.$el; fieldEl.removeClass('error'); if (field.def.required && _.isEmpty(model.get(fieldName))) { fieldEl.addClass('error'); isValid = false; } }, this); return isValid; }, /** * Function to show alert message * * @param err [Error] if an error occurred, this value will be filled * @private */ _showErrorAlert: function(err) { let saveButton = this.getField('save_button'); if (saveButton && _.isFunction(saveButton.setDisabled)) { saveButton.setDisabled(false); } app.alert.show('module-names-and-icons-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: err.message, }); }, /** * Calls the context model save and saves the config model in case * the default model save needs to be overwritten * * @protected */ _saveConfig: function() { let options = { success: _.bind(function() { this.showSavedConfirmation(); this.collection.reset(); if (app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } else { app.router.navigate(this.module, {trigger: true}); } app.sync(); }, this), error: _.bind(this._showErrorAlert, this), complete: _.bind(function() { app.alert.dismiss('module-names-and-icons-save'); }, this), }; app.alert.show('module-names-and-icons-save', { level: 'process', title: app.lang.get('LBL_SAVING'), autoClose: false }); app.api.call('update', this._getConfigURL(), this._getSaveConfigAttributes(), options); }, /** * Extensible function that returns the module/config URL for save * * @return {string} The Config Save URL * @protected */ _getConfigURL: function() { return app.api.buildURL(this.module, `module-names-and-icons/${this.model.get('language_selection')}`); }, /** * Extensible function that returns the model attributes for save * * @return {Object} The Config Save attributes object * @protected */ _getSaveConfigAttributes: function() { let changedModules = []; this.collection.models.forEach(function(model) { if (Object.keys(model.changedAttributes()).length > 0) { changedModules.push({ module_key: model.get('module_key'), module_name: model.get('module_name'), module_singular: model.get('module_singular'), module_plural: model.get('module_plural'), module_display_type: model.get('module_display_type'), module_abbreviation: model.get('module_abbreviation'), module_icon: model.get('module_icon'), module_color: model.get('module_color'), }); } }); return { changedModules: changedModules }; }, /** * Show the saved confirmation alert * * @param {Object|Undefined} [onClose] the function fired upon closing. */ showSavedConfirmation: function(onClose) { onClose = onClose || function() {}; app.alert.dismiss('module-names-and-icons-save'); var alert = app.alert.show('module_config_success', { level: 'success', title: app.lang.get('LBL_MODULE_NAMES_AND_ICONS_SETTINGS', this.module, this.moduleLangObj) + ':', messages: app.lang.get('LBL_MODULE_NAMES_AND_ICONS_SETTINGS_SAVED', this.module, this.moduleLangObj), autoClose: true, autoCloseDelay: 10000, onAutoClose: _.bind(function() { alert.getCloseSelector().off(); onClose(); }) }); var $close = alert.getCloseSelector(); $close.on('click', onClose); app.accessibility.run($close, 'click'); }, /** * Cancels the changing module names and icons process and redirects back */ cancelConfig: function() { if (this.triggerBefore('cancel')) { // If we're inside a drawer if (app.drawer.count()) { // close the drawer app.drawer.close(this.context, this.context.get('model')); } else { app.router.navigate(this.module, {trigger: true}); } } }, /** * @inheritdoc */ dispose: function() { app.routing.offBefore('route', this.beforeRouteChange, this); this.stopListening(); this._super('dispose'); } }) }, "aws-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationAwsConfigView * @alias SUGAR.App.view.views.BaseAdministrationAwsConfigView * @extends View.Views.Base.AdministrationConfigView */ ({ // Aws-config View (base) extendsFrom: 'AdministrationConfigView', /** * Label of the help text of login url */ endPointHelpLabel: 'LBL_AWS_LOGIN_URL_HELP_TEXT', defaultIdentityProvider: 'Connect', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.meta.firstNonHeaderPanelIndex = 0; // there is no header, so it's always 0 }, /** * @inheritdoc */ loadSettingsSuccessCallback: function(settings) { this._super('loadSettingsSuccessCallback', [settings]); this._bindEvents(); }, /** * Attach events to fields * @inheritdoc */ _bindEvents: function() { var self = this; var nameField = this.getField('aws_connect_instance_name'); var regionField = this.getField('aws_connect_region'); var setRegionRequired = _.bind(function() { var input = nameField.$('input'); var required = input.val() ? !!input.val().trim() : !!input.val(); if (regionField.def.required !== required) { var metaRegionField = _.findWhere(this.options.meta.panels[0].fields, {'name': regionField.name}); regionField.def.required = metaRegionField.required = required; regionField._render(); } }, this); nameField.$el.on('input', function() { var isEmptyName = !$(this).find('input').val().length; setRegionRequired(); if (isEmptyName) { self.model.set('aws_connect_region', ''); } regionField.setDisabled(isEmptyName); }); setRegionRequired(); this.model.on( 'change:aws_connect_identity_provider', this._toggleEndpointField, this ); this._toggleEndpointField(); this.model.on( 'change:aws_connect_enable_portal_chat', this._toggleChatSettings, this ); this._toggleChatSettings(); }, /** * On a successful save the Save button has to be disabled and * a message will be shown indicating that the settings have been saved. * * @param {Object} settings The aws connect settings. */ saveSuccessHandler: function(settings) { this.updatePendoMetadata(settings); this._super('saveSuccessHandler', [settings]); }, /** * Show an error message if the settings could not be saved. */ saveErrorHandler: function() { app.alert.show(this.settingPrefix + '-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: app.lang.get('LBL_AWS_SAVING_ERROR', this.module), }); }, /** * Hide/Show api gateway and contact flow id fields depending on whether the chat is enabled. If * the fields are shown, they is required. If the field is hidden, we remove the * required metadata to avoid an error during save. * * @private */ _toggleChatSettings: function() { var enableChatField = this.getField('aws_connect_enable_portal_chat'); var required = !!enableChatField.getFormattedValue(); var fields = [ 'aws_connect_api_gateway_url', 'aws_connect_contact_flow_id', 'aws_connect_instance_id' ]; _.each(fields, function(fieldName) { var field = this.getField(fieldName); var metaAField = _.findWhere(this.options.meta.panels[1].fields, {'name': field.name}); field.def.required = metaAField.required = required; field.render(); this._toggleFieldVisibility(field, required); }, this); if (enableChatField.$el) { enableChatField.$el.closest('.tab-pane').find('.admin-config-help-block').toggle(required); } }, /** * Hide/Show endpoint url field depending on identity provider value. If * the field is shown, it is required. If the field is hidden, we remove the * required metadata to avoid an error during save. * * @private */ _toggleEndpointField: function() { // Get our fields and field metadata var identityField = this.getField('aws_connect_identity_provider'); var endpointField = this.getField('aws_login_url'); var metaEndpointField = _.findWhere(this.options.meta.panels[0].fields, {'name': endpointField.name}); // Set the `required` attribute accordingly var required = identityField.getFormattedValue() !== this.defaultIdentityProvider; endpointField.def.required = metaEndpointField.required = required; endpointField.render(); // Show/Hide the help text of login url var endpointHelp = this.$el.find('li.' + this.endPointHelpLabel); endpointHelp.toggle(required); // Hide or show the field this._toggleFieldVisibility(endpointField, required); }, /** * Get fields to validate * @return {Object} */ getFieldsToValidate: function() { return _.union( this.options.meta.panels[0].fields, this.options.meta.panels[1].fields ); }, /** * Render the help blocks in their respective tabpanel. * * @inheritdoc */ renderHelpBlock: function() { _.each(this.helpBlock, function(help, name) { var $panel = this.$('#' + name + this.cid); if ($panel) { $panel.append(help); } }, this); }, /** * Update pendo metadata when saving new connect settings * * @param settings */ updatePendoMetadata: function(settings) { settings = _.pick(settings, ['aws_connect_url', 'aws_connect_instance_name']); app.utils.updatePendoMetadata({}, settings); } }) }, "maps-logger-details-modal": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsLoggerDetailsModalView * @alias SUGAR.App.view.views.AdministrationMapsLoggerDetailsModalView * @extends View.View */ ({ // Maps-logger-details-modal View (base) /** * @inheritdoc */ events: { 'click .close': 'closeModal', 'click [class="modal-backdrop in"]': 'closeModal', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(options); }, /** * Init properties * * @param {Object} options */ _initProperties: function(options) { this.detailedLogs = options.detailedLogs; }, /** * Open Detail Modal */ openModal: function() { this.render(); let modalEl = this.$('[data-content=maps-logger-details-modal]'); modalEl.modal({ backdrop: 'static' }); modalEl.modal('show'); modalEl.on('hidden.bs.modal', _.bind(function handleModalClose() { this.$('[data-content=maps-logger-details-modal]').remove(); }, this)); }, /** * Close the modal and destroy it */ closeModal: function() { this.dispose(); }, /** * @inheritdoc */ _dispose: function() { this.$('[data-content=maps-logger-details-modal]').remove(); $('.modal-backdrop').remove(); this._super('_dispose'); }, }) }, "actionbutton-side-tabs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button tab view * * @class View.Views.Base.AdministrationActionbuttonSideTabsView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonSideTabsView * @extends View.View */ ({ // Actionbutton-side-tabs View (base) events: { 'click li [data-tabId]': 'tabButtonClicked', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initial setup of properties * */ _initProperties: function() { }, /** * Context model event registration * */ _registerEvents: function() { }, /** * Handler for tab selection * * @param {UIEvent} e * */ tabButtonClicked: function(e) { var tabId = e.currentTarget.dataset.tabid; this.context.get('model').trigger('update:side-pane:view', tabId); }, }) }, "maps-logger-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsLoggerHeaderView * @alias SUGAR.App.view.views.BaseAdministrationMapsLoggerHeaderView * @extends View.Views.Base.AdministrationConfigHeaderView */ ({ // Maps-logger-header View (base) extendsFrom: 'AdministrationConfigHeaderView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Property initialization * */ _initProperties: function() { //remove the main dropdown with save button since this is a log viewer if (_.has(this.meta, 'buttons')) { this.meta.buttons = _.chain(this.meta.buttons) .filter(function map(button) { if (!(button.name === 'main_dropdown' && button.type === 'actiondropdown')) { return button; } }) .value(); } }, /** * @inheritdoc */ enableButton: function(flag) { const saveButton = this.getField('save_button'); if (saveButton) { saveButton.setDisabled(!flag); } }, }) }, "maps-logger-display": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for maps configuration * * @class View.Layouts.Base.AdministrationMapsLoggerDisplayView * @alias SUGAR.App.view.layouts.BaseAdministrationMapsLoggerDisplayView */ ({ // Maps-logger-display View (base) extends: 'BasePaginationView', /** * Event listeners */ events: { 'click [data-action=paginate-prev]': 'clickPrevPage', 'click [data-action=paginate-next]': 'clickNextPage', 'click [data-action=log-details]': 'clickLogDetails', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this.listenTo(this.context, 'retrieved:maps:logs', this.loadLogs, this); }, /** * Property initialization * */ _initProperties: function() { this.paginationLimit = 25; this.defaultCurrentPage = 1; if (!this.model.get('totalPages')) { this.model.set('totalPages', 0); } if (!this.model.get('records')) { this.model.set('records', 0); } if (!this.model.get('offset')) { this.model.set('offset', 0); } if (!this.model.get('limit')) { this.model.set('limit', this.paginationLimit); } if (!this.model.get('currentPage')) { this.model.set('currentPage', this.defaultCurrentPage); } }, /** * Retrieve the maps geocode service then display it */ loadLogs: function() { if (this.disposed) { return; } const enabledModules = this.model.get('enabledLoggingModules'); if (!enabledModules || !(_.isArray(enabledModules) && enabledModules.length > 0)) { this.model.set({ 'totalPages': 0, 'records': 0, 'limit': this.paginationLimit, 'currentPage': this.defaultCurrentPage, }); this.render(); return; } const logsConfig = { startDate: this.model.get('maps_loggerStartdate'), logLevel: this.model.get('maps_loggerLevel'), modules: enabledModules, offset: this.model.get('offset'), limit: this.model.get('limit') || 0, }; const url = App.api.buildURL('Administration/maps/logs', null, {}, logsConfig); app.alert.show('loading-logs', { level: 'process' }); app.api.call('read', url, null, { success: _.bind(this.displayLogs, this), error: _.bind(this.errorLoadLogs, this), }); }, /** * Handling error for getting logs * * @param {Object} error */ errorLoadLogs: function(error) { app.alert.dismiss('loading-logs'); app.alert.show('error-load-logs', { level: 'error', messages: error.message, }); }, /** * Display the logs * * @param {Object} data */ displayLogs: function(data) { app.alert.dismiss('loading-logs'); this.model.set({ totalPages: data.totalPages, records: data.records }); this.render(); }, /** * Pagination back */ clickPrevPage: function() { const currentPage = this.model.get('currentPage'); const nextPageNr = currentPage - 1; const limit = this.model.get('limit'); if (currentPage === 1) { return; } let nextOffset = ((nextPageNr) - 1) * limit; this.model.set({ offset: nextOffset, currentPage: nextPageNr }); this.loadLogs(); }, /** * Pagination forward */ clickNextPage: function() { const currentPage = this.model.get('currentPage'); const totalPages = this.model.get('totalPages'); const limit = this.model.get('limit'); if (currentPage === totalPages) { return; } const nextPageNr = currentPage + 1; const nextOffset = ((nextPageNr) - 1) * limit; this.model.set({ currentPage: nextPageNr, offset: nextOffset }); this.loadLogs(); }, /** * Get more details about a specific log * * @param {UIEvent} e */ clickLogDetails: function(e) { this.disposeModal(); const placeholderEl = e.currentTarget.closest('tr'); if (!placeholderEl) { return; } const recordId = placeholderEl.getAttribute('data-id'); const recordModule = placeholderEl.getAttribute('data-module'); const records = this.model.get('records'); if (!recordId || !recordModule) { return; } let detailedLogs = app.lang.getModString('LBL_MAPS_LOGGER_NO_LOGS_AVAILABLE', this.module); const targetRecord = _.chain(records) .filter(record => record.parent_id === recordId) .first() .value(); const errorMessageKey = 'error_message'; if (targetRecord && _.has(targetRecord, errorMessageKey) && targetRecord[errorMessageKey]) { //set the error message also replace some characters that are coming from provider detailedLogs = targetRecord[errorMessageKey].replace(/[-[/\]{}()*+?".,\\^$|#\s]/g, ' '); } let mapsLoggerDetails = { name: 'maps-logger-details-modal', type: 'maps-logger-details-modal', recordId, recordModule, detailedLogs, }; this.modal = app.view.createView(mapsLoggerDetails); $('body').append(this.modal.$el); this.modal.openModal(); }, /** * Dispose the modal view */ disposeModal: function() { if (this.modal && this.modal.dispose) { this.modal.dispose(); this.modal = null; } }, /** * @inheritdoc */ _dispose: function() { this.disposeModal(); this._super('_dispose'); }, }) }, "maps-module-mappings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsModuleMappingsView * @alias SUGAR.App.view.views.BaseAdministrationMapsModuleMappingsView */ ({ // Maps-module-mappings View (base) /** * Event listeners */ events: { 'change [data-action=mapping-changed]': 'mappingChanged', 'change [data-action=mapping-type-changed]': 'mappingTypeChanged', 'change [data-action=related-record-changed]': 'relatedRecordChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { this._mappingData = { locality: { label: 'LBL_ADDRESS_CITY', id: 'locality' }, countryRegion: { label: 'LBL_ADDRESS_COUNTRY', id: 'country-region' }, addressLine: { label: 'LBL_ADDRESS_STREET', id: 'address-line' }, postalCode: { label: 'LBL_ADDRESS_POSTALCODE', id: 'postal-code' }, adminDistrict: { label: 'LBL_ADDRESS_STATE', id: 'admin-district' }, }; this._mappingTypes = { moduleFields: 'LBL_MAPS_MODULE_FIELDS', relateRecord: 'LBL_MAPS_RELATE_RECORD', }; this._relatedRecords = {}; if (options.widgetModule) { this.widgetModule = options.widgetModule; this.widgetFields = app.metadata.getModule(this.widgetModule).fields; if (!_.isEmpty(this.widgetFields)) { this._fields = app.utils.maps.arrayToObject( _.chain(this.widgetFields) .filter(function getAddressLikeFields(field) { return field.vname && (field.type === 'varchar' || field.type === 'text' || field.type === 'dropdown') && (field.source !== 'non-db'); }) .map(function buildFields(field) { let data = {}; data[field.name] = app.lang.get(field.vname, this.widgetModule); return data; }, this) .value() ); } const moduleData = app.metadata.getModule(this.widgetModule).fields; _.chain(moduleData) .filter(function getRelatedFields(field) { const isValidModule = _.contains(this.model.get('maps_enabled_modules'), field.module); const linkTypeCstmKey = 'link-type'; const linkTypeKey = 'link_type'; return (field[linkTypeKey] === 'one' || field[linkTypeCstmKey] === 'one') && field.type === 'link' && isValidModule && (field.vname || field.label); }, this) .each(function mapFields(field) { this._relatedRecords[field.name] = { label: field.vname ? field.vname : field.label, module: field.module, rel: field.relationship }; }, this) .value(); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createMappingTypeSelect(); this._createRelatedRecordSelect(); _.each(this._mappingData, function createSelect2(data, addressKey) { this.$('[data-fieldname=' + data.id + ']').select2(); this._updateSelect2El(addressKey, data.id); }, this); this._updateMappingVisibility(); }, /** * Transform Related Record into Select2 * */ _createRelatedRecordSelect: function() { const widgetModule = this.widgetModule; let modulesData = this.context.safeRetrieveModulesData(widgetModule); let mappingRecord = modulesData[widgetModule].mappingRecord; const relField = _.chain(mappingRecord) .keys() .first() .value(); const relatedRecordEl = this.$('[data-action=related-record-changed]'); const relRecordLabel = mappingRecord[relField] ? mappingRecord[relField].label : 'LBL_MAPS_SELECT_FIELD'; let relRecordTxt = app.lang.getModString(relRecordLabel, widgetModule); if (!relRecordTxt) { relRecordTxt = app.lang.get(relRecordLabel); } relatedRecordEl.select2(); relatedRecordEl.select2('data', { id: relField, text: relRecordTxt }); }, /** * Transform Mapping Type into Select2 * */ _createMappingTypeSelect: function() { const widgetModule = this.widgetModule; let modulesData = this.context.safeRetrieveModulesData(widgetModule); let mappingType = modulesData[widgetModule].mappingType; const mappingTypeEl = this.$('[data-action=mapping-type-changed]'); mappingTypeEl.select2(); mappingTypeEl.select2('data', { id: mappingType, text: app.lang.get(this._mappingTypes[mappingType]), }); }, /** * Update select2 value * * @param {string} addressType * @param {string} elId */ _updateSelect2El: function(addressType, elId) { const widgetModule = this.widgetModule; let modulesData = this.context.safeRetrieveModulesData(widgetModule); let mappedAddress = modulesData[widgetModule].mappings[addressType]; if (mappedAddress) { this.$('[data-fieldname=' + elId + ']').select2('data', { id: mappedAddress, text: this._fields[mappedAddress] }); } else { this.$('[data-fieldname=' + elId + ']').select2('data', { id: 'chooseMappingField', text: app.lang.getModString('LBL_MAPS_CHOOSE_FIELD', this.module) }); } }, /** * Event handler for mapping type selection change * * @param {UIEvent} e * */ mappingTypeChanged: function(e) { const mappingType = e.currentTarget.value; const widgetModule = this.widgetModule; if (widgetModule) { let modulesData = this.context.safeRetrieveModulesData(widgetModule); modulesData[widgetModule].mappingType = mappingType; this.model.set('maps_modulesData', modulesData); this.model.trigger('change', this.model); } this._updateMappingVisibility(); }, /** * Updates mapping el visibility * */ _updateMappingVisibility: function() { const widgetModule = this.widgetModule; let modulesData = {}; let mappingType = 'moduleFields'; if (widgetModule) { modulesData = this.context.safeRetrieveModulesData(widgetModule); mappingType = modulesData[widgetModule].mappingType; } if (mappingType === 'moduleFields') { this.$('[data-container="mappings-container"]').show(); this.$('[data-container="mapping-related-record"]').hide(); } else { this.$('[data-container="mappings-container"]').hide(); this.$('[data-container="mapping-related-record"]').show(); } }, /** * Event handler for mapping field selection change * * @param {UIEvent} e * */ relatedRecordChanged: function(e) { const value = e.currentTarget.value; const widgetModule = this.widgetModule; if (widgetModule) { let modulesData = this.context.safeRetrieveModulesData(widgetModule); modulesData[widgetModule].mappingRecord = {}; modulesData[widgetModule].mappingRecord[value] = this._relatedRecords[value]; this.model.set('maps_modulesData', modulesData); this.model.trigger('change', this.model); } }, /** * Event handler for mapping field selection change * * @param {UIEvent} e * */ mappingChanged: function(e) { const value = e.currentTarget.value; const addressType = app.utils.kebabToCamelCase(e.currentTarget.dataset.fieldname); const widgetModule = this.widgetModule; if (widgetModule) { let modulesData = this.context.safeRetrieveModulesData(widgetModule); modulesData[widgetModule].mappings[addressType] = value; this.model.set('maps_modulesData', modulesData); this.model.trigger('change', this.model); } }, }) }, "package-builder-content-table": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderContentTableView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderContentTableView * @extends View.View */ ({ // Package-builder-content-table View (base) /** * Title */ title: '', /** * Headers */ headers: [], /** * Header labels */ headerLabels: [], /** * Entries */ entries: [], /** * Has content */ hasContent: false, /** * Sorting data */ sortingData: false, /** * Checked rows */ checkedRows: {}, /** * Search term */ searchTerm: '', /** * All header labels */ allHeaderLabels: { dashboards: { 'id': 'LBL_PACKAGE_BUILDER_ID', 'name': 'LBL_PACKAGE_BUILDER_NAME', 'dashboard_module': 'LBL_PACKAGE_BUILDER_DASHBOARD_MODULE', 'view_name': 'LBL_PACKAGE_BUILDER_VIEW', 'team_id': 'LBL_PACKAGE_BUILDER_TEAM', 'default_dashboard': 'LBL_PACKAGE_BUILDER_DEFAULT_DASHBOARD', 'assigned_user_id': 'LBL_PACKAGE_BUILDER_ASSIGNED_USER', 'date_modified': 'LBL_PACKAGE_BUILDER_DATE_MODIFIED', }, fields: { 'Name': 'LBL_PACKAGE_BUILDER_NAME', 'Type': 'LBL_PACKAGE_BUILDER_TYPE', 'Custom Module': 'LBL_PACKAGE_BUILDER_CUSTOM_MODULE', 'Date Modified': 'LBL_PACKAGE_BUILDER_DATE_MODIFIED', }, layouts: { 'Module': 'LBL_PACKAGE_BUILDER_MODULE', 'Client': 'LBL_PACKAGE_BUILDER_CLIENT', 'View': 'LBL_PACKAGE_BUILDER_VIEW', }, search_layouts: { 'Module': 'LBL_PACKAGE_BUILDER_MODULE', 'Client': 'LBL_PACKAGE_BUILDER_CLIENT', 'Filters': 'LBL_PACKAGE_BUILDER_FILTERS', }, dropdowns: { 'Dropdown Name': 'LBL_PACKAGE_BUILDER_DROPDOWN_NAME', }, relationships: { 'Name': 'LBL_PACKAGE_BUILDER_NAME', 'Type': 'LBL_PACKAGE_BUILDER_TYPE', 'Left Module': 'LBL_PACKAGE_BUILDER_LEFT_MODULE', 'Right Module': 'LBL_PACKAGE_BUILDER_RIGHT_MODULE', 'From Studio': 'LBL_PACKAGE_BUILDER_FROM_STUDIO', }, workflows: { 'Name': 'LBL_PACKAGE_BUILDER_NAME', 'Base Module': 'LBL_PACKAGE_BUILDER_BASE_MODULE', }, reports: { 'Report Name': 'LBL_PACKAGE_BUILDER_REPORT_NAME', 'Report Type': 'LBL_PACKAGE_BUILDER_REPORT_TYPE', 'Module': 'LBL_PACKAGE_BUILDER_MODULE', }, advanced_workflows: { 'Name': 'LBL_PACKAGE_BUILDER_NAME', 'Module': 'LBL_PACKAGE_BUILDER_MODULE', 'Status': 'LBL_PACKAGE_BUILDER_STATUS', 'Description': 'LBL_PACKAGE_BUILDER_DESCRIPTION', }, acl: { 'Name': 'LBL_PACKAGE_BUILDER_NAME', 'Description': 'LBL_PACKAGE_BUILDER_DESCRIPTION', }, language: { 'Module': 'LBL_PACKAGE_BUILDER_MODULE', }, miscellaneous: { 'Category': 'LBL_PACKAGE_BUILDER_CATEGORY', 'Description': 'LBL_PACKAGE_BUILDER_DESCRIPTION', }, }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.checkedRows = {}; this.title = options.tableData.tableName; this.headers = options.tableData.tableHeaders; this.headerLabels = _.map(this.headers, function(value) { return this.allHeaderLabels[this.title][value]; }.bind(this)); this.initEntries(options); this.tableContext = new app.Context({}); this.addCheckboxFields(); this.registerHandleBarsHelpers(); this._bindEvents(); this.initMatchingRows(); }, /** * @inheritdoc * @private */ _bindEvents: function() { this.tableContext.on('list:paginate', this.renderTable.bind(this)); }, /** * Initialize entries * * @param {Object} options */ initEntries: function(options) { this.entries = options.tableData.tableEntries ? options.tableData.tableEntries : []; this.entries.map((el, i) => this.entries[i] = _.extend({}, el, {_id: i})); }, /** * Add all entries IDs to the list of matching rows */ initMatchingRows: function() { const matchingRows = Array.from(this.entries.keys()); this.tableContext.set('matchingRows', matchingRows); }, /** * @inheritdoc */ render: function() { this.hasContent = (this.entries.length > 0); this.model.set('filterInput', this.searchTerm); // Call super this._super('render'); this.showPagination(); this.initSearchInputEvent(); this.doneTyping(); this.updateSelectedDisplay(); this.bindCheckboxEvents(); }, /** * Add events for rendered checkboxes */ bindCheckboxEvents: function() { const checkboxes = this.$el.find('.checkboxFilter'); _.each(checkboxes, (checkbox) => checkbox.addEventListener('click', this.doneTyping.bind(this))); }, /** * Render table and add it to the view */ renderTable: function() { const collection = this.tableContext.get('collection').models || []; this.collectionIds = []; collection.map((model) => this.collectionIds.push(model.get('_id'))); const tableTpl = app.template.get(this.type + '.table.' + this.module); const tableHtml = tableTpl(this); this.$('#table-content').html(tableHtml); this.initClickHandlers(); this.activateCheckedRows(); this.updateCheckAll(); this._drawAlert(); const selection = this.$('.checkAll .selection'); const currentState = selection.prop('checked'); selection.attr('data-bs-original-title', app.lang.get(currentState ? 'LBL_LISTVIEW_DESELECT_ALL_ON_PAGE' : 'LBL_LISTVIEW_SELECT_ALL_ON_PAGE') ); }, /** * Register handlebars helpers */ registerHandleBarsHelpers: function() { if (_.isUndefined(Handlebars.helpers.wciFormatEntryValue)) { Handlebars .registerHelper( 'wciFormatEntryValue', function wRegisterWciFormatEntryValue(rawValue, tabTitle, options) { // Alter this variable in order to alter the displayed value let modifiedValue = rawValue; // If we are on reports and header number is 2 (Report Type), alter value let reportTypeIndex = 2; if (tabTitle === 'reports' && options.data.index === reportTypeIndex) { // Report Type Mapping let typeMap = { 'summary': app.lang.get( 'LBL_PACKAGE_BUILDER_REPORT_SUMMATION', 'Administration' ), 'tabular': app.lang.get( 'LBL_PACKAGE_BUILDER_REPORT_ROWS_AND_COLUMNS', 'Administration' ), 'detailed_summary': app.lang.get( 'LBL_PACKAGE_BUILDER_REPORT_SUMMATION_WITH_DETAILS', 'Administration' ), 'Matrix': app.lang.get( 'LBL_PACKAGE_BUILDER_REPORT_MATRIX', 'Administration' ), }; modifiedValue = typeMap[rawValue]; } return options.fn(modifiedValue); } ); } if (_.isUndefined(Handlebars.helpers.getValueByIndex)) { Handlebars .registerHelper( 'getValueByIndex', (el, index) => (_.isArray(el) || _.isObject(el)) ? el[index] : '' ); } }, /** * This function will init the stop-typing events for the filter search input */ initSearchInputEvent: function() { // Timming setup let typingTimer = null; let doneTypingInterval = 1000; // 1s after the user is done typing let searchInput = this.$el.find('.filterSearchInput'); // Get the search input element // On keyup, start the countdown searchInput.on('keyup', function onKeyupStartCd() { clearTimeout(typingTimer); typingTimer = setTimeout(this.doneTyping.bind(this), doneTypingInterval); }.bind(this)); // On keydown, clear the countdown searchInput.on('keydown', function onKeydownClearCd() { clearTimeout(typingTimer); }.bind(this)); }, /** * User is done typing in the search filter */ doneTyping: function() { let searchTerm = this.$el.find('.filterSearchInput input').val(); let filterHeaders = this.getCheckedHeaders(); // array with checked headers by index (e.g. [0,2,3]) if (_.isEmpty(searchTerm) || _.isEmpty(filterHeaders)) { // If currently the search term is empty, but this.searchTerm is not empty(previously was a value) if (!_.isEmpty(this.searchTerm)) { // Reset this.searchTerm this.searchTerm = ''; // Uncheck the CheckAll header checkbox, this means the user is done filtering the rows this.$el.find('.table .headerRow .selection').eq(0).prop('checked', false); } } this.filterRows(searchTerm, filterHeaders); }, /** * Get the index for the checked types * @return {Array} Array with the checked headers by index */ getCheckedHeaders: function() { // Get the checkboxes used for filtering let checkboxes = this.$el.find('#filterCustomizations .checkboxFilter [type="checkbox"]'); let headers = []; // here insert the checked headers _.each(checkboxes, function(checkbox, key) { if (checkbox.checked === true) { headers.push(key); } }); return headers; // return the list with the checked headers }, /** * Filter the rows based on the search term and the headers that are checked * * @param {string} searchTerm - The search term * @param {Array} filterHeaders - The headers that are checked */ filterRows: function(searchTerm, filterHeaders) { app.alert.show('pb_filtering_loading', { level: 'process', title: app.lang.get('LBL_PACKAGE_BUILDER_FILTERING', 'Administration'), }); let matchingRows = []; _.each(this.entries, function(entry, entryKey) { let entryMached = false; // current entry/row is maching the filtering condition if (!searchTerm || _.isEmpty(filterHeaders)) { entryMached = true; } else { _.each(filterHeaders, function(headerIndex) { let indexValue = entry[headerIndex]; if (_.isNull(indexValue) === false && _.isUndefined(indexValue) === false && indexValue.toString().toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase())) { entryMached = true; } }); } // If the row is matching if (entryMached) { // Push the row index in to the matchingRows list matchingRows.push(entryKey); } }); // Unhide the rows that mach the filter app.alert.dismiss('pb_filtering_loading'); this.searchTerm = searchTerm; // After the filtering process is done, save the search term on this this.tableContext.set('matchingRows', matchingRows); this.tableContext.trigger('filter:fetch:success'); }, /** * Render pagination and add it to the view */ showPagination: function() { let pagination = app.view.createView({ name: 'package-builder-pagination', module: this.module, context: this.tableContext, layout: {}, tableData: this.entries, }); pagination.render(); this.$('.customizations-pagination').html(pagination.$el); }, /** * Update the selected display */ updateSelectedDisplay: function() { // Get selected rows length const selectedCount = Object.keys(this.checkedRows).length; let displayText = ''; this._hideAlert(); if (selectedCount > 0) { const totalCount = this.entries.length; displayText = '(' + selectedCount + '/' + totalCount + ')'; this._drawAlert(); } const classPath = '.' + this.title + '_tab #selected_display'; // Tab header class path this.$el.parent().parent().find(classPath).html(displayText); // Set display text this.updateCheckAll(); const selection = this.$('.checkAll .selection'); selection.attr('data-bs-original-title', app.lang.get(selection.prop('checked') ? 'LBL_LISTVIEW_DESELECT_ALL_ON_PAGE' : 'LBL_LISTVIEW_SELECT_ALL_ON_PAGE') ); }, /** * Update Check All checkbox state */ updateCheckAll: function() { // If all rows are selected, check the checkAll checkbox const checkAll = this._isFullConsist(); this.$el.find('.checkAll .selection').eq(0).prop('checked', checkAll); }, /** * Set click handlers */ initClickHandlers: function() { this.$el.find('.regularRow').click(this.checkRegularRowHandler.bind(this)); // CheckAll function this.$el.find('.checkAll').click(this.checkAllHandler.bind(this)); this.$el.find('.clearFilterInputButton').click(this.clearFilterInputClicked.bind(this)); // Sorting Function this.$el.find('.sorting').click( function clickHandle() { const sortIndex = $(arguments[0].currentTarget).attr('id'); const sortField = this.headers[sortIndex]; let sortType = ''; // Choose sorting order if (this.sortingData === false) { sortType = 'asc'; } else { const currentSortType = this.sortingData.type; if (this.sortingData.field == sortField) { sortType = currentSortType == 'asc' ? 'desc' : 'asc'; } else { sortType = 'asc'; } } let a = this.entries; let swapped = null; // Begin sorting do { swapped = false; for (var i = 0; i < a.length - 1; i++) { let temp = null; let condition = false; // Get swapping terms let aTerm = this.formatSortTerm(a[i][sortIndex]); let bTerm = this.formatSortTerm(a[i + 1][sortIndex]); // Make comparison if (sortType == 'asc') { condition = aTerm > bTerm; } else if (sortType == 'desc') { condition = aTerm < bTerm; } // Swap if condition true if (condition) { temp = a[i]; a[i] = a[i + 1]; a[i + 1] = temp; swapped = true; } } } while (swapped); this.sortingData = {field: sortField, type: sortType}; this.render(); let sortEl = this.$el.find('th#' + sortIndex)[0]; if (sortEl) { sortEl.className = 'sorting_' + sortType; } }.bind(this) ); }, /** * Function-handler for Check All click event * * @param e */ checkAllHandler: function(e) { const target = $(e.target); const selection = target.closest('.headerRow').find('.selection').eq(0); let currentState = selection.prop('checked'); // activate checkbox if click target is not input selector if (target.hasClass('checkAll')) { currentState = !currentState; selection.prop('checked', currentState); } const tableElement = target.closest('#fields_table'); tableElement.find('tr.regularRow').map((i, elItem) => $(elItem).find('.selection').eq(0).prop('checked', currentState)); if (currentState) { this.collectionIds.map(rowId => { this.checkedRows[rowId] = rowId; }); } else { this.collectionIds.map(rowId => { if (!_.isUndefined(this.checkedRows[rowId])) { delete this.checkedRows[rowId]; } }); } this.updateSelectedDisplay(); }, /** * Function-handler for Row selection * * @param e */ checkRegularRowHandler: function(e) { const target = $(e.target); const selection = target.hasClass('selection') ? target : target.closest('.regularRow').find('.selection').eq(0); const row = selection.closest('tr'); let currentState = selection.prop('checked'); // activate checkbox if click target is not input selector if (!target.hasClass('selection')) { currentState = !currentState; selection.prop('checked', currentState); } const rowId = row.data('id'); if (currentState && _.isUndefined(this.checkedRows[rowId])) { this.checkedRows[rowId] = rowId; } else if (!currentState) { delete this.checkedRows[rowId]; } this.updateSelectedDisplay(); }, /** * Activate checkboxes of selected rows */ activateCheckedRows: function() { this.collectionIds.map(rowId => { if (!_.isUndefined(this.checkedRows[rowId])) { this.$el.find(`.regularRow[data-id=${rowId}] .selection`).prop('checked', true); } }); }, /** * Format sorting term * @param {string} sortingTerm The sorting term * @return {(string|number)} The formatted sorting term */ formatSortTerm: function(sortingTerm) { if (_.isNull(sortingTerm) || _.isUndefined(sortingTerm)) { return ''; } if (isNaN(sortingTerm)) { // Is not a number return _.isEmpty(sortingTerm) ? '' : sortingTerm; } return parseInt(sortingTerm); }, /** * Clear filter input clicked */ clearFilterInputClicked: function() { this.$el.find('.filterSearchInput input').val(''); this.doneTyping(); // Trigger search }, /** * Get selection * @return {Array} The selected rows */ getSelection: function() { let restrictions = []; if (this.hasContent === false) { restrictions = {sync: this.sync}; } else { const indexesMatching = {}; this.entries.map((el, i) => indexesMatching[el._id] = i); _.each(this.checkedRows, (id) => { let newElem = {}; const index = indexesMatching[id]; for (let i = 0; i < this.headers.length; i++) { newElem[this.headers[i]] = this.entries[index][i]; } restrictions.push(newElem); }); } return restrictions; }, /** * Add checkbox fields */ addCheckboxFields: function() { _.each(this.headers, function(value, key, list) { if (value != 'Date Modified') { const checkField = { 'allowClear': false, 'default': true, 'label': app.lang.get(this.headerLabels[key], 'Administration'), 'labelDown': true, 'name': this.buildFieldName(value), 'type': 'bool', 'openRow': (key === 0), 'closeRow': (key === list.length), 'css_class': 'checkboxFilter', }; this.meta.panels[0].fields.push(checkField); } }.bind(this)); }, /** * Build field name * @param {string} displayValue - The display value * @return {string} The field name */ buildFieldName: function(displayValue) { let fieldName = ''; if (displayValue.includes(' ')) { // If header name has multiple words, return e.g. Custom Module => custom_module let fieldWords = displayValue.split(' '); let lowercased = fieldWords.map(word => word.toLowerCase()); fieldName = lowercased.join('_'); } else { fieldName = displayValue.toLowerCase(); } // There are several categories with module field, create unique one for each category if (fieldName === 'module') { fieldName = fieldName + '_' + this.title; } return fieldName; }, /** * Draw various type of alerts * @private */ _drawAlert: function() { const selectedCount = Object.keys(this.checkedRows).length; if (selectedCount > 0) { const totalCount = this.entries.length; const alert = (this._isFullConsist() && selectedCount < totalCount) ? this._getSelectAllAlert() : this._getSelectedOffsetAlert(); this._showAlert(alert); } }, /** * Get selected offset alert * @return {jQuery} * @private */ _getSelectedOffsetAlert: function() { const selectedCount = Object.keys(this.checkedRows).length; const totalCount = this.entries.length; const selectedOffsetTpl = app.template.getView('list.selected-offset'); const selectedOffsetAlert = $('<span></span>').append(selectedOffsetTpl({ num: selectedCount, all_selected: totalCount === selectedCount })); selectedOffsetAlert.find('[data-action=clear]').map((index, el) => { $(el).on('click', () => { this.checkedRows = {}; const selectedCheckboxes = this.$el.find('#fields_table').find('tr.regularRow .selection:checked'); selectedCheckboxes.map((i, elItem) => $(elItem).prop('checked', false)); this.$el.find('.headerRow').find('.selection:checked').prop('checked', false) .attr('data-bs-original-title', app.lang.get('LBL_LISTVIEW_SELECT_ALL_ON_PAGE')); this.$el.parent().parent().find('.' + this.title + '_tab #selected_display').html(''); this._hideAlert(); }); }); return selectedOffsetAlert; }, /** * Get select all alert * @return {jQuery} * @private */ _getSelectAllAlert: function() { const selectedCount = Object.keys(this.checkedRows).length; const totalCount = this.entries.length; const selectAllTpl = app.template.compile(null, app.lang.get('TPL_LISTVIEW_SELECT_ALL_RECORDS')); const selectAllLinkTpl = new Handlebars.SafeString( '<button type="button" class="btn btn-link btn-inline" data-action="select-all">' + app.lang.get('LBL_LISTVIEW_SELECT_ALL_RECORDS') + '</button>' ); const selectAllAlert = $('<span></span>').append(selectAllTpl({ num: selectedCount, link: selectAllLinkTpl })); selectAllAlert.find('[data-action=select-all]').map((index, el) => { $(el).on('click', () => { const matchingRows = this.tableContext.get('matchingRows'); matchingRows.map(rowId => { this.checkedRows[rowId] = rowId; }); this._hideAlert(); this._showAlert(this._getSelectedOffsetAlert()); const displayText = '(' + totalCount + '/' + totalCount + ')'; this.$el.parent().parent().find('.' + this.title + '_tab #selected_display').html(displayText); }); }); return selectAllAlert; }, /** * Show alert * @param {jQuery} alert * @private */ _showAlert: function(alert) { this.$('[data-target=alert]').html(alert); this.$('[data-target=alert-container]').removeClass('hide'); }, /** * Hide alert * @private */ _hideAlert: function() { this.$('[data-target=alert-container]').addClass('hide'); this.$('[data-target=alert]').empty(); }, /** * Check if all rows of the displayed table were selected * @private * @return {boolean} True if all rows are selected */ _isFullConsist: function() { return !this.collectionIds.some(rowId => _.isUndefined(this.checkedRows[rowId])); }, }) }, "package-builder-configuration-tab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderConfigurationTabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderConfigurationTabView * @extends View.View */ ({ // Package-builder-configuration-tab View (base) /** * Flag to check if the data was refetched */ dataRefetched: false, /** * Customizations extracted from the instance */ customizations: false, /** * Connection info for remote instance */ connectionInfo: false, /** * Progress alert view */ progressAlertView: false, /** * Elements selected to fetch */ elementsSelectedToFetch: [], /** * Lablels for categories */ categoriesLabels: { acl: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_ACL_T', advanced_workflows: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_ADVANCEDWORKFLOWS_T', dashboards: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_DASHBOARDS_T', dropdowns: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_DROPDOWNS_T', fields: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_FIELDS_T', language: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_LANGUAGE_T', layouts: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_LAYOUTS_T', miscellaneous: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_MISCELLANEOUS_T', relationships: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_RELATIONSHIPS_T', reports: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_REPORTS_T', workflows: 'LBL_PACKAGE_BUILDER_TAB_CUSTOMIZATIONS_WORKFLOWS_T', }, /** * All categories */ categoriesOptions: { acl: 'Roles', advanced_workflows: 'Process Definitions', dashboards: 'Dashboards', dropdowns: 'Dropdowns', fields: 'Fields', language: 'Language', layouts: 'Layouts', miscellaneous: 'Miscellaneous', relationships: 'Relationships', reports: 'Reports', workflows: 'Workflows', }, /** * Default categories */ categoriesDefault: [ 'dashboards', 'dropdowns', 'fields', 'language', 'layouts', 'relationships', 'reports', ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setDefaultValues(); }, /** * Set default values for the categories field */ setDefaultValues: function() { _.each(this.categoriesLabels, function(label, key) { this.categoriesOptions[key] = app.lang.get(label, 'Administration'); }, this); this.model.set('categories', this.categoriesDefault); }, /** * Add events to the view */ initEvents: function() { if (_.isUndefined(this.context._events) || _.isEmpty(this.context._events['button:fetch_customizations_button:click'])) { this.listenTo(this.context, 'button:fetch_customizations_button:click', this.fetchCustomizations.bind(this)); } // click event for each Push Package button this.$el.find('.selectAllButton').click(this.selectAll.bind(this)); }, /** * @inheritdoc */ render: function() { this._super('render'); this.initEvents(); }, /** * Fetch customizations */ fetchCustomizations: function() { this.elementsSelectedToFetch = this.model.get('categories'); if (_.isEmpty(this.elementsSelectedToFetch)) { app.alert.show('empty-categories-alert', { 'level': 'error', 'messages': app.lang.get('LBL_PACKAGE_BUILDER_TAB_CONFIG_EMPTY_C_BUTTON', 'Administration'), 'autoClose': true }); return; } this.extractCustomizations(this.elementsSelectedToFetch); }, /** * Select all categories */ selectAll: function() { const allValues = Object.keys(this.categoriesOptions); if (this.model.get('categories').length === allValues.length) { this.model.set('categories', []); } else { this.model.set('categories', allValues); } }, /** * Extract customizations * @param {Array} elementsToFetch */ extractCustomizations: function(elementsToFetch) { let url = ''; this.data = {}; let numberElements = elementsToFetch.length; let nrProcessedEl = 0; if (!_.isEmpty(this.customizations)) { this.dataRefetched = true; } // Reset customizations this.customizations = {}; url = app.api.buildURL('Administration/package/customizations'); // Create and display progress alert this.showFetchingAlert(); _.each(elementsToFetch, function callForEachElement(element) { let data = {elementsToFetch: [element]}; let callback = { success: function(data) { if ('installed_packages' in data) { let installedPackages = {}; _.each(data.installed_packages, function(installedPackage) { installedPackages[installedPackage.id] = installedPackage; }); this.context.set('installedPackages', installedPackages); } else { Object.assign(this.customizations, data); } nrProcessedEl++; let calculatedProgress = this.getPercentage(nrProcessedEl, numberElements); let message = app.lang.get( 'LBL_PACKAGE_BUILDER_CATEGORY_RECEIVED', 'Administration', {category: this.categoriesOptions[element]} ); this.progressAlertView.logMessage(message); this.progressAlertView.setProgress(calculatedProgress); }.bind(this), error: function(errorData) { nrProcessedEl++; let calculatedProgress = this.getPercentage(nrProcessedEl, numberElements); let message = app.lang.get( 'LBL_PACKAGE_BUILDER_CATEGORY_RETRIEVE_FAILED', 'Administration', {category: this.categoriesOptions[element]} ); this.progressAlertView.logMessage(message); this.progressAlertView.setProgress(calculatedProgress); }.bind(this), complete: function() { // Check if all the customizations were extracted if (nrProcessedEl === numberElements) { // Enable Customizations tab this.enableTab('customizations'); // Sort the extracted customizations this.customizations = Object.keys(this.customizations).sort().reduce( (obj, key) => { obj[key] = this.customizations[key]; return obj; }, {} ); this.progressAlertView.processSuccessful(app.lang.get( 'LBL_PACKAGE_BUILDER_COMPLETE', 'Administration' )); } }.bind(this) }; app.api.call('create', url, data, callback); }.bind(this)); }, /** * Get progress percentage * @param {number} first * @param {number} second * @return {string} */ getPercentage: function(first, second) { // eslint-disable-next-line no-magic-numbers return Math.round(((first / second) * 100)) + '%'; }, /** * Enable tab * @param {string} tabName */ enableTab: function(tabName) { const tabClass = '.' + tabName + '_tab'; // e.g.g .customizations_tab const dotClass = '.' + tabName + 'Dot'; // e.g.g .customizationsDot const tab = this.$el.parent().parent().find(tabClass); // Get tab element tab.removeClass('disabled'); // Enable Tab tab.find(dotClass).show(); // Show blue notification dot tab.addClass('start_animation'); }, /** * Load the progress-alert view, custom view to display the fetching progress */ showFetchingAlert: function() { let alertContainer = this.$el.find('.progress-alert-container'); let progressAlertView = app.view.createView({ name: 'package-builder-progress-alert', initialTitle: app.lang.get('LBL_PACKAGE_BUILDER_FETCHING_CUSTOMIZATIONS', 'Administration'), successTitle: app.lang.get('LBL_PACKAGE_BUILDER_CUSTOMIZATIONS_FETCHED', 'Administration'), errorTitle: app.lang.get('LBL_PACKAGE_BUILDER_CUSTOMIZATIONS_FETCHING_FAILED', 'Administration'), }); progressAlertView.render(); alertContainer.empty(); alertContainer.append(progressAlertView.$el); // Keep alert view on this this.progressAlertView = progressAlertView; }, }) }, "config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * This is the base view for Config Framework. To create a new config page, eg, for category 'test' with one setting 'anything', * at least you need to create a metadata file: modules/Administration/clients/base/views/test-config/test-config.php * with following content: * * $viewdefs['Administration']['base']['view']['test-config'] = [ * 'template' => 'record', * 'label' => 'LBL_TEST_CONFIG_TITLE', * 'saveMessage' => 'LBL_TEST_CONFIG_SAVE_MESSAGE', * 'panels' => [ * [ * 'name' => 'panel_body', * 'label' => 'LBL_PANEL_1', * 'columns' => 1, * 'labelsOnTop' => true, * 'placeholders' => true, * 'newTab' => false, * 'panelDefault' => 'expanded', * 'fields' => [ * [ * 'name' => 'test_anything', * 'type' => 'text', * 'label' => 'LBL_TEST_ANYTHING', * 'span' => 6, * 'labelSpan' => 4, * 'required' => true, * ], * ], * 'helpLabels' => [ * [ * 'text' => 'LBL_TEST_CONFIG_HELP_TEXT_CONTENT', * ], * ], * ], * ], * ]; * * The url for this page will be sugar_url/#Administration/config/test. * * If needed, you can create a custom controller by extending this view. * * @class View.Views.Base.AdministrationConfigView * @alias SUGAR.App.view.views.BaseAdministrationConfigView * @extends View.Views.Base.RecordView */ ({ // Config View (base) extendsFrom: 'RecordView', /** * The main setting prefix. * @property {string} */ settingPrefix: '', /** * Message to show on successful save. * @property {string} */ saveMessage: '', /** * The css class used for the main element. * * @property {string} */ className: 'admin-config-body', /** * The help strings to be displayed in the help block. * @property {Object} */ helpBlock: {}, /** * A collection of variables used for help block text interpolation. * @property {Object} */ helpBlockContext: null, /** * Initialize the help block displayed below the configuration field(s). * * @inheritdoc */ initialize: function(options) { if (!options.meta) { options.meta = app.metadata.getView(options.context.get('module'), 'config'); } this._super('initialize', [options]); this.helpBlock = this.generateHelpBlock(); this.addValidationTask(); this.settingPrefix = 'config/' + options.context.get('category'); this.saveMessage = this.meta['saveMessage'] || ''; this.boundSaveHandler = _.bind(this.validateModel, this); this.context.on('save:config', this.boundSaveHandler); this.loadSettings(); }, /** * Load any existing configuration. */ loadSettings: function() { var options = { success: _.bind(this.loadSettingsSuccessCallback, this) }; app.api.call('get', app.api.buildURL(this.module, this.settingPrefix), [], options, {context: this}); }, /** * The success callback to execute when settings have been retrieved * * @param settings */ loadSettingsSuccessCallback: function(settings) { this.copySettingsToModel(settings); this.render(); }, /** * Render the view in edit mode and display the help block. * @inheritdoc */ _render: function() { this._super('_render'); this.action = 'edit'; this.toggleEdit(true); this.renderHelpBlock(); }, /** * Save the settings. */ save: function() { var options = { error: _.bind(this.saveErrorHandler, this), success: _.bind(this.saveSuccessHandler, this) }; app.api.call('create', app.api.buildURL(this.module, this.settingPrefix), this.model.toJSON(), options); }, /** * On a successful save the Save button has to be disabled and * a message will be shown indicating that the settings have been saved. * * @param {Object} settings The aws connect settings. */ saveSuccessHandler: function(settings) { this.toggleHeaderButton(false); this.updateConfig(settings); this.closeView(); app.alert.show(this.settingPrefix + '-info', { autoClose: true, level: 'success', messages: app.lang.get(this.saveMessage, this.module) }); }, /** * Set settings on the model. * * @param {Object} settings details. */ copySettingsToModel: function(settings) { this.boundChangeHandler = _.bind(this.toggleHeaderButton, this); _.each(settings, function(value, key) { this.model.set(key, value); }, this); this.model.on('change', this.boundChangeHandler); }, /** * It will change the Save button enabled/disabled state. * * @param {boolean} state The state to be set. */ toggleHeaderButton: function(state) { var header = this.layout.getComponent(this.name + '-header'); if (header) { header.enableButton(state); } }, /** * Show an error message if the settings could not be saved. */ saveErrorHandler: function() { app.alert.show(this.settingPrefix + '-warning', { level: 'error', title: app.lang.get('LBL_ERROR') }); }, /** * Add validation tasks to the current model so any aws related fields could be validated. */ addValidationTask: function() { this.model.addValidationTask('fields_required', _.bind(this.validateRequiredFields, this.model)); }, /** * It will validate required fields. * * @param {Array} fields The list of fields to be validated. * @param {Object} errors A list of error messages. * @param {Function} callback Callback to be called at the end of the validation. */ validateRequiredFields: function(fields, errors, callback) { _.each(fields, function(field) { if (_.has(field, 'required') && field.required) { var key = field.name; if (!this.get(key)) { errors[key] = errors[key] || {}; errors[key].required = true; } } }, this); callback(null, fields, errors); }, /** * It triggers the save process if all fields are valid. * * @param {boolean} isValid If all the fields are valid. */ validationComplete: function(isValid) { if (isValid) { this.save(); } }, /** * Trigger the field validation through the model */ validateModel: function() { var fields = this.getFieldsToValidate(); this.model.doValidate(fields, _.bind(this.validationComplete, this)); }, /** * Get fields to validate * @return {Object} */ getFieldsToValidate: function() { return this.meta.panels && this.meta.panels[0] && this.meta.panels[0].fields || {}; }, /** * On a successful save return to the Administration page. */ closeView: function() { // Config changed... reload metadata app.sync(); if (app.drawer && app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } else { app.router.navigate(this.module, {trigger: true}); } }, /** * Update the settings stored in the front-end. * * @param {Object} settings. */ updateConfig: function(settings) { _.each(settings, function(value, key) { app.config[app.utils.getUnderscoreToCamelCaseString(key)] = value; }); }, /** * Return the strings for help block. * * @return {Object} */ generateHelpBlock: function() { var helpTemplate = app.template.getView(this.name + '.help-block', this.module) || app.template.getView('config.help-block', this.module); var block = {}; _.each(this.meta.panels, function(panel) { var contents = []; contents.push(...this.getHelpLabels(panel)); contents.push(...this.getLinkLabels(panel)); if (!_.isEmpty(contents)) { block[panel.name] = helpTemplate(contents); } }, this); return block; }, /** * Creates and returns the translated help block title. * * @param {Object} label An object holding labels to be translated. * @return {string} The help block name. */ getHelpBlockName: function(label) { if (_.isUndefined(label.name)) { return ''; } var translation = app.lang.get(label.name, this.module, this.helpBlockContext); return translation + ':'; }, /** * Creates and returns the translated help block text. * * @param {string} text The key to the language text * @return {string} The help block text. */ getHelpBlockText: function(text) { if (_.isEmpty(text)) { return ''; } var translation = app.lang.get(text, this.module, this.helpBlockContext); return new Handlebars.SafeString(translation); }, /** * Render help block. By default it will append the blocks to the record container. */ renderHelpBlock: function() { var panel = this.$el.find('.record'); _.each(this.helpBlock, function(block) { if (panel.length) { panel.append(block); } }, this); }, /** Get hbs ready help labels * * @param panel * @return {Array} */ getHelpLabels: function(panel) { var help = []; if (!panel.helpLabels) { return help; } _.each(panel.helpLabels, function(label) { help.push({ name: this.getHelpBlockName(label), label: label.text || '', css_class: label.css_class, text: this.getHelpBlockText(label.text) }); }, this); return help; }, /** * Get hbs ready link labels * * @param panel * @return {Array} */ getLinkLabels: function(panel) { var links = []; if (!panel.linkLabels) { return links; } _.each(panel.linkLabels, function(label) { if (!label.link) { return; } links.push({ is_link: true, link_text: this.getHelpBlockText(label.link.text), link_css_class: label.link.css_class, link_href: label.link.href, link_target: label.link.target ? label.link.target : '_self', name: label.name, css_class: label.css_class ? label.css_class : '', text: this.getHelpBlockText(label.text) }); }, this); return links; }, /** * Toggles visibility for a field. On these fields we have * to hide/show the field itself, and its parent record-cell * * @param {Object} field - field to hide/show * @param {boolean} show - whether or not to display the field */ _toggleFieldVisibility: function(field, show) { if (!field) { return; } field.$el.closest('.record-cell').toggle(show); }, /** * @inheritdoc */ _dispose: function() { if (!this.disposed) { this.context.off('save:config', this.boundSaveHandler); this.model.off('change', this.boundChangeHandler); this._super('_dispose'); } } }) }, "actionbutton-properties": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button properties view * * @class View.Views.Base.AdministrationActionbuttonPropertiesView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonPropertiesView * @extends View.View */ ({ // Actionbutton-properties View (base) events: { 'input .ab-admin-button-property input[type=text]': 'textPropChanged', 'change .ab-admin-button-property input[type=checkbox]': 'boolPropChanged', 'change .ab-admin-button-property select': 'enumPropChanged', 'click .btn.btn-invisible': 'panelCollapsedChanged', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Property initialization * */ _initProperties: function() { this.properties = {}; this.buttonData = this._getActiveButtonData(); this._formulaBuilder = false; }, /** * Register context event handlers * */ _registerEvents: function() { this.listenTo(this.context.get('model'), 'update:button:view', this.changeProperties, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.properties.isDependent.value) { this.$('div[data-container="formula"]').show(); this._createFormulaBuilder(); } else { this.$('div[data-container="formula"]').hide(); } _.each(this.properties, function createSelect2(data, propId) { if (data.options) { this.$('select[data-id="' + propId + '"]').select2(this._getOptions(data)); } }, this); }, /** * Generic text property change handler * * @param {UIEvent} e * */ textPropChanged: function(e) { var propertyId = $(e.currentTarget).data('id'); this.properties[propertyId].value = e.currentTarget.value; this._updateActionButtons(); this.context.get('model').trigger('refresh:ui'); }, /** * Generic checkbox property change handler * * @param {UIEvent} e * */ boolPropChanged: function(e) { var propertyId = $(e.currentTarget).data('id'); this.properties[propertyId].value = e.currentTarget.checked; this._updateActionButtons(); if (propertyId === 'isDependent') { this.render(); } }, /** * Generic list property change handler * * @param {UIEvent} e * */ enumPropChanged: function(e) { var propertyId = $(e.currentTarget).data('id'); this.properties[propertyId].value = e.currentTarget.value; this._updateActionButtons(); }, /** * Handler for panel collapse * * @param {UIEvent} e * */ panelCollapsedChanged: function(e) { const $el = $(e.currentTarget); const isCollapsed = $el.hasClass('collapsed'); let $icon = $el.find('i'); $icon.toggleClass('sicon-chevron-down', isCollapsed); $icon.toggleClass('sicon-chevron-right', !isCollapsed); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { if (!(app.utils.isTruthy(this.buttonData.properties.showIcon) || app.utils.isTruthy(this.buttonData.properties.showLabel))) { app.alert.show('validation-error', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_VALIDATION_ERROR'), messages: ['LBL_ACTIONBUTTON_VALIDATION_ERROR_NEED_LABEL_OR_ICON'], autoClose: true }); return false; } if (app.utils.isTruthy(this.buttonData.properties.showLabel) && _.isEmpty((this.buttonData.properties.label || '').trim())) { app.alert.show('validation-error', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_VALIDATION_ERROR'), messages: ['LBL_ACTIONBUTTON_VALIDATION_ERROR_NEED_LABEL'], autoClose: true }); return false; } if (this.properties.isDependent.value) { return this._formulaBuilder.isValid(); } return true; }, /** * Handler for formula change * * @param {string} formula * */ formulaChanged: function(formula) { this._formula = formula; this._updateActionButtons(); }, /** * Update button proerties * * @param {string} buttonId * */ changeProperties: function(buttonId) { this.buttonData = this._getActiveButtonData(); this.render(); }, /** * Build select2 control options * * @param {Object} data * * @return {Object} */ _getOptions: function(data) { var select2Options = { dropdownCssClass: 'ab-admin-select-icon' }; if (data.formatOptions) { select2Options.formatResult = data.formatOptions.bind(this); } return select2Options; }, /** * Create the formula builder sidecar field * */ _createFormulaBuilder: function() { this._disposeFormulaBuilder(); this._formulaBuilder = app.view.createField({ def: { type: 'formula-builder', name: 'ABCustomAction' }, view: this, viewName: 'edit', targetModule: this.options.context.get('model').get('module'), returnType: 'boolean', callback: this.formulaChanged.bind(this), formula: this._formula }); this._formulaBuilder.render(); this.$('span[data-fieldname="formula"]').append(this._formulaBuilder.$el); }, /** * Update Action buttons configuration * */ _updateActionButtons: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); var updatedProps = {}; _.each(this.properties, function updateProps(property, name) { updatedProps[name] = property.value; }); updatedProps.formula = this._formula; buttonsData.buttons[this.buttonData.buttonId].properties = updatedProps; ctxModel.set('data', buttonsData); this.context.trigger('update-buttons-preview', buttonsData); }, /** * Return action button configuration * * @return {Array} */ _getActiveButtonData: function() { var buttons = this.context.get('model').get('data').buttons; var activeButton = _.filter(buttons, function getActiveButtonData(buttonData) { return buttonData.active === true; })[0]; this._updateProperties(activeButton); return activeButton; }, /** * Build action button properties * * @param {Object} activeButton * * @return {undefined} */ _updateProperties: function(activeButton) { this._formula = activeButton.properties.formula; if (activeButton.properties.label && activeButton.properties.label.includes('LBL_')) { const module = this.context.get('model').get('module'); activeButton.properties.label = app.lang.get(activeButton.properties.label, module); } this.properties = { label: { template: 'actionbutton-text-property', label: 'LBL_ACTIONBUTTON_LABEL_TITLE', id: 'label', value: activeButton.properties.label, }, description: { template: 'actionbutton-text-property', label: 'LBL_ACTIONBUTTON_DESC', id: 'description', value: activeButton.properties.description, }, showLabel: { template: 'actionbutton-bool-property', label: 'LBL_ACTIONBUTTON_SHOW_LABEL', id: 'showLabel', value: app.utils.isTruthy(activeButton.properties.showLabel), }, showIcon: { template: 'actionbutton-bool-property', label: 'LBL_ACTIONBUTTON_SHOW_ICON', id: 'showIcon', value: app.utils.isTruthy(activeButton.properties.showIcon), }, colorScheme: { template: 'actionbutton-enum-property', label: 'LBL_ACTIONBUTTON_SCHEME', id: 'colorScheme', value: activeButton.properties.colorScheme, options: { primary: app.lang.get('LBL_ACTIONBUTTON_THEME_PRIMARY'), secondary: app.lang.get('LBL_ACTIONBUTTON_THEME_SECONDARY'), highViz: app.lang.get('LBL_ACTIONBUTTON_THEME_HIGHVIZ'), ocean: app.lang.get('LBL_ACTIONBUTTON_THEME_OCEAN'), pacific: app.lang.get('LBL_ACTIONBUTTON_THEME_PACIFIC'), teal: app.lang.get('LBL_ACTIONBUTTON_THEME_TEAL'), green: app.lang.get('LBL_ACTIONBUTTON_THEME_GREEN'), army: app.lang.get('LBL_ACTIONBUTTON_THEME_ARMY'), yellow: app.lang.get('LBL_ACTIONBUTTON_THEME_YELLOW'), orange: app.lang.get('LBL_ACTIONBUTTON_THEME_ORANGE'), red: app.lang.get('LBL_ACTIONBUTTON_THEME_RED'), coral: app.lang.get('LBL_ACTIONBUTTON_THEME_CORAL'), pink: app.lang.get('LBL_ACTIONBUTTON_THEME_PINK'), purple: app.lang.get('LBL_ACTIONBUTTON_THEME_PURPLE') } }, icon: { template: 'actionbutton-enum-property', label: 'LBL_ACTIONBUTTON_ICON', id: 'icon', useIcon: true, value: activeButton.properties.icon, formatOptions: function(option) { return '<div><i class="sicon ' + option.text + '"></i>' + option.text + '</div>'; }, options: {} }, isDependent: { template: 'actionbutton-bool-property', label: 'LBL_ACTIONBUTTON_IS_DEPENDENT', id: 'isDependent', value: app.utils.isTruthy(activeButton.properties.isDependent), }, stopOnError: { template: 'actionbutton-bool-property', label: 'LBL_ACTIONBUTTON_STOP_ON_ERROR', id: 'stopOnError', value: app.utils.isTruthy(activeButton.properties.stopOnError), }, }; this.properties.icon.options = { 'sicon-arrow-down': 'sicon-arrow-down', 'sicon-chevron-down': 'sicon-chevron-down', 'sicon-chevron-left': 'sicon-chevron-left', 'sicon-chevron-right': 'sicon-chevron-right', 'sicon-check': 'sicon-check', 'sicon-clock': 'sicon-clock', 'sicon-dashboard-default': 'sicon-dashboard-default', 'sicon-dashboard': 'sicon-dashboard', 'sicon-edit': 'sicon-edit', 'sicon-caret-down': 'sicon-caret-down', 'sicon-folder': 'sicon-folder', 'sicon-info': 'sicon-info', 'sicon-kebab': 'sicon-kebab', 'sicon-link': 'sicon-link', 'sicon-list': 'sicon-list', 'sicon-logout': 'sicon-logout', 'sicon-minus': 'sicon-minus', 'sicon-folder-open': 'sicon-folder-open', 'sicon-plus-sm': 'sicon-plus-sm', 'sicon-refresh': 'sicon-refresh', 'sicon-plus': 'sicon-plus', 'sicon-arrow-up': 'sicon-arrow-up', 'sicon-settings': 'sicon-settings', 'sicon-arrow-right-double': 'sicon-arrow-right-double', 'sicon-reports': 'sicon-reports', 'sicon-user': 'sicon-user', 'sicon-upload': 'sicon-upload', 'sicon-user-group': 'sicon-user-group', 'sicon-arrow-left-double': 'sicon-arrow-left-double', 'sicon-chevron-up': 'sicon-chevron-up', 'sicon-remove': 'sicon-remove', 'sicon-caret-up': 'sicon-caret-up', 'sicon-star-fill': 'sicon-star-fill', 'sicon-download': 'sicon-download', 'sicon-close': 'sicon-close', 'sicon-tile-view': 'sicon-tile-view', 'sicon-list-view': 'sicon-list-view', 'sicon-thumbs-down': 'sicon-thumbs-down', 'sicon-warning-circle': 'sicon-warning-circle', 'sicon-phone': 'sicon-phone', 'sicon-email': 'sicon-email', 'sicon-document': 'sicon-document', 'sicon-note': 'sicon-note', 'sicon-preview': 'sicon-preview', 'sicon-copy': 'sicon-copy', 'sicon-launch': 'sicon-launch', 'sicon-mask': 'sicon-mask', 'sicon-lock': 'sicon-lock', 'sicon-arrow-top-right': 'sicon-arrow-top-right', 'sicon-full-screen': 'sicon-full-screen', 'sicon-full-screen-exit': 'sicon-full-screen-exit', 'sicon-expand-left': 'sicon-expand-left', 'sicon-expand-right': 'sicon-expand-right', 'sicon-focus-drawer': 'sicon-focus-drawer', 'sicon-ban': 'sicon-ban', 'sicon-thumbs-up': 'sicon-thumbs-up', 'sicon-search': 'sicon-search', 'sicon-calendar': 'sicon-calendar', 'sicon-calendar-lg': 'sicon-calendar-lg', 'sicon-mobile-lg': 'sicon-mobile-lg', 'sicon-star-fill-lg': 'sicon-star-fill-lg', 'sicon-star-outline-lg': 'sicon-star-outline-lg', 'sicon-refresh-lg': 'sicon-refresh-lg', 'sicon-exchange-lg': 'sicon-exchange-lg', 'sicon-help-lg': 'sicon-help-lg', 'sicon-close-lg': 'sicon-close-lg', 'sicon-plus-lg': 'sicon-plus-lg', 'sicon-shortcuts-lg': 'sicon-shortcuts-lg', 'sicon-search-lg': 'sicon-search-lg', 'sicon-phone-lg': 'sicon-phone-lg', 'sicon-email-lg': 'sicon-email-lg', 'sicon-note-lg': 'sicon-note-lg', 'sicon-document-lg': 'sicon-document-lg', 'sicon-add-dashlet-lg': 'sicon-add-dashlet-lg', 'sicon-collapse-lg': 'sicon-collapse-lg', 'sicon-hamburger-lg': 'sicon-hamburger-lg', 'sicon-pin-lg': 'sicon-pin-lg', 'sicon-expand-lg': 'sicon-expand-lg', 'sicon-copy-lg': 'sicon-copy-lg', 'sicon-dashboard-lg': 'sicon-dashboard-lg', 'sicon-trash-lg': 'sicon-trash-lg', 'sicon-star-outline': 'sicon-star-outline' }; }, /** * Clear out formula builder field wrapper contents and dispose the sidecar component * */ _disposeFormulaBuilder: function() { this.$('span[data-fieldname="formula"]').empty(); if (this._formulaBuilder) { this._formulaBuilder.dispose(); } }, /** * @inheritdoc */ _dispose: function() { this._disposeFormulaBuilder(); this._super('_dispose'); }, }) }, "actionbutton-compose-email": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Compose Email action configuration view * * @class View.Views.Base.AdministrationActionbuttonComposeEmailView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonComposeEmailView * @extends View.View */ ({ // Actionbutton-compose-email View (base) events: { 'change input[type=checkbox][data-fieldname=bpm]': 'bpmChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._buttonId = options.buttonId; this._actionId = options.actionId; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { id: '', name: '', emailToFormula: '', pmse: false, }; } this._properties.pmse = app.utils.isTruthy(this._properties.pmse); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSelection(); this._createFormulaBuilder(); }, /** * Handle update of selected PMSE Email Template change * * @param {UIEvent} e * */ bpmChanged: function(e) { this._properties = { id: '', name: '', pmse: e.currentTarget.checked }; this._updateActionProperties(); this.render(); }, /** * Handle Recipient formula change * * @param {Object} data * */ formulaChanged: function(data) { this._properties.emailToFormula = data; this._updateActionProperties(); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { return this._formulaBuilder.isValid(); }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Update action properties & UI based on selection * * @param {Object} selection * */ setValue: function(selection) { if (selection) { this._properties = { id: selection.id, name: selection.name, pmse: this._properties.pmse, emailToFormula: this._properties.emailToFormula }; this._updateSelect2View(); this._updateActionProperties(); } }, /** * Create the formula builder sidecar field * */ _createFormulaBuilder: function() { this.disposeFormulaBuilderField(); var formulaContainer = this.$('div[data-fieldname="formula"]'); formulaContainer.empty(); this._formulaBuilder = app.view.createField({ def: { type: 'formula-builder', name: 'ABCustomAction' }, view: this, viewName: 'edit', targetModule: this.options.context.get('model').get('module'), callback: _.bind(this.formulaChanged, this), formula: this._properties.emailToFormula }); this._formulaBuilder.render(); formulaContainer.append(this._formulaBuilder.$el); }, /** * Update Select2 selection with configured action * */ _updateSelect2View: function() { if (this.disposed) { return; } this.$('[name="template_name"]').select2('data', { id: this._properties.id, text: this._properties.name }); }, /** * Update action properties in context * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Create relate field against Users module * */ _createSelection: function() { this.disposeTemplateSelectField(); var moduleName = this._properties.pmse ? 'pmse_Emails_Templates' : 'EmailTemplates'; this.model.set({ template_name: this._properties.name, template_id: this._properties.id, name: this._properties.name }); this._templateSelectionField = app.view.createField({ def: { type: 'relate', module: moduleName, name: 'template_name', rname: 'name', id_name: 'template_id' }, view: this, viewName: 'edit', }); this._templateSelectionField.render(); this._templateSelectionField.setValue = _.bind(this.setValue, this); var templateContainer = this.$('[data-fieldname="template"]'); templateContainer.empty(); templateContainer.append(this._templateSelectionField.$el); if (this._properties.id !== '') { this._updateSelect2View(); } }, /** * Dipose the sidecar relate field created for template selection * */ disposeTemplateSelectField: function() { if (this._templateSelectionField) { this._templateSelectionField.dispose(); this._templateSelectionField = null; } }, /** * Dispose the formula builder field created for recipient calculation * */ disposeFormulaBuilderField: function() { if (this._formulaBuilder) { this._formulaBuilder.dispose(); this._formulaBuilder = null; } }, /** * @inheritdoc */ _dispose: function() { this.disposeTemplateSelectField(); this.disposeFormulaBuilderField(); this._super('_dispose'); }, }) }, "timeline-config-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationTimelineConfigHeaderView * @alias SUGAR.App.view.views.BaseTimelineConfigHeaderView * @extends View.Views.Base.AdministrationConfigHeaderView */ ({ // Timeline-config-header View (base) extendsFrom: 'AdministrationConfigHeaderView', /** * Get title for this header * @return {string} */ getTitle: function() { let title = ''; let module = this.context.get('target'); if (module) { title = app.lang.get('TPL_ACTIVITY_TIMELINE_SETTINGS', 'Administration', {moduleSingular: app.lang.getModuleName(module)}); } return title; }, /** * Enable the save button. * * @inheritdoc */ _render: function(options) { this._super('_render', [options]); this.enableButton(true); }, }) }, "actionbutton-open-url": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Open URL action configuration view * * @class View.Views.Base.AdministrationActionbuttonAssignRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonAssignRecordView * @extends View.View */ ({ // Actionbutton-open-url View (base) events: { 'change input[type="checkbox"][data-fieldname="calculated"]': 'calculatedChanged', 'change textarea[data-fieldname="url"]': 'urlChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._buttonId = options.buttonId; this._actionId = options.actionId; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { url: '', calculated: false, formula: '' }; } this._properties.calculated = app.utils.isTruthy(this._properties.calculated); this._formulaBuilder = null; }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this._properties.calculated) { this._createFormulaBuilder(); } }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Handler for calculated URL checkbox selection * * @param {UIEvent} e * */ calculatedChanged: function(e) { this._properties.calculated = e.currentTarget.checked; this._updateActionProperties(); this.render(); }, /** * Handler for URL value change * * @param {UIEvent} e * */ urlChanged: function(e) { this._properties.url = e.currentTarget.value; this._updateActionProperties(); }, /** * Handler for URL formula change * * @param {Object} data * */ formulaChanged: function(data) { this._properties.formula = data; this._updateActionProperties(); }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { if (this._properties.calculated) { return this._formulaBuilder.isValid(); } return true; }, /** * Create the formula builder sidecar field * */ _createFormulaBuilder: function() { this.$('[data-fieldname="url"]').hide(); this._disposeFormulaBuilder(); var fbContainer = this.$('[data-fieldname="formula"]'); fbContainer.empty(); fbContainer.toggleClass('hidden', false); this._formulaBuilder = app.view.createField({ def: { type: 'formula-builder', name: 'ABCustomAction' }, view: this, viewName: 'edit', targetModule: this.options.context.get('model').get('module'), callback: _.bind(this.formulaChanged, this), formula: this._properties.formula }); this._formulaBuilder.render(); fbContainer.append(this._formulaBuilder.$el); }, /** * Update action properties in context * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Dispose formula builder field * */ _disposeFormulaBuilder: function() { if (this._formulaBuilder) { this._formulaBuilder.dispose(); this._formulaBuilder = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeFormulaBuilder(); this._super('_dispose'); }, }) }, "config-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Base header view for Config Framework. * * @class View.Views.Base.AdministrationConfigHeaderView * @alias SUGAR.App.view.views.BaseAdministrationConfigHeaderView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * Config title * @property {string} */ title: '', /** * @inheritdoc */ initialize: function(options) { if (!options.meta) { options.meta = app.metadata.getView(options.context.get('module'), 'config-header'); } this._super('initialize', [options]); this.title = this.getTitle(); }, /** * @inheritdoc */ _loadTemplate: function(options) { this.templateName = this.name; this.template = app.template.getView(this.templateName, this.module) || app.template.getView('config-header', this.module); }, /** * Disable the save button. * * @inheritdoc */ _render: function(options) { this._super('_render', [options]); this.enableButton(false); }, /** * Get title for this header * @return {string} */ getTitle: function() { var title = this.meta && this.meta.label || ''; if (!title) { var category = this.context.get('category'); if (category) { var configView = this.layout.getComponent(category + '-config'); if (configView) { title = configView.meta.label || ''; } } } return title; }, /** * Trigger save process. * * @inheritdoc */ saveConfig: function() { this.context.trigger('save:config'); }, /** * Toggle the save button enabled/disabled state. * * @param {boolean} flag True for enabling the button. */ enableButton: function(flag) { this.getField('save_button').setDisabled(!flag); } }) }, "actionbutton-preview-action-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Parent field view selection * * @class View.Views.Base.AdministrationActionbuttonPreviewActionMenuView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonPreviewActionMenuView * @extends SUGAR.App.view.views.BaseAdministrationActionbuttonPreviewRecordView */ ({ // Actionbutton-preview-action-menu View (base) extendsFrom: 'AdministrationActionbuttonPreviewRecordView', /** * @inheritdoc */ _getPreparedButtonsData: function(buttonsData) { let data = this._super('_getPreparedButtonsData', [buttonsData]); data.settings.type = 'dropdown'; _.each(data.buttons, function increaseOrderNumber(buttonData) { buttonData.orderNumber = buttonData.orderNumber + 1; }); const dropdownButtonData = { actions: {}, active: true, buttonId: app.utils.generateUUID(), orderNumber: 0, properties: { label: 'Edit', colorScheme: 'primary', showLabel: true, showIcon: false, }, }; data.buttons[dropdownButtonData.buttonId] = dropdownButtonData; return data; }, }) }, "actionbutton-parent-field": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Parent field view selection * * @class View.Views.Base.AdministrationActionbuttonParentFieldView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonParentFieldView * @extends View.View */ ({ // Actionbutton-parent-field View (base) /** * Fields which should not be available to automatically update */ badFields: [ 'deleted', 'team_count', 'account_description', 'opportunity_role_id', 'opportunity_role_fields', 'opportunity_role', 'email_and_name1', 'dnb_principal_id', 'email1', 'email2', 'email_addresses', 'email_addresses_non_primary', 'email_addresses_primary', 'email_and_name1', 'primary_address_street_2', 'primary_address_street_3', 'alt_address_street_2', 'alt_address_street_3', 'portal_app', 'portal_user_company_name', 'mkto_sync', 'mkto_id', 'mkto_lead_score', 'cookie_consent', 'cookie_consent_received_on', 'dp_consent_last_updated', 'accept_status_id', 'sync_key', 'locked_fields', 'billing_address_street_2', 'billing_address_street_3', 'billing_address_street_4', 'shipping_address_street_2', 'shipping_address_street_3', 'shipping_address_street_4', 'related_languages', ], /** * Field types which should not be available to automatically update */ badFieldTypes: [ 'link', 'id', 'collection', 'widget', 'html', 'htmleditable_tinymce', 'image', 'teamset', 'team_list', 'email', 'password', 'file' ], /** * Event listeners */ events: { 'click [data-action=remove-field]': 'removeField', 'change [data-fieldname=field]': 'parentFieldChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._properties = { _fieldName: options.fieldName, _parentFieldName: options.parentFieldName, }; this._callback = options.callback; this._deleteCallback = options.deleteCallback; if (options.fieldModule) { this._fieldDef = app.metadata.getModule(options.fieldModule).fields[options.fieldName]; this._fieldType = this._fieldDef.type; this._module = options.fieldModule; this._fieldLabel = app.lang.get(this._fieldDef.vname, this._module); this._parentModule = options.context.get('model').get('module'); this._populateParentFields(this._parentModule); if (options.parentFieldName) { this._parentFieldDef = app.metadata.getModule(this._parentModule).fields[options.parentFieldName]; this._parentFieldLabel = app.lang.get(this._parentFieldDef.vname, this._parentModule); } } }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * Update list of available fields to copy from the parent module * * @param {string} module Parent module */ _populateParentFields: function(module) { var fields = _.chain(app.metadata.getModule(module).fields).values(); fields = fields.filter(this._checkFieldCompatibility, this) .map(function fieldToTuple(field) { return [field.name, app.lang.get(field.vname, module)]; }).value(); this._parentFields = _.object(fields); }, /** * Check field copy value compatibility * * @param {string} field * * @return {bool} */ _checkFieldCompatibility: function(field) { if ( this._normalizeType(this._fieldType) === 'parent' && this._normalizeType(field.type) === 'relate' ) { const modules = app.lang.getAppListStrings(this._fieldDef.options); return this.badFields.indexOf(field.name) === -1 && modules.hasOwnProperty(field.module) && !_.isEmpty(field.vname); }; if ( this._normalizeType(this._fieldType) === 'relate' && this._normalizeType(field.type) === 'relate' ) { return this._fieldDef.module === field.module; }; return ( !_.isEmpty(field.name) && !_.isEmpty(field.vname) && !_.contains(this.badFields, field.name) && !_.contains(this.badFieldTypes, field.type) && this._normalizeType(field.type) === this._normalizeType(this._fieldType) && field.studio !== false && field.calculated !== true ); }, /** * Normalize a field type * * @param {string} field * * @return {string} */ _normalizeType: function(type) { if (type === 'name') { type = 'varchar'; } return type; }, /** * @inheritdoc */ _render: function() { this._super('_render'); // add the style that couldn't be added via hbs this.$el.addClass('span6 ab-parent-field-wrapper ' + this._properties._fieldName); this._createSelect2(); if (this._properties._parentFieldName !== '') { this.$field.select2('data', { id: this._properties._parentFieldName, text: this._parentFieldLabel }); } }, /** * Remove field selection handler * * @param {UIEvent} e * */ removeField: function(e) { if (this._deleteCallback) { this._deleteCallback(this._properties._fieldName); } }, /** * Parent field change handler * * @param {UIEvent} e * */ parentFieldChanged: function(e) { this._properties._parentFieldName = e.currentTarget.value; if (this._callback) { this._callback(this._properties); } }, /** * Create select2 control * */ _createSelect2: function() { this.$field = this.$('[data-fieldname=field]'); this.$field.select2(this._getSelect2Options()) .data('select2'); }, /** * Build select2 options * */ _getSelect2Options: function() { var select2Options = {}; select2Options.placeholder = app.lang.get('LBL_ACTIONBUTTON_SELECT_FIELD'); select2Options.query = _.bind(this._queryFields, this); select2Options.dropdownAutoWidth = true; return select2Options; }, /** * Build select2 query function * * @param {Function} query * * @return {Function} */ _queryFields: function(query) { this._query(query, '_parentFields'); }, /** * Generic select2 query function implementation * * @param {string} query * @param {Object} options * */ _query: function(query, options) { var listElements = this[options]; var data = { results: [], more: false }; if (_.isObject(listElements)) { _.each(listElements, function pushValidResults(element, index) { if (query.matcher(query.term, element)) { data.results.push({id: index, text: element}); } }); } else { listElements = null; } query.callback(data); }, }) }, "relate-denormalization": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDenormFrameworkView * @alias SUGAR.App.view.views.BaseDenormFrameworkView * @extends View.Views.Base.ConfigPanelView */ ({ // Relate-denormalization View (base) extendsFrom: 'ConfigPanelView', denormFieldList: null, moduleFields: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); var self = this; this.model.on('change:modules', function() { self.updateFieldList(); }); this.context.on('relate-denormalization:save', _.bind(this.save, this)); this.context.on('field-lists:pending', function() { self.layout.getComponent('config-header-buttons').enableButton(true); }); this.context.on('field-lists:initial', function() { self.layout.getComponent('config-header-buttons').enableButton(false); }); }, /** * @inheritdoc */ render: function() { this._super('render'); var module = app.cache.get('relate-denormalization.selected-module'); if (module) { this.model.set('modules', module); } }, updateFieldList: function() { var moduleList = this.getField('modules'); if (!moduleList || moduleList.waitingForRender) { return; } var module = moduleList.getFormattedValue(); app.cache.set('relate-denormalization.selected-module', module); var fields = this.getFieldsForModule(module); this.getField('field-lists').refresh(fields, this.getField('modules').getFieldsForModule(module)); this.layout.getComponent('config-header-buttons').enableButton(false); }, getFieldsForModule: function(module) { if (this.moduleFields === null || this.moduleFields[module] === undefined) { var moduleDef = App.metadata.getModule(module); if (!moduleDef) { return []; } var moduleFields = []; var listView = moduleDef.views ? moduleDef.views.list : null; if (listView && listView.meta && listView.meta.panels && listView.meta.panels[0] && listView.meta.panels[0].fields) { moduleFields = listView.meta.panels[0].fields; } var fieldDefs = moduleDef.fields || []; moduleFields = _.map(moduleFields, function(field) { let isSortable = field.sortable !== false; field = fieldDefs[field.original_name || field.name] || field; field.sortable = isSortable; return field; }); moduleFields = _.filter(moduleFields, function(field) { // Users.full_name field ignored due to complicated structure (last_name + first_name) let isUserFullName = field.rname === 'full_name' && field.module === 'Users'; let isSortable = field.sortable !== false; return field.type === 'relate' && !isUserFullName && isSortable; }); this.moduleFields = this.moduleFields || {}; this.moduleFields[module] = moduleFields; } return this.moduleFields[module] || []; }, save: function() { self = this; app.api.call( 'create', app.api.buildURL(this.module, 'denormalization/pre-check'), this.model.toJSON(), { success: _.bind(function(data) { if (data && data.message) { app.alert.show('relate-denormalization-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: data.message, }); } else if (data && data.report) { data = self.preparePreCheckResponse(data); var template = app.template.getView('relate-denormalization.pre-check-result', this.module); data.module = this.module; app.alert.show('relate-denormalization-warning', { level: data.message_type, title: app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_PRE_CHECK_RESULTS', this.module), messages: template(data), onConfirm: _.bind(function() { this.runProcess(data); }, this) }); } }, this), error: function() { app.alert.show('relate-denormalization-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), }); } } ); }, runProcess: function(preCheckData) { self = this; var template = app.template.getView('relate-denormalization.start-process', this.module); var modal = app.alert.show('start-process', { title: app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_PROCESS_RUNNING', this.module) + ' ' + ( preCheckData.report[0].is_denormalized ? app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_NORMALIZATION', this.module) : app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_DENORMALIZATION', this.module) ), messages: template(preCheckData) }); self.layout.getComponent('config-header-buttons').enableButton(false); var metadataSyncRequired = true; app.api.call( 'create', app.api.buildURL(this.module, 'denormalization/apply'), this.model.toJSON(), { success: function(data) { if (!data || !data.ok) { app.alert.show('relate-denormalization-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: data.message || 'Error' }); } else if (data.denormalized) { metadataSyncRequired = false; } }, error: function() { app.alert.show('relate-denormalization-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_COMMUNICATION_ERROR', self.module) }); }, complete: function() { if (metadataSyncRequired) { app.metadata.sync(function() { self.getField('job-list').loadData(); modal.close(); app.alert.show('relate-denormalization-success', { level: 'info', title: 'success', messages: app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_SYNC_COMPLETE', self.module), autoClose: true }); self.getField('modules').load(); }); } else { app.alert.show('relate-denormalization-success', { level: 'info', title: 'success', messages: app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_SYNC_COMPLETE', self.module), autoClose: true }); self.getField('job-list').loadData(); self.getField('modules').load(); modal.close(); } } }, {cache: false} ); }, preparePreCheckResponse: function(data) { data.message_type = 'error'; if (data.overall_possibility) { data.message_type = 'confirmation'; } _.each(data.report, function(item) { if (item.details && item.details.count_rhs) { var det = item.details; det.estimation_unit = 'seconds'; if (det.estimated_time > 180) { det.estimation_unit = 'minutes'; det.estimated_time = Math.round(det.estimated_time / 60); } if (det.estimated_time > 180) { det.estimation_unit = 'hours'; det.estimated_time = Math.round(det.estimated_time / 60); } det.count_rhs = det.count_rhs.toLocaleString(); det.count_rel = det.count_rel.toLocaleString(); det.count_lhs = det.count_lhs.toLocaleString(); } }); return data; } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OpportunitiesConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseOpportunitiesConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', render: function(options) { this._super('render', options); this.enableButton(false); }, /** * @inheritdoc * @param {*} onClose */ showSavedConfirmation: function(onClose) { app.alert.dismiss('opp.config.save'); this._super('showSavedConfirmation', [onClose]); }, /** * Displays confirm alert */ displayWarningAlertDenormFramework: function() { var message = app.lang.get('LBL_MANAGE_RELATE_DENORMALIZATION_SAVE_WARNING', 'Administration'); app.alert.show('relate-denormalization-warning', { level: 'confirmation', title: app.lang.get('LBL_WARNING'), messages: message, onConfirm: _.bind(function() { this.context.trigger('relate-denormalization:save'); }, this) }); }, /** * Overriding the default saveConfig to display the warning alert first, then on confirm of the * warning alert, save the config. * * @inheritdoc */ saveConfig: function() { this.displayWarningAlertDenormFramework(); }, enableButton: function(flag) { this.getField('save_button').setDisabled(!flag); } }) }, "portaltheme-config-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPortalThemeConfigHeaderView * @alias SUGAR.App.view.views.BasePortalThemeConfigHeaderView * @extends View.Views.Base.AdministrationConfigHeaderView */ ({ // Portaltheme-config-header View (base) extendsFrom: 'AdministrationConfigHeaderView', /** * URL of main portal config page. Used to navigate back on clicking "cancel" */ portalConfigUrl: '#bwc/index.php?module=ModuleBuilder&action=index&type=sugarportal', /** * Event handler for clicking "Cancel" button * @param evt */ cancelConfig: function(evt) { evt.stopPropagation(); window.location.href = this.portalConfigUrl; } }) }, "actionbutton-update-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Update Record action configuration view * * @class View.Views.Base.AdministrationActionbuttonUpdateRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonUpdateRecordView * @extends View.View */ ({ // Actionbutton-update-record View (base) /** * Fields which should not be available to automatically update */ badFields: [ 'deleted', 'team_count', 'account_description', 'opportunity_role_id', 'opportunity_role_fields', 'opportunity_role', 'email_and_name1', 'dnb_principal_id', 'email1', 'email2', 'email_addresses', 'email_addresses_non_primary', 'email_addresses_primary', 'email_and_name1', 'primary_address_street_2', 'primary_address_street_3', 'alt_address_street_2', 'alt_address_street_3', 'portal_app', 'portal_user_company_name', 'mkto_sync', 'mkto_id', 'mkto_lead_score', 'cookie_consent', 'cookie_consent_received_on', 'dp_consent_last_updated', 'accept_status_id', 'sync_key', 'locked_fields', 'billing_address_street_2', 'billing_address_street_3', 'billing_address_street_4', 'shipping_address_street_2', 'shipping_address_street_3', 'shipping_address_street_4', 'related_languages', ], /** * Field types which should not be available to automatically update */ badFieldTypes: [ 'link', 'id', 'collection', 'widget', 'html', 'htmleditable_tinymce', 'image', 'teamset', 'team_list', 'email', 'password', 'file' ], /** * Event listeners */ events: { 'change input[type="checkbox"][data-fieldname="auto-save"]': 'autoSaveFlagChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Quick initialization of action properties * * @param {Object} options * */ _beforeInit: function(options) { var ctxModel = options.context.get('model'); this._actionId = options.actionId; this._buttonId = options.buttonId; this._module = ctxModel.get('module'); this._fields = {}; if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { fieldsToBeUpdated: {}, autoSave: false, }; } this._properties.autoSave = app.utils.isTruthy(this._properties.autoSave); this._populateFields(this._module); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * Updates the currently selected module fields with anything that can be updated * * @param {string} module * */ _populateFields: function(module) { var fields = _.chain(app.metadata.getModule(module).fields).values(); fields = fields.filter( function filterField(field) { return ( !_.isEmpty(field.name) && !_.isEmpty(field.vname) && !_.contains(this.badFields, field.name) && !_.contains(this.badFieldTypes, field.type) && field.studio !== false && field.readonly !== true && field.calculated !== true ); }, this ).map(function fieldToTuple(field) { return [field.name, app.lang.get(field.vname, module)]; }).value(); this._fields = _.object(fields); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSelect2(_.bind(this._addUpdateField, this)); this._createExistingControllers(); }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Update action configuration * * @param {Object} data * */ updateProperties: function(data) { this._properties.fieldsToBeUpdated[data._fieldName] = { fieldName: data._fieldName, isCalculated: data._isCalculated, formula: data._formula, value: data._value }; this._updateActionProperties(); }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Handler for autosave checkbox change event * * @param {UIEvent} e * */ autoSaveFlagChanged: function(e) { this._properties.autoSave = e.currentTarget.checked; this._updateActionProperties(); }, /** * Create field value sidecar components for configured fields * */ _createExistingControllers: function() { _.each(this._properties.fieldsToBeUpdated, function create(data, name) { this._createUpdateFieldController(data); }, this); }, /** * Create the select2 field selector control * * @param {Function} callback * */ _createSelect2: function(callback) { this.$field = this.$('[data-fieldname="field"]') .select2(this._getSelect2Options()) .data('select2'); this.$field.onSelect = (function select(fn) { return function returnCallback(data, options) { if (callback) { callback(data); } // after each select we set the default label if (arguments) { arguments[0] = { id: 'select', text: app.lang.get('LBL_ACTIONBUTTON_SELECT_FIELD') }; } return fn.apply(this, arguments); }; })(this.$field.onSelect); }, /** * Create generic Select2 options object * * @param {string} queryFunc * * @return {Object} */ _getSelect2Options: function() { var select2Options = {}; select2Options.placeholder = app.lang.get('LBL_ACTIONBUTTON_SELECT_FIELD'); select2Options.query = _.bind(this._queryFields, this); select2Options.dropdownAutoWidth = true; return select2Options; }, /** * Wrapper for querying fields for select2 components * * @param {string} query * */ _queryFields: function(query) { this._query(query, '_fields'); }, /** * Wrapper for querying functions for select2 components * * @param {string} query * */ _query: function(query, options) { var listElements = this[options]; var data = { results: [], more: false }; if (_.isObject(listElements)) { _.each(listElements, function pushValidResults(element, index) { if (query.matcher(query.term, element)) { data.results.push({id: index, text: element}); } }); } else { listElements = null; } query.callback(data); }, /** * Adds a new field update setting * * @param {Object} data * */ _addUpdateField: function(data) { this._properties.fieldsToBeUpdated[data.id] = { fieldName: data.id, isCalculated: false, formula: '', value: '' }; this._updateActionProperties(); this._createUpdateFieldController(this._properties.fieldsToBeUpdated[data.id]); }, /** * Removes a field update setting * * @param {string} fieldId * */ removeUpdateField: function(fieldId) { // remove from data delete this._properties.fieldsToBeUpdated[fieldId]; this._updateActionProperties(); this.disposeField(fieldId); }, /** * Create field value edit component * * @param {Object} fieldData * */ _createUpdateFieldController: function(fieldData) { this.$('.' + fieldData.fieldName).remove(); var updateFieldController = app.view.createView({ name: 'actionbutton-update-field', context: this.context, model: this.context.get('model'), layout: this, isCalculated: fieldData.isCalculated, fieldName: fieldData.fieldName, value: fieldData.value, formula: fieldData.formula, fieldModule: this._module, deleteCallback: _.bind(this.removeUpdateField, this), callback: _.bind(this.updateProperties, this) }); this.$('div[data-container="fields"]').prepend(updateFieldController.$el); updateFieldController.render(); if (!this._subComponents) { this._subComponents = []; } this._subComponents.push(updateFieldController); }, /** * Update action properties in context * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Dipose and remove a given field from the record update list * * @param {string} fieldId * */ disposeField: function(fieldId) { var field = _.find(this._subComponents, function checkField(controller, index) { if (controller && controller._properties) { return controller._properties._fieldName === fieldId; } return false; }); if (field) { this._subComponents = _.chain(this._subComponents).without(field).value(); field.dispose(); } }, /** * @inheritdoc */ _dispose: function() { _.each(this._subComponents, function(component) { component.dispose(); }); this._super('_dispose'); }, }) }, "package-builder-progress-alert": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderProgressAlertView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderProgressAlertView * @extends View.View */ ({ // Package-builder-progress-alert View (base) /** * Initial title */ initialTitle: 'Processing', /** * Success title */ successTitle: 'Success', /** * Error title */ errorTitle: 'Error', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.initialTitle = options.initialTitle ? options.initialTitle : 'Processing'; this.successTitle = options.successTitle ? options.successTitle : 'Success'; this.errorTitle = options.errorTitle ? options.errorTitle : 'Error'; }, /** * Set progress bar percentage * @param {string} */ setProgress: function(percentage) { this.$el.find('.progress .bar').css('width', percentage); this.$el.find('h5').html(percentage); }, /** * Set seccess message * @param {string} */ processSuccessful: function(message) { message = '<br/>' + message; this.$el.find('#ci-progress-lbl').toggle(); this.$el.find('#ci-progress-success-lbl').toggle(); this.$el.find('.alert-info').removeClass('alert-info').addClass('alert-success'); this.$el.find('button').toggle(); this.$el.find('.progress .bar').css('width', '100%'); this.$el.find('h5').html('100%'); this.$el.find('pre').append('<b>' + message + '</b>') .scrollTop(function getScHeight() { return this.scrollHeight; }); }, /** * Set error message * @param {string} */ processFailed: function(errorMessage) { errorMessage = '<br/>' + errorMessage; this.$el.find('pre').append('<b style="color: red">' + errorMessage + '</b>') .scrollTop(function getScHeight() { return this.scrollHeight; }); this.$el.find('#ci-progress-lbl').toggle(); this.$el.find('#ci-progress-error-lbl').toggle(); this.$el.find('.alert-info').removeClass('alert-info').addClass('alert-danger'); this.$el.find('button').toggle(); this.$el.find('.sicon-chevron-down').parent().find('a').click(); }, /** * Log message * @param {string} */ logMessage: function(messageText) { // Logger container let preEl = this.$el.find('pre'); messageText = messageText + '<br/>'; preEl.scrollTop(function getScHeight() { return this.scrollHeight; }); preEl.append(messageText); }, }) }, "maps-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsConfigView * @alias SUGAR.App.view.views.BaseAdministrationMapsConfigView * @extends View.Views.Base.AdministrationConfigView */ ({ // Maps-config View (base) extendsFrom: 'AdministrationConfigView', prefix: 'maps_', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Property initialization * */ _initProperties: function() { this.meta.label = app.lang.get('LBL_MAP_CONFIG_TITLE'); this.saveMessage = 'LBL_SAVE_SUCCESS'; this.context.safeRetrieveModulesData = _.bind(this.safeRetrieveModulesData, this); }, /** * @inheritdoc */ copySettingsToModel: function(settings) { this._super('copySettingsToModel', [settings]); let data = {}; _.each(settings, function resolveData(value, key) { if (key.indexOf(this.prefix) === 0) { data[key.replace(this.prefix, '')] = value; } }, this); this.context.trigger('retrived:maps:config', data); }, /** * Force load the header-view from layout * * It will change the Save button enabled/disabled state. * * @param {boolean} state The state to be set. */ toggleHeaderButton: function(state) { var header = this.layout.getComponent('config-header'); if (header) { header.enableButton(state); } }, /** * Retreive a deep clone of settings data * * @return {Object} */ safeRetrieveModulesData: function(module) { const _modulesData = _.isEmpty(this.model.get('maps_modulesData')) ? {} : this.model.get('maps_modulesData'); let modulesData = app.utils.deepCopy(_modulesData); if (_.isEmpty(modulesData)) { modulesData[module] = {}; } if (!_.has(modulesData, module)) { modulesData[module] = {}; } if (!_.has(modulesData[module], 'mappings')) { modulesData[module].mappings = { 'mappings': true }; modulesData[module].mappingType = 'moduleFields'; modulesData[module].mappingRecord = {}; } if (!_.has(modulesData[module], 'settings')) { modulesData[module].settings = { 'autopopulate': false }; } if (!_.has(modulesData[module], 'subpanelConfig')) { modulesData[module].subpanelConfig = []; } return modulesData; }, /** * Ensure that all modules are configured; * * @return {boolean} */ canSave: function() { const enabledModules = this.model.get('maps_enabled_modules'); const modulesData = this.model.get('maps_modulesData'); let invalidModules = _.filter(enabledModules, function isModuleValid(module) { const moduleData = modulesData[module]; return ((moduleData && !moduleData.mappings) || !moduleData); }); if (_.isEmpty(invalidModules)) { return true; } app.alert.show('maps-invalid-module-config', { level: 'error', title: app.lang.get('LBL_MAPS_CONFIG_INVALID_MODULE_TITLE'), messages: app.lang.get('LBL_MAPS_CONFIG_INVALID_MODULE', null, { module: invalidModules.toString(), }), autoClose: true }); return false; }, /** * @inheritdoc */ save: function() { if (!this.canSave()) { return; } const options = { error: _.bind(this.saveErrorHandler, this), success: _.bind(this.saveSuccessHandler, this) }; const apiUrl = app.api.buildURL(`${this.module}/maps`, this.settingPrefix); app.api.call('create', apiUrl, this.model.toJSON(), options); }, /** * @inheritdoc */ saveErrorHandler: function(e) { app.alert.show(this.settingPrefix + '-warning', { level: 'error', title: app.lang.get('LBL_ERROR'), messages: e.message ? e.message : app.lang.get('LBL_AWS_SAVING_ERROR', this.module), }); }, }) }, "package-builder-installed-packages-subtab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderInstalledPackagesSubtabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderInstalledPackagesSubtabView * @extends View.View */ ({ // Package-builder-installed-packages-subtab View (base) /** * Entries to display in the table */ entries: [], /** * Headers to display in the table */ headers: [ 'Name', 'Version', 'Description', ], /** * Header labels */ headerLabels: [ 'LBL_PACKAGE_BUILDER_NAME', 'LBL_PACKAGE_BUILDER_VERSION', 'LBL_PACKAGE_BUILDER_DESCRIPTION', ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // Sync view customizations this.entries = options.installedPackages; }, }) }, "package-builder-packages-tab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderPackagesTabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderPackagesTabView * @extends View.View */ ({ // Package-builder-packages-tab View (base) /** * Package subtabs */ packagesSubtabs: {}, /** * Package subtabs views */ packagesSubtabsView: {}, /** * Connection info for the remote instance */ connectionInfo: {}, /** * Active subtab */ activeSubtab: 'installed_packages', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setDefaultValues(); }, /** * Set default values for the subtabs */ setDefaultValues: function() { this.packagesSubtabs = { 'installed_packages': { 'value': 'installed_packages', 'label': 'LBL_PACKAGE_BUILDER_TAB_PACKAGES_I_PACKAGES_T', 'active': true, 'disabled': false }, 'connection': { 'value': 'connection', 'label': 'LBL_PACKAGE_BUILDER_TAB_PACKAGES_CONNECTION_T', 'active': false, 'disabled': false }, 'remote_packages': { 'value': 'remote_packages', 'label': 'LBL_PACKAGE_BUILDER_TAB_PACKAGES_REMOTE_PACKAGES_T', 'active': false, 'disabled': true }, }; }, /** * @inheritdoc */ render: function() { // Handle the right active tab rendering this.buildTabs(); // Call super this._super('render'); this.initEvents(); this.showPackagesSubtabContent(); }, /** * Build tabs */ buildTabs: function() { _.each(this.packagesSubtabs, function(subtab) { subtab.active = (subtab.value === this.activeSubtab); }.bind(this)); }, /** * Add event listeners */ initEvents: function() { // Tabs events let subtabs = this.$el.find('.packagesSubtab'); _.each(subtabs, function(subtab) { subtab.addEventListener('click', this.packagesSubtabChanged.bind(this)); }.bind(this)); }, /** * Change the active subtab * @param {Object} $el */ packagesSubtabChanged: function($el) { this.activeSubtab = $el.target.getAttribute('name'); this.activeSubtabChanged(); if (this.activeSubtab === 'remote_packages') { this.$el.find('.' + this.activeSubtab + 'Dot').hide(); this.$el.find('.' + this.activeSubtab + '_tab').removeClass('start_animation'); this.packagesSubtabs.remote_packages.disabled = false; } this.showPackagesSubtabContent(); }, /** * Change the active subtab */ activeSubtabChanged: function() { let oldActiveSubtab = this.$el.find('.package-subtab-list .tab.active')[0]; let newActiveSubtab = this.$el.find('.package-subtab-list .' + this.activeSubtab + '_tab')[0]; if (_.isUndefined(oldActiveSubtab) === false && _.isUndefined(newActiveSubtab) === false && oldActiveSubtab !== newActiveSubtab) { oldActiveSubtab.classList.remove('active'); // Remove previous active tab class newActiveSubtab.classList.add('active'); // Add active class to the current active tab } }, /** * Show the subtab content */ showPackagesSubtabContent: function() { let tabContent = this.$el.find('.subtab-content'); if (this.activeSubtab === 'remote_packages') { this.syncConnectionData(); } let installedPackages = this.context.get('installedPackages') || {}; let otherInstancePackages = this.context.get('otherInstancePackages') || []; if (_.isUndefined(this.packagesSubtabsView[this.activeSubtab])) { let tabName = this.activeSubtab.replaceAll('_', '-'); let tabView = app.view.createView({ name: 'package-builder-' + tabName + '-subtab', //ex: ci-packages-connection-subtab installedPackages: installedPackages, connectionInfo: this.connectionInfo, }); tabView.render(); tabContent.empty(); tabContent.append(tabView.$el); this.packagesSubtabsView[this.activeSubtab] = tabView; } else { tabContent.empty(); tabContent.append(this.packagesSubtabsView[this.activeSubtab].$el); if (this.activeSubtab === 'installed_packages') { this.packagesSubtabsView[this.activeSubtab].entries = installedPackages; } if (this.activeSubtab === 'remote_packages') { this.packagesSubtabsView[this.activeSubtab].entries = otherInstancePackages; this.packagesSubtabsView[this.activeSubtab].localInstancePackages = installedPackages; } this.packagesSubtabsView[this.activeSubtab].connectionInfo = this.connectionInfo; this.packagesSubtabsView[this.activeSubtab].render(); } }, /** * Sync connection data */ syncConnectionData: function() { // If connection view is not rendered then return, we cannot sync if (_.isUndefined(this.packagesSubtabsView.connection)) { return; } if (!_.isUndefined(this.packagesSubtabsView.connection.connectionInfo)) { // Sync connection info (url, user, password) this.connectionInfo = this.packagesSubtabsView.connection.connectionInfo; } }, /** * @inheritdoc */ _dispose: function() { if (_.isObject(this.packagesSubtabsView.connection)) { this.packagesSubtabsView.connection.dispose(); delete this.packagesSubtabsView.connection; } this._super('_dispose'); }, }) }, "csp-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationCspConfigView * @alias SUGAR.App.view.views.BaseAdministrationCspConfigView * @extends View.Views.Base.AdministrationConfigView */ ({ // Csp-config View (base) extendsFrom: 'AdministrationConfigView', /** * @inheritdoc */ initialize: function(options) { this.helpBlockContext = { linkToDocumentation: app.help.getDocumentationUrl('ContentSecurityPolicy') }; this._super('initialize', [options]); this.meta.firstNonHeaderPanelIndex = 0; // there is no header, so it's always 0 }, /** * Triggers the field validation through the model. * Validation of the following components: IPv4 and IPv6 addresses. */ validateModel: function(fields = null) { if (!fields) { // pass 'fake' as the module name so the view fields don't get filtered out fields = this.getFieldNames('fake'); } let isValid = _.every(fields, function(field) { if (this.validateSpecificValue(this.model.get(field))) { this.showValidationAlert(); this.getField(field).decorateError(); return false; } return true; }, this); if (isValid) { this.model.doValidate(this.options.meta.panels[0].fields, _.bind(this.validationComplete, this)); } }, /** * Alert for validation failure. */ showValidationAlert: function() { var message = app.lang.get('LBL_CSP_ERROR_MESSAGE', null, this.helpBlockContext); app.alert.show('csp_send_warning', { level: 'error', messages: message, autoClose: false, }); }, /** * Show alert if validation fails. */ saveErrorHandler: function() { this.showValidationAlert(); }, /** * On a successful save a message will be shown indicating that the settings have been saved. * The page will be reloaded in order to refresh CSP settings in browser. * * @param {Object} settings The CSP settings. */ saveSuccessHandler: function(settings) { this.updateConfig(settings); this.closeView(); app.alert.show(this.settingPrefix + '-info', { autoClose: true, level: 'success', messages: app.lang.get(this.saveMessage, this.module), onAutoClose: () => window.location.reload(true) }); }, /** * Render the help blocks in their respective tabpanel. * * @inheritdoc */ renderHelpBlock: function() { _.each(this.helpBlock, function(help, name) { var $panel = this.$('#' + name + this.cid); if ($panel) { $panel.append(help); } }, this); }, /** * Simple initial validation. The main validation will be handled on the backend. * * @param {string} string The string to validate. * @return {boolean} */ validateSpecificValue: function(string) { return new RegExp(['\'none\'|\'strict-dynamic\'|[,;"]'].join(''), 'g').test(string); } }) }, "package-builder-pagination": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderPaginationView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderPaginationView * @extends View.Views.Base.ListPaginationView */ ({ // Package-builder-pagination View (base) extendsFrom: 'ListPaginationView', /** * Number of rows to display */ limit: 50, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.template = app.template.getView('list-pagination'); this.tableData = options.tableData; this.initCollection(); }, /** * Initialize empty collection */ initCollection: function() { this.collection = app.data.createBeanCollection('package-builder-content'); this.collection.dataFetched = true; this.collection.setOption('limit', this.limit); this.context.set('collection', this.collection); }, /** * @inheritdoc */ bindLayoutEvents: function() {}, /** * @inheritdoc */ getPage: function(page) { this.page = page; const limit = this.collection.getOption('limit'); const from = (page - 1) * limit; const to = page * limit; const indexes = this.context.get('matchingRows').slice(from, to); let newList = []; indexes.map((index) => { newList.push(this.tableData[index]); }); this.collection.reset(newList); this.context.trigger('list:paginate', page); this.render(); }, /** * @inheritdoc */ getPageCount: function() { this.pageTotalFetched(this.context.get('matchingRows').length); }, /** * @inheritdoc */ handleListFilter: function() { this.getPageCount(); this.getFirstPage(true); }, }) }, "maps-module-subpanel-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsModuleSubpanelConfigView * @alias SUGAR.App.view.views.BaseAdministrationMapsModuleSubpanelConfigView */ ({ // Maps-module-subpanel-config View (base) plugins: [ 'ReorderableColumns', ], /** * Event listeners */ events: { 'click [data-action=reset-default]': 'resetDefault', 'click [data-action=add-column]': 'addColumn', 'click [data-action=remove-column]': 'removeColumn', 'change .field_map_select': 'fieldsChanged', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(options); this._registerEvents(); }, /** * Property initialization * * @param {Object} options */ _initProperties: function(options) { if (options.widgetModule) { this.widgetModule = options.widgetModule; this._initDefaultData(); const widgetFields = app.metadata.getModule(this.widgetModule).fields; if (!_.isEmpty(widgetFields)) { this._moduleFields = app.utils.maps.arrayToObject( _.chain(widgetFields) .filter(function removeLinks(widgetField) { return widgetField.type !== 'link' && widgetField.dbType !== 'id'; }) .map(function buildFields(field) { let data = {}; data[field.name] = field.name; return data; }, this) .value() ); } } }, /** * Default Properties initialization * * @param {bool} force */ _initDefaultData: function(force) { const module = this.widgetModule; const _modulesData = this.context.safeRetrieveModulesData(module); const key = `${module}:maps-subpanel-list:visible-fields`; const savedSubpanelConfig = app.user.lastState.get(`${key}:admin:config`); if (savedSubpanelConfig) { _modulesData[module].subpanelConfig = savedSubpanelConfig; } if (_.isEmpty(_modulesData[module].subpanelConfig) || force) { _modulesData[module].subpanelConfig = []; } this.model.set('maps_modulesData', _modulesData); this._fields = { all: _modulesData[module].subpanelConfig }; if (force) { this.render(); } }, /** * Register context event handlers * */ _registerEvents: function() { this.listenTo(this, 'list:reorder:columns', this.subpanelColumnsOrderChanged, this); }, /** * @inheritdoc */ _render: function() { this._super('_render', arguments); this._disposeSelect2Elements(); this._buildSelect2s(); this._buildDraggable(); }, /** * Build select2 ui elements */ _buildSelect2s: function() { this.$('.field_map_select').select2({ closeOnSelect: true, containerCssClass: 'select2-choices-pills-close', width: '100%', }); this.$('.select2-choices').removeClass('maps-input-label').addClass('maps-input-label'); _.each(this._fields.all, function setDefaultValues(column) { this.$('[data-fieldname="' + column.name + '-select"]').select2('val', column.fieldName); }, this); }, /** * Build draggable ui elements */ _buildDraggable: function() { this.$('.ui-draggable').draggable({ cancel: null }); this.$('.ui-draggable input').click(function() { $(this).focus(); }); }, /** * Reorder fields array * * @param {Object} fields * @param {Array} newOrder */ subpanelColumnsOrderChanged: function(fields, newOrder) { // update position _.each(newOrder, function reorderColumns(columnId, position) { _.each(this._fields.all, function changeOrder(column) { if (column.name === columnId) { column.position = position; } }, this); }, this); //sort with the new positions this._fields.all.sort(function sortByPos(fColumn, sColumn) { return fColumn.position - sColumn.position; }); const module = this.widgetModule; const _modulesData = this.context.safeRetrieveModulesData(module); _modulesData[module].subpanelConfig = this._fields.all; this._resetModuleSubpanelOrder(module); this.model.set('maps_modulesData', _modulesData); this.model.trigger('change', this.model); }, /** * Resets subpanel columns order * * @param {string} module */ _resetModuleSubpanelOrder: function(module) { const key = `${module}:maps-subpanel-list:visible-fields`; app.user.lastState.remove(key); app.user.lastState.remove(`${key}:admin:config`); }, /** * Restore layout config data to default * * @param {jQuery} e */ resetDefault: function(e) { this._initDefaultData(true); }, /** * Adds a new column to the subpanel layout * * @param {jQuery} e */ addColumn: function(e) { const module = this.widgetModule; const _modulesData = this.context.safeRetrieveModulesData(module); _modulesData[module].subpanelConfig.push({ name: app.utils.generateUUID(), label: app.lang.getModString('LBL_ID', module), fieldName: 'id', position: _modulesData[module].subpanelConfig.length }); this.model.set('maps_modulesData', _modulesData); this._fields.all = _modulesData[module].subpanelConfig; this._resetModuleSubpanelOrder(module); this.render(); }, /** * Remove one of the subpanel column * * @param {jQuery} e */ removeColumn: function(e) { const module = this.widgetModule; const columnId = e.currentTarget.dataset.fieldname; const _modulesData = this.context.safeRetrieveModulesData(module); let columnIndex = -1; _.each(_modulesData[module].subpanelConfig, function changeLabel(column, index) { if (column.name === columnId) { columnIndex = index; } }, this); if (columnIndex > -1) { _modulesData[module].subpanelConfig.splice(columnIndex, 1); } this.model.set('maps_modulesData', _modulesData); this._fields.all = _modulesData[module].subpanelConfig; this._resetModuleSubpanelOrder(module); this.render(); }, /** * Manages column field changes * * @param {jQuery} e */ fieldsChanged: function(e) { const module = this.widgetModule; const columnId = e.currentTarget.dataset.type; const fieldName = e.val; const _modulesData = this.context.safeRetrieveModulesData(module); const fieldMeta = app.metadata.getModule(module).fields[fieldName]; const fieldLabelKey = fieldMeta.vname ? fieldMeta.vname : fieldMeta.label; let fieldLabel = app.lang.getModString(fieldLabelKey, module); fieldLabel = fieldLabel ? fieldLabel : app.lang.get(fieldLabelKey); _.each(_modulesData[module].subpanelConfig, function changeLabel(column) { if (column.name === columnId) { column.fieldName = fieldName; column.label = fieldLabel; } }, this); this.model.set('maps_modulesData', _modulesData); this._fields.all = _modulesData[module].subpanelConfig; this._resetModuleSubpanelOrder(module); this.render(); }, /** * Dispose a select2 element */ _disposeSelect2: function(name) { this.$('[data-fieldname=' + name + ']').select2('destroy'); }, /** * Dispose all select2 elements */ _disposeSelect2Elements: function() { _.each(this._fields.all, function setDefaultValues(column) { this._disposeSelect2(column.name); }, this); this._disposeSelect2('field_map_select'); }, /** * @inheritdoc */ _dispose: function() { this._disposeSelect2Elements(); this._super('_dispose'); }, }) }, "actionbutton-display-settings-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button configuration settings view * * @class View.Views.Base.AdministrationActionbuttonDisplaySettingsRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonDisplaySettingsRecordView * @extends View.View */ ({ // Actionbutton-display-settings-record View (base) events: { 'change [data-fieldname=buttonType]': 'buttonTypeChanged', 'change [data-fieldname=buttonSize]': 'buttonSizeChanged', 'change [data-fieldname=showFieldLabel]': 'showFieldLabel', 'change [data-fieldname=showInRecordHeader]': 'showInRecordHeader', 'change [data-fieldname=hideOnEdit]': 'hideOnEdit', 'change input.default-action': 'onChangeDefaultHandler', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._updateDisplaySettings(); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { this._settings = options.context.get('model').get('data').settings; if (Object.keys(this._settings).length === 0) { this._settings = { type: 'button', size: 'default', showFieldLabel: false, showInRecordHeader: false, hideOnEdit: false, displayOnFocusDashboard: false, }; } }, /** * Updates configuration and re-renders preview * */ _updateDisplaySettings: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.settings = this._settings; this.context.trigger('update-buttons-preview', buttonsData); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('.ab-admin-select').select2(); this._showHideButtonSizeController(); }, /** * Handle button type change event * * @param {UIEvent} e */ buttonTypeChanged: function(e) { this._settings.type = e.currentTarget.value; this._updateDisplaySettings(); this._showHideButtonSizeController(); }, /** * Handle button size change event * * @param {UIEvent} e */ buttonSizeChanged: function(e) { this._settings.size = e.currentTarget.value; this._updateDisplaySettings(); }, /** * Update field label visibility property * * @param {UIEvent} e */ showFieldLabel: function(e) { this._settings.showFieldLabel = e.currentTarget.checked; this._updateDisplaySettings(); }, /** * Update record header visibility property * * @param {UIEvent} e */ showInRecordHeader: function(e) { this._settings.showInRecordHeader = e.currentTarget.checked; this._updateDisplaySettings(); }, /** * Update record edit mode visibility property * * @param {UIEvent} e */ hideOnEdit: function(e) { this._settings.hideOnEdit = e.currentTarget.checked; this._updateDisplaySettings(); }, /** * Update field value by * * @param {UIEvent} e */ onChangeDefaultHandler: function(e) { const fieldName = e.currentTarget.getAttribute('data-fieldname'); this._settings[fieldName] = e.currentTarget.checked; }, /** * Toggle button size selector based on action button type * */ _showHideButtonSizeController: function() { if (this._settings.type === 'button') { this.$('[data-container=button-size]').show(); } else { this.$('[data-container=button-size]').hide(); } }, }) }, "actionbutton-preview-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Parent field view selection * * @class View.Views.Base.AdministrationActionbuttonPreviewRecordView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonPreviewRecordView * @extends View.View */ ({ // Actionbutton-preview-record View (base) /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options */ _beforeInit: function(options) { this._encodeData = options.context.get('model').get('encodeData'); }, /** * Property initialization * */ _initProperties: function() { this.context.on('update-buttons-preview', this._createPreview, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createPreview(this.context.get('model').get('data')); }, /** * Initialize and render an ActionButton based on current configuration * * @param {Object} buttonsData */ _createPreview: function(buttonsData) { const data = this._getPreparedButtonsData(buttonsData); var metadata = JSON.stringify(this._encodeData(data, true)); var previewContainer = this.$('.ab-admin-preview-container'); previewContainer.empty(); this._disposeButton(); this.buttonPreview = app.view.createField({ def: { type: 'actionbutton', name: 'PreviewButton', options: metadata }, view: this, viewName: 'detail', }); this.buttonPreview.render(); previewContainer.append(this.buttonPreview.$el); }, /** * Removes actions and dependencies from buttons * * @param {Object} buttonsData */ _getPreparedButtonsData: function(buttonsData) { var data = app.utils.deepCopy(buttonsData); // remove dependencies and actions data.buttons = _.each(data.buttons, function removeDep(buttonData, id) { buttonData.properties.isDependent = false; buttonData.actions = {}; }); // if there are no settings yet applied, set default ones if (Object.keys(data.settings).length < 1) { data.settings = { type: 'button', size: 'default' }; } return data; }, /** * Dispose the current button field * */ _disposeButton: function() { if (this.buttonPreview && this.buttonPreview.dispose) { this.buttonPreview.dispose(); this.buttonPreview = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeButton(); this._super('_dispose'); }, }) }, "drive-path-records": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDrivePathRecordsView * @alias SUGAR.App.view.views.BaseAdminstrationDrivePathRecordsView * @extends View.Views.Base.View */ ({ // Drive-path-records View (base) /** * drive types that do support variable paths */ variablePathDisabled: [], variablePathReadOnly: ['onedrive', 'sharepoint'], variablePathEnabled: ['google', 'dropbox', 'onedrive', 'sharepoint'], /** * field types to use in paths */ acceptedFieldTypes: [ 'varchar', 'text', 'datetime', 'relate', 'phone', 'url', ], /** * flag to disable interaction with the view */ canInteract: true, /** * list of modules which can't be used */ denyModules: [ 'Login', 'Home', 'WebLogicHooks', 'UpgradeWizard', 'Styleguide', 'Activities', 'Administration', 'Audit', 'Calendar', 'MergeRecords', 'Quotas', 'Teams', 'TeamNotices', 'TimePeriods', 'Schedulers', 'Campaigns', 'CampaignLog', 'CampaignTrackers', 'Documents', 'DocumentRevisions', 'Connectors', 'ReportMaker', 'DataSets', 'CustomQueries', 'WorkFlow', 'EAPM', 'Users', 'ACLRoles', 'InboundEmail', 'Releases', 'EmailMarketing', 'EmailTemplates', 'SNIP', 'SavedSearch', 'Trackers', 'TrackerPerfs', 'TrackerSessions', 'TrackerQueries', 'SugarFavorites', 'OAuthKeys', 'OAuthTokens', 'EmailAddresses', 'Sugar_Favorites', 'VisualPipeline', 'ConsoleConfiguration', 'SugarLive', 'iFrames', 'Sync', 'DataArchiver', 'MobileDevices', 'PushNotifications', 'PdfManager', 'Dashboards', 'Expressions', 'DataSet_Attribute', 'EmailParticipants', 'Library', 'Words', 'EmbeddedFiles', 'DataPrivacy', 'CustomFields', 'ArchiveRuns', 'KBDocuments', 'KBArticles', 'FAQ', 'Subscriptions', 'ForecastManagerWorksheets', 'ForecastWorksheets', 'pmse_Business_Rules', 'pmse_Project', 'pmse_Inbox', 'pmse_Emails_Templates', 'ProductBundleNotes', 'Comments', 'Feeds', 'HintAccountsets', 'HintNewsNotifications', 'HintEnrichFieldConfigs', 'HintNotificationTargets', 'CloudDrivePaths', 'Worksheet', 'Employees', 'Newsletters', 'Filters', 'Feedbacks', 'Tags', 'Categories', 'CommentLog', 'Holidays', 'OutboundEmail', 'Shippers' ], /** * initial record path */ recordPath: '', /** * @inheritdoc */ events: { 'click .addField': 'addField', 'change .moduleList': 'updateFieldList', 'click .selectPath': 'selectPath', 'click .savePath': 'savePath', 'click .removePath': 'removePath', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.driveType = this.context.get('driveType'); this.getModuleList(); this.loadPaths(); }, /** * initial load of saved paths */ loadPaths: function() { app.alert.dismissAll(); const url = app.api.buildURL('CloudDrivePaths', null, null, { max_num: -1, filter: [ { type: { $equals: this.driveType }, is_root: { $equals: 0 }, } ] }); app.alert.show('path-loading', { level: 'process' }); app.api.call('read', url, null, { success: _.bind(this._renderPaths, this), error: function(error) { app.alert.show('path-load-error', { level: 'error', messages: app.lang.get('LBL_DRIVE_LOAD_PATH_ERROR'), }); }, }); }, /** * Manipulate paths so we can render them * * @param {Array} data */ _renderPaths: function(data) { app.alert.dismiss('path-loading'); this.paths = _.isArray(data.records) && !_.isEmpty(data.records) ? data.records : []; this._getFilteredModuleList(); for (let path of this.paths) { try { let pathDisplay = _.map(JSON.parse(path.path), function(item) { return item.name; }).join('/'); path.pathDisplay = pathDisplay; } catch (e) { path.pathDisplay = path.path; } } /** * Make sure we have one empty path at the begining */ let rootName = app.lang.getAppString('LBL_MY_FILES'); if (this.driveType !== 'sharepoint') { this.paths.unshift({ path: '', pathDisplay: rootName, }); } else { this.paths.unshift({ path: '', pathDisplay: app.lang.get('LBL_DEFAULT_STARTING_PATH', this.module), }); } this.render(); this.canInteract = true; }, /** * set initial record path upon addition * * @param {string} module * @param {Event} evt * @param {string} path */ setRecordPath: function(module, evt, path) { let defaultRecordName = module === 'Contacts' || module === 'Leads' ? `${module}/$first_name $last_name` : `${module}/$name`; if (!module) { defaultRecordName = ''; } this.$(evt.target) .parent() .parent() .children('.span3') .children('.recordPath') .val(defaultRecordName); }, /** * @inheritdoc */ _render: function(options) { this._super('_render', arguments); this.initDropdowns(); }, /** * list of available modules */ getModuleList: function() { let modulesMeta = app.metadata.getModules({ filter: 'display_tab', access: true, }); this.modules = Object.keys(modulesMeta) .filter(key => !this.denyModules.includes(key)) .reduce((obj, key) => { obj[key] = modulesMeta[key]; return obj; }, {}); }, /** * dropdowns as select2 */ initDropdowns: function() { this.$('.moduleList').select2({ autoClear: true, containerCssClass: 'select2-choices-pills-close', placeholder: app.lang.get('LBL_SELECT_MODULE', this.module), }); this.$('.moduleList').trigger('change'); }, /** * Add a field variable to the record path * * @param {Event} evt */ addField: function(evt) { let fieldDropdown = this.$(evt.target) .closest('.span6') .parent() .children('.span6') .children('.fieldList'); let fieldName = fieldDropdown.select2('data').id; let recordPath = this.$(evt.target) .closest('.span6') .parent() .children('.span3') .children('.recordPath'); let currentRecordPath = recordPath.val(); let newPath = currentRecordPath.concat(fieldName); recordPath.val(newPath); }, /** * Whenever the module changes we need to make sure the field list changes * * @param {Event} evt */ updateFieldList: function(evt) { let _dropdown = this.$(evt.target) .parent() .parent() .children('.span6') .children('.fieldList'); let path = this.$(evt.target) .closest('.span3') .parent() .find('.recordPath') .val(); let _module = this.$(evt.target) .parent() .find('select.moduleList') .val(); let dropdownFields = []; if (_.isObject(this.modules[_module]) && _.has(this.modules[_module], 'fields')) { let fields = _.filter(this.modules[_module].fields, function(field) { return field.type !== 'link' && field.name && typeof field.name === 'string' && field.name.length > 0; }); _.each(fields, targetField => { if (_.isObject(targetField)) { let itemName = app.lang.get(targetField.vname, _module) || targetField.name; let itemId = `$${targetField.name}`; const duplicatedName = _.filter(fields, field => field.vname === targetField.vname); if (duplicatedName.length > 1) { itemName = `${itemName} (${targetField.name})`; } dropdownFields.push({ id: itemId, text: itemName, }); } }); } _dropdown.select2({ data: { results: dropdownFields } }); }, /** * Opens the remote selection drawer so we can select paths from drive * * @param {Event} evt */ selectPath: function(evt) { evt.preventDefault(); evt.stopPropagation(); const pathModule = this.$(evt.target) .parents('.row-fluid') .children('.span3') .children('select.moduleList').val(); const pathId = evt.target.dataset.id; if (_.isEmpty(pathModule)) { app.alert.show('module-required', { level: 'error', messages: app.lang.getModString('LBL_MODULE_REQUIRED', this.module), }); return; } // open the selection drawer app.drawer.open({ context: { pathModule: pathModule, isRoot: false, parentId: 'root', folderName: '', driveType: this.driveType, pathId: pathId }, layout: 'drive-path-select', }, _.bind(this.loadPaths, this)); }, /** * Save a path * * @param {Event} evt */ savePath: function(evt) { let variablePath = ''; const pathModule = this.$(evt.target) .parents('.row-fluid') .children('.span3') .children('select.moduleList').val(); const pathId = evt.target.dataset.id; // we cannot save a module path without module if (!pathModule) { app.alert.show('module-required', { level: 'error', messages: app.lang.getModString('LBL_MODULE_REQUIRED', this.module), }); return; } let path = this.$(evt.target) .parents('.row-fluid') .children('.span3') .children('.recordPath').val() || 'My files'; const url = app.api.buildURL('CloudDrive', 'path'); app.alert.show('path-saving-processing', { level: 'process' }); const pathRow = this.$(evt.target).parents('.row-fluid.path'); const isShared = pathRow.data('isshared'); let folderId = pathRow.data('folderid'); const currentPath = pathRow.data('currentpath'); const driveId = pathRow.data('driveid'); //reset folder id if paths do not match if (currentPath !== path && !this.variablePathReadOnly.includes(this.driveType)) { folderId = null; } else if (this.variablePathReadOnly.includes(this.driveType)) { // this is meant for onedrive and sharepoint where setting variable paths follow a more strict logic // do not change the path. It is readonly. path = currentPath; // we need to check if recordPathVariable is set const recordPathVariable = this.$(evt.target) .parents('.row-fluid') .children('.span6') .children('.recordPathVariable').val(); if (recordPathVariable) { variablePath = recordPathVariable; } } this.canInteract = false; // do not save empty paths if (_.isEmpty(path)) { app.alert.show('path-required', { level: 'error', messages: app.lang.getModString('LBL_PATH_REQUIRED', this.module), }); app.alert.dismiss('path-saving-processing'); return; } // make sure path is a string // path might be a json object if it is a variable path if (typeof path !== 'string') { try { path = JSON.stringify(path); } catch (e) { app.alert.show('path-error', { level: 'error', messages: app.lang.getModString('LBL_PATH_ERROR', this.module), }); app.alert.dismiss('path-saving-processing'); return; } } app.api.call('create', url, { pathModule: pathModule, isRoot: false, type: this.driveType, drivePath: path, isShared: isShared, folderId: folderId, driveId: driveId, pathId: pathId, variablePath: variablePath, modifySiteId: false, } , { success: _.bind(function() { app.alert.show('path-saved', { level: 'success', messages: app.lang.getModString('LBL_PATH_SAVED', this.module), }); this.loadPaths(); }, this), error: function(error) { app.alert.show('path-error', { level: 'error', messages: error.message, }); }, complete: function() { app.alert.dismiss('path-saving-processing'); } }); }, /** * Remove a path * * @param {Event} evt */ removePath: function(evt) { if (!this.canInteract) { app.alert.show('disallow-action', { level: 'warning', messages: app.lang.get('LBL_DISALLOW_ACTION', this.module), autoClose: true, }); return; } const pathId = evt.target.dataset.id; const url = app.api.buildURL('CloudDrive', 'path'); evt.currentTarget.classList.add('disabled'); app.api.call('delete', url, { pathId: pathId, }, { success: _.bind(function() { app.alert.show('path-deleted', { level: 'success', messages: app.lang.get('LBL_ROOT_PATH_REMOVED', this.module), }); this.loadPaths(); }, this), }); }, /** * Get the updated module list ( without the already selected modules ) */ _getFilteredModuleList: function() { let selectedModules = this.paths.map((cPath) => cPath.path_module).filter((module) => module !== undefined); this.filteredModuleList = {}; for (let module of selectedModules) { this.filteredModuleList[module] = this.modules; let moduleToExclude = selectedModules.filter((currentModule) => currentModule !== module); this.filteredModuleList[module] = _.omit(this.filteredModuleList[module],moduleToExclude); } this.moduleList = _.omit(this.modules,selectedModules); }, }) }, "maps-module-widget": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationMapsModuleWidgetView * @alias SUGAR.App.view.views.BaseAdministrationMapsModuleWidgetView */ ({ // Maps-module-widget View (base) /** * Event listeners */ events: { 'click [data-action=open-settings]': 'displaySetting', 'click [data-action=open-subpanel-config]': 'displaySubpanelConfig', 'click [data-action=open-mappings]': 'displayMappings', 'click [data-action=remove-module]': 'removeModule', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(options); }, /** * Property initialization * * @param {Object} options */ _initProperties: function(options) { if (options.widgetModule) { this.widgetModule = options.widgetModule; } }, /** * Message parent to add a new settings view for selected module */ displaySetting: function() { const viewMeta = { viewName: 'maps-module-settings', widgetModule: this.widgetModule, }; this.triggerDisplayView(viewMeta); }, /** * Message parent to add a new subpanel-config view for selected module */ displaySubpanelConfig: function() { const viewMeta = { viewName: 'maps-module-subpanel-config', widgetModule: this.widgetModule, }; this.triggerDisplayView(viewMeta); }, /** * Message parent to add a new mappings view for selected module */ displayMappings: function() { const viewMeta = { viewName: 'maps-module-mappings', widgetModule: this.widgetModule, }; this.triggerDisplayView(viewMeta); }, /** * Remove selected module from geocoding */ removeModule: function() { let availableModules = this.model.get('maps_enabled_modules'); availableModules = _.reject(availableModules, function removeModule(module) { return module === this.widgetModule; }, this); this.model.set('maps_enabled_modules', availableModules); }, /** * Trigger an action to display the view * * @param {Array} viewMeta */ triggerDisplayView: function(viewMeta) { this.context.trigger('display:map:module:config', viewMeta); }, }) }, "timeline-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationTimelineConfigView * @alias SUGAR.App.view.views.BaseAdministrationTimelineConfigView * @extends View.Views.Base.View */ ({ // Timeline-config View (base) /** * The api path. * @property {string} */ apiPath: 'config/timeline', /** * The main setting name. * @property {string} */ configName: '', /** * Config module. * @property {string} */ configModule: '', /** * The css class used for the main element. * @property {string} */ className: 'admin-config-body', /** * Enabled modules * @property {Object} */ enabledModules: [], /** * Available modules * @property {Object} */ availableModules: [], /** * List of modules enabled by default * @property {Object} */ defaultModules: [ 'Meetings', 'Calls', 'Notes', 'Emails', 'Messages', 'Tasks', 'Audit', // Market Modules 'sf_webActivity', 'sf_Dialogs', 'sf_EventManagement', ], /** * Event listeners */ events: { 'change input[type=checkbox]': 'changeHandler', }, /**. * @inheritdoc */ render: function(options) { this._super('render', [options]); this._processLimitAlert(); }, /**. * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.configModule = this.context.get('target'); if (this.configModule) { this.configName = 'timeline_' + this.configModule; this.boundSaveHandler = _.bind(this.saveSettings, this); this.context.on('save:config', this.boundSaveHandler); this.getAvailableModules(); } }, /** * Set available modules from subpanel metadata. */ getAvailableModules: function() { let self = this; let configedModules = null; if (app.config.timeline && app.config.timeline[this.configModule]) { configedModules = app.config.timeline[this.configModule].enabledModules || []; configedModules = _.filter(configedModules, function(link) { // make sure the link is still valid return self._isValidLink(link); }); } let enabledModules = configedModules ? _.clone(configedModules) : []; let availableModules = []; let meta = app.metadata.getModule(this.configModule); let subpanels = meta && meta.layouts && meta.layouts.subpanels && meta.layouts.subpanels.meta && meta.layouts.subpanels.meta.components || []; _.each(subpanels, function(subpanel) { let link = subpanel.context && subpanel.context.link || ''; if (self._isValidLink(link)) { let label = app.lang.get(subpanel.label, self.configModule); let relatedModule = app.data.getRelatedModule(self.configModule, link); if (!app.acl.hasAccess('view', relatedModule)) { return; } availableModules.push({link: link, label: label}); if (_.isNull(configedModules) && _.contains(self.defaultModules, relatedModule)) { enabledModules.push(link); } } }); this.enabledModules = enabledModules; this.availableModules = _.sortBy(availableModules, (module) => module.label.toLowerCase()); }, /** * Check if a link is valid. * @param {string} link * @return {boolean} * @private */ _isValidLink: function(link) { if (!link) { return false; } const hiddenSubpanels = app.metadata.getHiddenSubpanels(); const relatedModule = app.data.getRelatedModule(this.configModule, link); return relatedModule && !_.contains(hiddenSubpanels, relatedModule.toLowerCase()); }, /** * Save the settings. */ saveSettings: function() { let options = { error: _.bind(this.saveErrorHandler, this), success: _.bind(this.saveSuccessHandler, this) }; let settings = {}; settings[this.configName] = {enabledModules: this.enabledModules}; let url = app.api.buildURL(this.module, this.apiPath); app.api.call('create', url, settings, options); }, /** * Enable/disable a module. * @param {UIEvent} e */ changeHandler: function(e) { let link = $(e.currentTarget).data('link'); let enabled = e.currentTarget.checked; if (_.contains(this.enabledModules, link) && !enabled) { this.enabledModules = _.without(this.enabledModules, link); } else if (!_.contains(this.enabledModules, link) && enabled) { this.enabledModules.push(link); } this._processLimitAlert(); }, /** * On a successful save, a message will be shown indicating that the settings have been saved. * * @param {Object} settings */ saveSuccessHandler: function(settings) { app.config.timeline = app.config.timeline || {}; app.config.timeline[this.configModule] = app.config.timeline[this.configModule] || {}; app.config.timeline[this.configModule].enabledModules = this.enabledModules; this.closeView(); app.alert.show(this.settingPrefix + '-info', { autoClose: true, level: 'success', messages: app.lang.get('LBL_ACTIVITY_TIMELINE_SETTINGS_SAVED', this.module) }); }, /** * Show an error message if the settings could not be saved. */ saveErrorHandler: function() { app.alert.show(this.settingPrefix + '-warning', { level: 'error', title: app.lang.get('LBL_ERROR') }); }, /** * On a successful save return to the Administration page. */ closeView: function() { // Config changed... reload metadata app.sync(); if (app.drawer && app.drawer.count()) { app.drawer.close(this.context, this.context.get('model')); } else { app.router.navigate(this.module, {trigger: true}); } }, /** * @inheritdoc */ _dispose: function() { if (this.context) { this.context.off('save:config', this.boundSaveHandler); } this._super('_dispose'); }, /** * Dissallow to enable more modules then allowed. * * @private */ _processLimitAlert: function() { const state = this.enabledModules.length >= 10; this.$('.timeline-alert').toggleClass('hidden', !state); this.$('input:checkbox:not(:checked)').attr('disabled', state); }, }) }, "package-builder-remote-packages-subtab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderRemotePackagesSubtabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderRemotePackagesSubtabView * @extends View.View */ ({ // Package-builder-remote-packages-subtab View (base) /** * Progress alert view */ progressAlertView: false, /** * Process steps */ processSteps: false, /** * Process completed steps */ processCompletedSteps: false, /** * Show disabled row/button or not */ showDisabled: true, /** * Connection info for remote instance */ connectionInfo: {}, /** * Packages on local instance */ localInstancePackages: {}, /** * Entries to display in the table */ entries: [], /** * Headers to display in the table */ headers: [ 'Name', 'Version', 'Description', 'Actions' ], /** * Header labels */ headerLabels: [ 'LBL_PACKAGE_BUILDER_NAME', 'LBL_PACKAGE_BUILDER_VERSION', 'LBL_PACKAGE_BUILDER_DESCRIPTION', 'LBL_PACKAGE_BUILDER_ACTIONS' ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.connectionInfo = options.connectionInfo; this.entries = this.context.get('otherInstancePackages') || []; this.localInstancePackages = this.context.get('installedPackages') || {}; }, /** * @inheritdoc */ render: function() { this._super('render'); this.setActionButtons(); this.initEvents(); }, /** * Add event listeners */ initEvents: function() { let self = this; // click event for each Pull Package button this.$el.find('.pullButton').click(this.pullPackage.bind(self)); // click event for each Update Package button this.$el.find('.updateButton').click(this.updatePackagePressed.bind(self)); // showDisabled button/icon this.$el.find('#showDisabled').click(this.disabledVisibilityChanged.bind(self)); }, /** * Event handler for the showDisabled button */ disabledVisibilityChanged: function() { // Update showDisbaled flag this.showDisabled = this.showDisabled === false; this.render(); // Recreate the view with the new stats }, /** * After all the remote packages are displayed in the table, * this function will edit each package action, based * on the local instance packages */ setActionButtons: function() { let packageRows = this.$el.find('.regularRow'); _.each(packageRows, function(row, key) { // Get entry based on key index let entry = this.entries[key]; let packageName = entry.name; let localInstancePackage = _.find(this.localInstancePackages, function(localPackage) { return (localPackage.name === packageName && localPackage.status === 'installed'); }); // If package is not installed on local instance, do nothing if (_.isEmpty(localInstancePackage)) { return; } // If we find a matching package, save it on entry this.entries[key].localInstanceVersion = localInstancePackage; let rowId = '#' + row.id; // If this package exisits on local instance with the same version if (entry.version === localInstancePackage.version) { // Disable pull button from this row this.disablePullButton(rowId); } else { // If package exisits on local instance with a diffrent version this.$el.find(rowId + ' .actionsSpan').addClass('hide');// Hide pull button this.$el.find(rowId + ' .updateButton').removeClass('hide'); // Show update button } }.bind(this)); }, /** * Disable pull button from this row * * @param {string} rowId */ disablePullButton: function(rowId) { // Disable pull button from this row this.$el.find(rowId + ' .pullButton').addClass('disabled'); this.$el.find(rowId + ' .actionsSpan').removeClass('hide'); this.$el.find(rowId + ' .actionsSpan').attr( 'data-bs-original-title', app.lang.get('LBL_PACKAGE_BUILDER_PULL_PACKAGE_TOOLTIP_DISABLED', 'Administration') // tooltip message ); // If user chosee to hide disbaled rows, hide the row as well if (this.showDisabled === false) { this.$el.find(rowId + ' .pullButton')[0] .parentElement.parentElement.parentElement.parentElement.classList.add('hide'); } }, /** * Pull package from the remote instance * * @param {Object} $el */ pullPackage: function($el) { // Get selected entry (button id stores the index of the entry) let rowIndex = $el.currentTarget.id; let selectedEntry = this.entries[rowIndex]; selectedEntry.rowIndex = rowIndex; // Show confirmation alert App.alert.show('confirm-pull-package', { level: 'confirmation', messages: app.lang.get('LBL_PACKAGE_BUILDER_CONFIRM_PULL_PACKAGE', 'Administration'), autoClose: false, onConfirm: () => this.uploadAndInstallPackage(selectedEntry), }); }, /** * Upload and install the package * * @param {Object} package */ uploadAndInstallPackage: function(packageInfo) { let url = app.api.buildURL('Administration/package/remote'); let stagedVersions = this.getStagedVersions(packageInfo.name); let data = { 'connectionInfo': this.connectionInfo, 'id': packageInfo.id, 'stagedVersions': stagedVersions, }; let uploadCallBack = { success: function(uploadResponse) { if (uploadResponse.access === false) { // Access denied this.closeProgressAlert( 'error', app.lang.get( 'LBL_PACKAGE_BUILDER_ACCESS_DENIED_WRONG_CONNECTION', 'Administration' ) ); return; } // Check if we got the file installation id if (_.isEmpty(uploadResponse.fileInstallId)) { // Error we didn't get the id of the uploaded package this.closeProgressAlert( 'error', app.lang.get( 'LBL_PACKAGE_BUILDER_WRONG_UPLOAD', 'Administration' ) ); return; } this.processCompletedSteps++; let calculatedProgress = this.getPercentage(this.processCompletedSteps, this.processSteps); this.progressAlertView.logMessage(app.lang.get('LBL_PACKAGE_BUILDER_UPLOAD_SUCCESS', 'Administration')); this.progressAlertView.setProgress(calculatedProgress); let rowId = '#row-' + packageInfo.rowIndex; this.disablePullButton(rowId); // Disable pull button // Remove staged packages from the localInstancePackages list _.each(stagedVersions, function(stagedPackageId) { delete this.localInstancePackages[stagedPackageId]; }.bind(this)); // Add the new package to localInstancePackages packageInfo.status = 'staged'; this.localInstancePackages[uploadResponse.fileInstallId] = packageInfo; // Init installation package this.installPackage(uploadResponse.fileInstallId, packageInfo); }.bind(this), error: function(errorData) { this.closeProgressAlert('error', errorData); }.bind(this), }; // If this function is called after the uninstallation, process alert is already created if (_.isEmpty(this.progressAlertView)) { // There will be 2 stepts, updating and installing package this.processSteps = 2; this.processCompletedSteps = 0; // If the alert view is undefined, create it this.showPackagesProgressAlert({ 'initialTitle': app.lang.get('LBL_PACKAGE_BUILDER_PULLING_PACKAGE', 'Administration'), 'successTitle': app.lang.get('LBL_PACKAGE_BUILDER_PULL_SUCCESS', 'Administration'), 'errorTitle': app.lang.get('LBL_PACKAGE_BUILDER_PULL_FAILED', 'Administration'), }); } let uploadingMessage = app.lang.get('LBL_PACKAGE_BUILDER_UPLOADING', 'Administration') + ' ' + packageInfo.name + ' ' + packageInfo.version + ' ...'; this.progressAlertView.logMessage(uploadingMessage); app.api.call('create', url, data, uploadCallBack, {skipMetadataHash: true}); }, /** * Install the package * * @param {string} fileInstallId * @param {Object} packageInfo */ installPackage: function(fileInstallId, packageInfo) { let url = app.api.buildURL('Administration/packages/' + fileInstallId + '/install'); let installCallBack = { success: function(installResponse) { if (_.isUndefined(installResponse.id)) { this.closeProgressAlert( 'error', app.lang.get( 'LBL_PACKAGE_BUILDER_INSTALL_FAILED', 'Administration' ) ); return; } // Update the localInstancePackages, to display the correct actions for the package this.localInstancePackages[installResponse.id] = installResponse; // End progress alert this.closeProgressAlert( 'success', app.lang.get( 'LBL_PACKAGE_BUILDER_PACKAGE_INSTALLED', 'Administration' ) ); this.updateContextPackages(); }.bind(this), error: function(errorData) { this.closeProgressAlert('error', errorData); }.bind(this), }; let installMessage = app.lang.get('Installing', 'Addministration') + ' ' + packageInfo.name + ' ' + packageInfo.version + ' ...'; this.progressAlertView.logMessage(installMessage); app.api.call('read', url, null, installCallBack, {skipMetadataHash: true}); }, /** * Get all the staged versions of a package * @param {string} localPackageName * @return {Array} */ getStagedVersions: function(localPackageName) { // in other instance, get all the packages that have this name let stagedPackages = []; _.each(this.localInstancePackages, function(installedPackage) { if (installedPackage.status === 'staged' && installedPackage.name === localPackageName) { stagedPackages.push(installedPackage.id); } }); return stagedPackages; }, /** * Update package pressed * * @param {Object} $el */ updatePackagePressed: function($el) { let rowIndex = $el.currentTarget.id; let remotePackage = this.entries[rowIndex]; // Get entry by index (index is stored in id) let localInstancePackage = remotePackage.localInstanceVersion; remotePackage.rowIndex = rowIndex; // Save row index, this will be used to update the action buttons // Show confirmation alert App.alert.show('confirm-update-package', { level: 'confirmation', messages: app.lang.get('LBL_PACKAGE_BUILDER_CONFIRM_PULL_PACKAGE', 'Administration'), autoClose: false, onConfirm: function() { // There will be 3 steps, uninstall old package, update and install new package this.processSteps = 3; this.processCompletedSteps = 0; // Create the progress alert first this.showPackagesProgressAlert({ 'initialTitle': app.lang.get('LBL_PACKAGE_BUILDER_UPDATING_PACKAGE', 'Administration'), 'successTitle': app.lang.get('LBL_PACKAGE_BUILDER_UPDATE_SUCCESS', 'Administration'), 'errorTitle': app.lang.get('LBL_PACKAGE_BUILDER_UPDATE_FAILED', 'Administration'), }); // Call the updating logic this.updatePackage(remotePackage, localInstancePackage); }.bind(this), }); }, /** * Update package * * @param {Object} remotePackage * @param {Object} localInstancePackage */ updatePackage: function(remotePackage, localInstancePackage) { let url = app.api.buildURL('Administration/packages/' + localInstancePackage.id + '/uninstall'); let uninstallCallBack = { success: function(uninstallResponse) { if (_.isUndefined(uninstallResponse.id)) { this.closeProgressAlert( 'error', app.lang.get( 'LBL_PACKAGE_BUILDER_WRONG_UNINSTALL', 'Administration' ) ); return; } this.processCompletedSteps++; let calculatedProgress = this.getPercentage(this.processCompletedSteps, this.processSteps); this.progressAlertView.logMessage(app.lang.get('LBL_PACKAGE_BUILDER_UNINSTALL_SUCCESS', 'Administration')); this.progressAlertView.setProgress(calculatedProgress); // Remove uninstalled package from the localInstancePackages list delete this.localInstancePackages[remotePackage.localInstanceVersion.id]; this.localInstancePackages[uninstallResponse.id] = uninstallResponse; // Hide update button, show pull button let rowId = '#row-' + remotePackage.rowIndex; this.$el.find(rowId + ' .updateButton').hide(); // Hide update button this.$el.find(rowId + ' .pullButton').removeClass('disabled'); // Show pull button this.$el.find(rowId + ' .actionsSpan').removeClass('hide'); this.$el.find(rowId + ' .actionsSpan').attr( 'data-bs-original-title', app.lang.get('LBL_PACKAGE_BUILDER_PULL_PACKAGE_TOOLTIP', 'Administration') ); // package uninstalled sucessfully on the other instance begin installing selected package this.uploadAndInstallPackage(remotePackage); }.bind(this), error: function(errorData) { this.closeProgressAlert('error', errorData); }.bind(this), }; let uninstallingMessage = app.lang.get('LBL_PACKAGE_BUILDER_UNINSTALLING', 'Administration') + ' ' + localInstancePackage.name + ' ' + localInstancePackage.version + ' ...'; this.progressAlertView.logMessage(uninstallingMessage); app.api.call('read', url, null, uninstallCallBack); }, /** * Load the progress-alert view, custom view to display the fetching progress * @param {Object} */ showPackagesProgressAlert: function(options) { let alertContainer = this.$el.find('.progress-alert-container'); let progressAlertView = app.view.createView({ name: 'package-builder-progress-alert', initialTitle: options.initialTitle, successTitle: options.successTitle, errorTitle: options.errorTitle, }); progressAlertView.render(); alertContainer.empty(); alertContainer.append(progressAlertView.$el); // Keep alert view on this this.progressAlertView = progressAlertView; }, /** * Get the percentage of the process * * @param {Integer} first * @param {Integer} second * @return {string} */ getPercentage: function(first, second) { // eslint-disable-next-line no-magic-numbers return Math.round(((first / second) * 100)) + '%'; }, /** * Close the progress alert * * @param {string} status * @param {string} message */ closeProgressAlert: function(status, message) { if (status === 'success') { this.progressAlertView.processSuccessful(message); } else if (status === 'error') { this.progressAlertView.processFailed(message); } this.progressAlertView = false; }, /** * Set packages in context */ updateContextPackages: function() { this.context.set('installedPackages', this.localInstancePackages); this.context.set('otherInstancePackages', this.entries); } }) }, "actionbutton-update-field": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Update Field action configuration view * * @class View.Views.Base.AdministrationActionbuttonUpdateFieldView * @alias SUGAR.App.view.views.BaseAdministrationActionbuttonUpdateFieldView * @extends View.View */ ({ // Actionbutton-update-field View (base) events: { 'change input[type="checkbox"][data-fieldname="calculated"]': 'calculatedChanged', 'click [data-action="remove-field"]': 'removeField', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Quick initialization of field properties * * @param {Object} options * */ _beforeInit: function(options) { options.fieldModule = options.fieldModule || 'Accounts'; options.fieldName = options.fieldName || 'name'; this._properties = { _isCalculated: options.isCalculated, _fieldName: options.fieldName, _value: options.value, _formula: options.formula, }; if (this._properties._value === '') { this._properties._value = {}; } this._fieldDef = app.metadata.getModule(options.fieldModule).fields[options.fieldName]; this._fieldLabel = app.lang.get(this._fieldDef.vname, options.fieldModule); this._callback = options.callback; this._deleteCallback = options.deleteCallback; this._module = options.fieldModule; }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); // setup all the css we could not add via hbs this.$el.addClass('span6 ab-update-field-wrapper ' + this._properties._fieldName); this._createController(); }, /** * Remove field handler * * @param {UIEvent} e * */ removeField: function(e) { if (this._deleteCallback) { this._deleteCallback(this._properties._fieldName); } }, /** * Some basic validation of properties * * @return {bool} */ canSave: function() { if (this._properties._isCalculated) { return this._controller.isValid(); } return true; }, /** * Handler for calculated URL checkbox selection * * @param {UIEvent} e * */ calculatedChanged: function(e) { this._properties._isCalculated = e.currentTarget.checked; this._createController(); if (this._callback) { this._callback(this._properties); } }, /** * Event handler for whenever the formula changes. * * @param {UIEvent} e * */ formulaChanged: function(data) { this._properties._formula = data; if (this._callback) { this._callback(this._properties); } }, /** * Event handler for field value change * * @param {Object} data * */ valueChanged: function(data) { _.each(data.changed, function storeData(fieldValue, fieldName) { this._properties._value[fieldName] = fieldValue; }, this); if (this._callback) { this._callback(this._properties); } }, /** * Create field value sidecar component * */ _createController: function() { var fieldContainer = this.$('div[data-container="field"]'); fieldContainer.empty(); if (this._properties._isCalculated) { // we simply create the formula builder field this._controller = app.view.createField({ def: { type: 'formula-builder', name: 'ABCustomAction' }, view: this, viewName: 'edit', targetModule: this._module, callback: _.bind(this.formulaChanged, this), formula: this._properties._formula, matchField: this._properties._fieldName }); } else { // get the field meta var moduleMeta = app.metadata.getModule(this._module); if (_.isEmpty(moduleMeta) || _.isEmpty(moduleMeta.fields)) { return; } var fieldsMeta = moduleMeta.fields; var fieldMeta = fieldsMeta[this._properties._fieldName]; var fieldDef = app.utils.deepCopy(fieldMeta); var controllerModel = app.data.createBean(this._module); // populate the model with all the data needed controllerModel.set(this._properties._value); controllerModel.on('change', _.bind(this.valueChanged, this)); // if we have a link type field we have to set it's type to relate if (fieldDef.type === 'link') { if (!fieldDef.module) { _.each(fieldsMeta, function getValidDef(def) { if (def.link === fieldDef.name) { fieldDef = def; } }); } if (fieldDef.module) { fieldDef.type = 'relate'; } } else if (fieldDef.type === 'multienum') { fieldDef.type = 'enum'; } else if (fieldDef.type === 'text') { fieldDef.type = 'textarea'; } else if (fieldDef.type === 'html') { fieldDef.type = 'htmleditable_tinymce'; } fieldDef.required = false; // simply create a controller matching the field metadata this._controller = app.view.createField({ def: fieldDef, view: this, layout: this.layout, viewName: 'edit', model: controllerModel }); } if (this.layout && this.layout.layout) { this.layout = this.layout.layout; } this._controller.render(); fieldContainer.append(this._controller.$el); }, /** * Remove the current field value sidecar component * Can be either a FormulaBuilder field for calculated values, * or any sugar field type field for a static value */ _disposeField: function() { if (this._controller) { // we have to force the _hasDatePicker to false in order to avoid // an issue in the date/timecombo dispose method // _dispose: function() { // // FIXME: new date picker versions have support for plugin removal/destroy // // we should do the upgrade in order to prevent memory leaks // if (this._hasDatePicker) { // $(window).off('resize', this.$(this.fieldTag).data('datepicker').place); // } // this._super('_dispose'); // } // As it happens, it goes off to error when trying to access the place property, because // the .data('datepicker') call returns undefined this._controller._hasDatePicker = false; this._controller.dispose(); this._controller = null; } }, /** * @inheritdoc * */ _dispose: function() { this._disposeField(); this._super('_dispose'); }, }) }, "package-builder-connection-subtab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationPackageBuilderConnnectionSubtabView * @alias SUGAR.App.view.views.BaseAdministrationPackageBuilderConnnectionSubtabView * @extends View.View */ ({ // Package-builder-connection-subtab View (base) /** * Connection info for remote instance. */ connectionInfo: {}, /** * @inheritdoc */ render: function() { this._super('render'); this.initEvents(); }, /** * Add event listeners. */ initEvents: function() { if (_.isUndefined(this.context._events) || _.isEmpty(this.context._events['button:test_connection_button:click'])) { this.listenTo(this.context, 'button:test_connection_button:click', this.testConnection.bind(this)); } }, /** * Test connection to remote instance. */ testConnection: function() { const data = { connectionInfo: this.getConnectionInfo(), }; if (data.connectionInfo === false) { app.alert.show('pb_empty_fields', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_TAB_PACKAGES_EMPTY_FIELD_ERROR', 'Administration'), autoClose: true, autoCloseDelay: 10000, }); return; } const urlObj = new URL(data.connectionInfo.url); if (urlObj.protocol !== 'https:') { app.alert.show('pb_connection_unsupported_protocol', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_INVALID_SCHEME', 'Administration'), autoClose: true, autoCloseDelay: 10000, }); return; } const url = app.api.buildURL('Administration/package/getRemotePackages'); const callback = { success: function(response) { // First check if we have access to the other instance (connection info are correct) if (response.access === false) { // Access denied app.alert.show('pb_connection_access_denied', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_ACCESS_DENIED', 'Administration'), autoClose: true, autoCloseDelay: 10000, }); return; } // Connection test successful app.alert.show('pb_test_successful', { level: 'success', messages: app.lang.get('LBL_PACKAGE_BUILDER_CONNECTION_SUCCESS', 'Administration'), autoClose: true }); this.enableRemotePackagesTab(); // Enable the Remote Packages tab this.connectionInfo = this.getConnectionInfo(); // Save the connection info on this. this.context.set('otherInstancePackages', _.values(response.otherInstancePackages || {})); }.bind(this), error: function(errorData) { app.alert.show('pb_test_connection_failed', { level: 'error', messages: app.lang.get('LBL_PACKAGE_BUILDER_CONNECTION_FAILED', 'Administration'), autoClose: true, autoCloseDelay: 10000, }); }.bind(this), }; // Check if the source info are correct app.api.call('create', url, data, callback); }, /** * Get connection info. * @return {(Object|boolean)} Connection info or false if any of the fields are empty. */ getConnectionInfo: function() { const url = this.model.get('instance_url'); const user = this.model.get('username'); const password = this.model.get('password'); if (_.isEmpty(url) || _.isEmpty(user) || _.isEmpty(password)) { return false; } return { 'url': url, 'user': user, 'pass': password, }; }, /** * Enable Remote Packages */ enableRemotePackagesTab: function() { const packagesTab = this.$el.parent().parent().find('.remote_packages_tab'); // Get tab element packagesTab.find('.remote_packagesDot').show(); // Show blue notification dot packagesTab.removeClass('disabled'); // Enable Tab packagesTab.addClass('start_animation'); }, }) }, "drive-config-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDriveConfigButtonsView * @alias SUGAR.App.view.views.BaseAdminstrationDriveConfigButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Drive-config-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); let driveType = this.context.get('driveType'); this.driveType = app.lang.getAppListStrings('drive_types')[driveType]; } }) }, "drive-path-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.AdministrationDrivePathButtonsView * @alias SUGAR.App.view.views.BaseAdminstrationDrivePathButtonsView * @extends View.Views.Base.View */ ({ // Drive-path-buttons View (base) /** * @inheritdoc */ events: { 'click [name=save_button]': 'saveCurrentPath', 'click [name=cancel_button]': 'closeDrawer', 'click [name=shared_button]': 'toggleCheckbox', 'change .sharedWithMe': 'toggleShared' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', arguments); this.driveType = this.context.get('driveType'); this.driveTypeLabel = app.lang.getAppListStrings('drive_types')[this.driveType]; }, _render: function() { this._super('_render'); this._handleSharedWithMeButton(); }, /** * Save the current path * * @param {Event} evt */ saveCurrentPath: function(evt) { let folders = this.layout.getComponent('drive-path-select').currentPathFolders; const folderId = this.layout.getComponent('drive-path-select').currentFolderId; const driveId = this.layout.getComponent('drive-path-select').driveId; const siteId = this.layout.getComponent('drive-path-select').siteId; const url = app.api.buildURL('CloudDrive', 'path'); app.alert.show('path-processing', { level: 'process' }); app.api.call('create', url, { isRoot: this.context.get('isRoot'), pathModule: this.context.get('pathModule'), type: this.driveType, drivePath: JSON.stringify(folders), folderId: folderId, driveId: driveId, siteId: siteId, isShared: this.context.get('sharedWithMe'), pathId: this.context.get('pathId'), } , { success: function() { app.alert.dismiss('path-processing'); app.drawer.close(); }, error: function(error) { app.alert.show('cloud-error', { level: 'error', messages: error.message, }); }, }); }, /** * Close drawer * * @param {Event} evt */ closeDrawer: function(evt) { app.drawer.close(); }, /** * Toggle between shard and My files * * @param {Event} evt */ toggleShared: function(evt) { if (this.driveType === 'sharepoint') { return; } this.sharedWithMe = this.$('.sharedWithMe').prop('checked'); this.context.set('sharedWithMe', this.sharedWithMe); let pathView = this.layout.getComponent('drive-path-select'); pathView.loadFolders(null, this.sharedWithMe); }, /** * Checkbox event * * @param {Event} evt */ toggleCheckbox: function(evt) { const checkbox = this.$('.sharedWithMe'); checkbox.prop('checked', !this.sharedWithMe); this.toggleShared(); }, /** * Disable the Shared with me button for Sharepoint * */ _handleSharedWithMeButton: function() { if (this.driveType === 'sharepoint') { this.$('[name="shared_button"]').attr('disabled', true); this.$('[name="shared_button"]').addClass('disabled'); } }, }) }, "actionbutton-create-record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Create Record action configuration view * * @class View.Views.Base.ActionbuttonCreateRecordView * @alias SUGAR.App.view.views.BaseActionbuttonCreateRecordView * @extends View.View */ ({ // Actionbutton-create-record View (base) /** * Fields which should not be available to automatically update */ badFields: [ 'deleted', 'team_count', 'account_description', 'opportunity_role_id', 'opportunity_role_fields', 'opportunity_role', 'email_and_name1', 'dnb_principal_id', 'email1', 'email2', 'email_addresses', 'email_addresses_non_primary', 'email_addresses_primary', 'email_and_name1', 'primary_address_street_2', 'primary_address_street_3', 'alt_address_street_2', 'alt_address_street_3', 'portal_app', 'portal_user_company_name', 'mkto_sync', 'mkto_id', 'mkto_lead_score', 'cookie_consent', 'cookie_consent_received_on', 'dp_consent_last_updated', 'accept_status_id', 'sync_key', 'locked_fields', 'billing_address_street_2', 'billing_address_street_3', 'billing_address_street_4', 'shipping_address_street_2', 'shipping_address_street_3', 'shipping_address_street_4', 'related_languages', 'dri_workflow_template_name', 'perform_sugar_action', ], /** * Field types which should not be available to automatically update */ badFieldTypes: [ 'link', 'id', 'collection', 'widget', 'html', 'htmleditable_tinymce', 'image', 'teamset', 'team_list', 'email', 'password', 'file' ], /** * Event listeners */ events: { 'change [data-fieldname=module]': 'moduleChanged', 'change [data-fieldname=link]': 'linkChanged', 'change input[type=checkbox][data-fieldname]': 'boolPropChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { var ctxModel = options.context.get('model'); this._buttonId = options.buttonId; this._actionId = options.actionId; this._modules = {}; _.each(ctxModel.get('modules'), function buildModulesList(moduleName) { this._modules[moduleName] = app.lang.getModuleName(moduleName, { plural: true }); }, this); this._module = ctxModel.get('module'); if (options.actionData && options.actionData.properties && Object.keys(options.actionData.properties).length !== 0) { this._properties = options.actionData.properties; } else { this._properties = { attributes: {}, parentAttributes: {}, module: 'Accounts', link: '', mustLinkRecord: false, copyFromParent: false, autoCreate: false }; }; this._properties.parentAttributes = _.omit(this._properties.parentAttributes, function filterEmptyParentAttributes(a) { return _.isEmpty(a.fieldName) && _.isEmpty(a.parentFieldName); }); this._properties.mustLinkRecord = app.utils.isTruthy(this._properties.mustLinkRecord); this._properties.copyFromParent = app.utils.isTruthy(this._properties.copyFromParent); this._properties.autoCreate = app.utils.isTruthy(this._properties.autoCreate); this._populateRelationships(); this._populateAttributes(this._properties.module); this._populateParentAttributes(this._module); }, /** * Property initialization, nothing to do for this view * */ _initProperties: function() { }, /** * Context event registration, nothing to do for this view * */ _registerEvents: function() { }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._select2 = null; this.select2('module', '_queryModules'); this.select2('link', '_queryLinks'); this.select2('attributes', '_queryAttributes', true, _.bind(this.addAttribute, this)); this.select2('parent-attributes', '_queryAttributes', true, _.bind(this.addParentAttribute, this)); if (this._properties.module !== '') { this.select2('module').data({ id: this._properties.module, text: this._modules[this._properties.module] }); } if (this._properties.link !== '') { this.select2('link').data({ id: this._properties.link, text: this._properties.link }); } this.$('[data-fieldname=must-link-record]').attr('disabled', _.isEmpty(this._links)); this._createExistingControllers(); }, /** * View setup, nothing to do for this view * */ setup: function() { }, /** * Some basic validation of properties * */ canSave: function() { if (this._properties.module === '') { app.alert.show('alert_actionbutton_create_nomodule', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_INVALID_DATA'), messages: app.lang.get('LBL_ACTIONBUTTON_SELECT_MODULE'), autoClose: true, autoCloseDelay: 5000 }); return false; } if (this._properties.link === '' && this._properties.mustLinkRecord) { app.alert.show('alert_actionbutton_create_nolink', { level: 'error', title: app.lang.get('LBL_ACTIONBUTTON_INVALID_DATA'), messages: app.lang.get('LBL_ACTIONBUTTON_SELECT_LINK'), autoClose: true, autoCloseDelay: 5000 }); return false; } return true; }, /** * Return action configuration * * @return {Object} */ getProperties: function() { return this._properties; }, /** * Add parent attribute * * @param {Object} data * */ addParentAttribute: function(data) { this._addParentField(data.id); }, /** * Add attribute * * @param {Object} data * */ addAttribute: function(data) { this._addAttributesField(data.id); }, /** * Model link field change event handler * * @param {UIEvent} e * */ linkChanged: function(e) { var fieldname = $(e.currentTarget).data('fieldname'); fieldname = this.kebabToCamelCase(fieldname); this._properties[fieldname] = e.currentTarget.value; this._updateActionProperties(); }, /** * Converts kebab-case to camelCase * * @param {string} str * @return {string} */ kebabToCamelCase: function(str) { str = str.split('-') .map(function(a, i) { return i === 0 ? a : app.utils.capitalize(a); }).join(''); return str; }, /** * Event handler for checkboxes * * @param {UIEvent} e * */ boolPropChanged: function(e) { var fieldname = $(e.currentTarget).data('fieldname'); fieldname = this.kebabToCamelCase(fieldname); this._properties[fieldname] = e.currentTarget.checked; this._updateActionProperties(); if (fieldname === 'mustLinkRecord') { this.render(); } }, /** * Event handler for module selection change * * @param {UIEvent} e * */ moduleChanged: function(e) { var fieldname = $(e.currentTarget).data('fieldname'); fieldname = this.kebabToCamelCase(fieldname); if (this._properties[fieldname] !== e.currentTarget.value) { this._properties = { attributes: {}, parentAttributes: {}, module: 'Accounts', link: '', mustLinkRecord: false, copyFromParent: false, autoCreate: false }; } this._properties[fieldname] = e.currentTarget.value; this._updateActionProperties(); this._populateRelationships(); this._populateAttributes(this._properties.module); this._populateParentAttributes(this._module); this.render(); }, /** * Updates the currently selected module fields array with anything that can be updated * * @param {string} module */ _populateAttributes: function(module) { var fields = _.chain(app.metadata.getModule(module).fields).values(); fields = fields.filter( function filterField(field) { return ( !_.isEmpty(field.name) && !_.isEmpty(field.vname) && !_.contains(this.badFields, field.name) && !_.contains(this.badFieldTypes, field.type) && field.link_type !== 'relationship_info' && field.readonly !== true && field.calculated !== true ); }, this ).map(function fieldToTuple(field) { return [field.name, app.lang.get(field.vname, module)]; }).value(); this._attributes = _.object(fields); }, /** * Update the parent module fields array that can be copied over * * @param {string} module */ _populateParentAttributes: function(module) { var fields = _.chain(app.metadata.getModule(module).fields).values(); fields = fields.filter( function filterField(field) { return ( !_.isEmpty(field.name) && !_.isEmpty(field.vname) && this.badFields.indexOf(field.name) === -1 && this.badFieldTypes.indexOf(field.type) === -1 && field.link_type !== 'relationship_info' && field.studio !== false ); }, this ).map(function fieldToTuple(field) { return [field.name, app.lang.get(field.vname, module)]; }).value(); this._parentAttributes = _.object(fields); }, /** * Update action configuration * * @param {Object} data * */ updateProperties: function(data) { this._properties.attributes[data._fieldName] = { fieldName: data._fieldName, isCalculated: data._isCalculated, formula: data._formula, value: data._value }; this._updateActionProperties(); }, /** * Update parent field updates configuration * * @param {Object} data * */ updateParentProperties: function(data) { this._properties.parentAttributes[data._fieldName] = { fieldName: data._fieldName, parentFieldName: data._parentFieldName, }; this._updateActionProperties(); }, /** * Remove a selected field from the parent/record update * * @param {string} fieldId * */ removeParentField: function(fieldId) { this.disposeField(fieldId); delete this._properties.parentAttributes[fieldId]; this._updateActionProperties(); }, /** * Remove a selected field from the record update * * @param {string} fieldId * */ removeAttributesField: function(fieldId) { this.disposeField(fieldId); delete this._properties.attributes[fieldId]; this._updateActionProperties(); }, /** * Create subviews based on action configuration * */ _createExistingControllers: function() { // create all the controllers that were previously saved _.each(this._properties.attributes, function create(data, name) { this._createAttributesFieldController(data); }, this); _.each(this._properties.parentAttributes, function create(data, name) { this._createParentFieldController(data); }, this); }, /** * Add a new parent field update configuration * * @param {string} fieldName * */ _addParentField: function(fieldName) { this._properties.parentAttributes[fieldName] = { fieldName: fieldName, parentFieldName: '', }; this._updateActionProperties(); this._createParentFieldController(this._properties.parentAttributes[fieldName]); }, /** * Add a new field update configuration * * @param {string} fieldName * */ _addAttributesField: function(fieldName) { this._properties.attributes[fieldName] = { fieldName: fieldName, isCalculated: false, formula: '', value: '' }; this._updateActionProperties(); this._createAttributesFieldController(this._properties.attributes[fieldName]); }, /** * Creates the parent field value selection view * * @param {Object} fieldData * */ _createParentFieldController: function(fieldData) { this.disposeField(fieldData.fieldName); var fieldController = app.view.createView({ name: 'actionbutton-parent-field', context: this.context, model: this.context.get('model'), layout: this, fieldName: fieldData.fieldName, parentFieldName: fieldData.parentFieldName, fieldModule: this._properties.module, deleteCallback: _.bind(this.removeParentField, this), callback: _.bind(this.updateParentProperties, this) }); this.$('[data-container=preset-fields]').prepend(fieldController.$el); fieldController.render(); if (!this._subComponents) { this._subComponents = []; } this._subComponents.push(fieldController); }, /** * Creates the normal field value selection view * * @param {Object} fieldData * */ _createAttributesFieldController: function(fieldData) { this.disposeField(fieldData.fieldName); var fieldController = app.view.createView({ name: 'actionbutton-update-field', context: this.context, model: this.context.get('model'), layout: this, isCalculated: fieldData.isCalculated, fieldName: fieldData.fieldName, value: fieldData.value, formula: fieldData.formula, fieldModule: this._properties.module, deleteCallback: _.bind(this.removeAttributesField, this), callback: _.bind(this.updateProperties, this) }); this.$('[data-container=preset-fields]').prepend(fieldController.$el); fieldController.render(); if (!this._subComponents) { this._subComponents = []; } this._subComponents.push(fieldController); }, /** * Update action properties in context * */ _updateActionProperties: function() { var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonId].actions[this._actionId].properties = this._properties; // update action data into the main data container ctxModel.set('data', buttonsData); }, /** * Create generic Select2 component or return a cached select2 element * * @param {string} fieldname */ select2: function(fieldname, queryFunc, reset, callback) { if (this._select2 && this._select2[fieldname]) { return this._select2[fieldname]; }; var el = this.$('[data-fieldname=' + fieldname + ']') .select2(this._getSelect2Options(queryFunc)) .data('select2'); this._select2 = this._select2 || {}; this._select2[fieldname] = el; if (reset) { el.onSelect = (function select(fn) { return function returnCallback(data, options) { if (callback) { callback(data); } if (arguments) { arguments[0] = { id: 'select', text: app.lang.get('LBL_ACTIONBUTTON_SELECT_OPTION') }; } return fn.apply(this, arguments); }; })(el.onSelect); } return el; }, /** * Create generic Select2 options object * * @param {string} queryFunc * * @return {Object} */ _getSelect2Options: function(queryFunc) { var select2Options = {}; select2Options.placeholder = app.lang.get('LBL_ACTIONBUTTON_SELECT_OPTION'); select2Options.dropdownAutoWidth = true; if (queryFunc && this[queryFunc]) { select2Options.query = _.bind(this[queryFunc], this); } return select2Options; }, /** * Populate parent field select2 component * * @param {Object} query * * @return {Function} */ _queryParentAttributes: function(query) { return this._query(query, '_parentAttributes'); }, /** * Populate normal field select2 component * * @param {Object} query * * @return {Function} */ _queryAttributes: function(query) { return this._query(query, '_attributes'); }, /** * Populate the module list select2 component * * @param {Object} query * * @return {Function} */ _queryModules: function(query) { return this._query(query, '_modules'); }, /** * Populate module link fields list select2 component * * @param {Object} query * * @return {Function} */ _queryLinks: function(query) { return this._query(query, '_links'); }, /** * Generic select2 selection list builder * * @param {Object} query * @param {string} list * */ _query: function(query, list) { var listElements = this[list]; var data = { results: [], more: false }; if (_.isObject(listElements)) { _.each(listElements, function pushValidResults(element, index) { if (query.matcher(query.term, element)) { data.results.push({id: index, text: element}); } }); } else { listElements = null; } query.callback(data); }, /** * Populate links data with label values * */ _populateRelationships: function() { const targetModule = this._properties.module; const moduleName = app.lang.getModuleName(targetModule, { plural: true }); var currentModuleName = this._module; var currentModuleFields = app.metadata.getModule(currentModuleName).fields; var relationships = {}; _.each(currentModuleFields, _.bind(function getLinks(linkData) { const moduleLabel = app.lang.get(linkData.vname, currentModuleName); if (linkData.type === 'link' && ( moduleLabel === targetModule || moduleLabel === moduleName || linkData.module === targetModule)) { relationships[linkData.name] = moduleLabel + ' (' + linkData.name + ')'; } }, this)); this._links = relationships; }, /** * Dipose and remove a given field from the record update list * * @param {string} fieldId * */ disposeField: function(fieldId) { var field = _.find(this._subComponents, function checkField(controller, index) { if (controller && controller._properties) { return controller._properties._fieldName === fieldId; } return false; }); if (field) { this._subComponents = _.chain(this._subComponents).without(field).value(); field.dispose(); } }, /** * @inheritdoc */ _dispose: function() { _.each(this._subComponents, function disposeChild(component) { component.dispose(); }); this._subComponents = []; this._super('_dispose'); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "actionbutton-display-settings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Action button tab view * * @class View.Layouts.Base.AdministrationActionbuttonDisplaySettingsLayout * @alias SUGAR.App.view.layouts.BaseAdministrationActionbuttonDisplaySettingsLayout * @extends View.Layout */ ({ // Actionbutton-display-settings Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initial setup of properties */ _initProperties: function() { this._sideView = null; this._sidePreview = null; }, /** * Context model event registration */ _registerEvents: function() { var ctxModel = this.context.get('model'); this.listenTo(ctxModel, 'update:side-pane:view', this.changeView, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this._createSideView('record'); this._createSidePreview('record'); }, /** * When the tab is changed, we need to change the view * @param {string} viewID */ changeView: function(viewID) { this._createSidePreview(viewID); this._createSideView(viewID); }, /** * Create the side view * @param {string} viewID */ _createSideView: function(viewID) { this._disposeSideView(); var svContainer = this.$('[data-container="ab-admin-side-container"]'); svContainer.empty(); this._sideView = app.view.createView({ name: 'actionbutton-display-settings-' + viewID, context: this.context, model: this.context.get('model'), layout: this, }); this._sideView.render(); svContainer.append(this._sideView.$el); }, /** * Create the side view * @param {string} viewID */ _createSidePreview: function(viewID) { this._disposeSidePreview(); var svContainer = this.$('[data-container="ab-admin-side-preview"]'); svContainer.empty(); this._sidePreview = app.view.createView({ name: 'actionbutton-preview-' + viewID, context: this.context, model: this.context.get('model'), layout: this, }); this._sidePreview.render(); svContainer.append(this._sidePreview.$el); }, /** * Dispose side pane view */ _disposeSideView: function() { if (this._sideView) { this._sideView.dispose(); this._sideView = null; } }, /** * Dispose side pane preview */ _disposeSidePreview: function() { if (this._sidePreview) { this._sidePreview.dispose(); this._sidePreview = null; } }, /** * @inheritdoc */ _dispose: function() { this._disposeSideView(); this._disposeSidePreview(); this._super('_dispose'); }, }) }, "portal-preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.AdministrationPortalPreviewLayout * @alias SUGAR.App.view.layouts.BaseAdministrationPortalPreviewLayout * @extends View.Layout */ ({ // Portal-preview Layout (base) /** * Cache the preview components as they may be expensive to retrieve * * { * layout-name: { * view-name-1: component, * view-name-2: component, * } * } */ componentsCache: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.componentsCache = {}; }, /** * @inheritdoc */ initComponents: function(components, options, module) { // set config-layout on context so dashboard-fabs and record dashlets // can adjust their behavior accordingly this.context.set('config-layout', true); this._super('initComponents', [components, options, module]); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.on('portal:config:preview', this.handleConfigPreview, this); this.on('dashboard:restore_dashlets_button:click', this.restorePortalHomeDashlets, this); }, /** * Restore dashboard metadata (set dashlets to initial state) * * @param {Object} context context of the dashboard component which has portal-preview layout */ restorePortalHomeDashlets: function(context) { var model = context ? context.get('model') : {}; if (!_.isEmpty(model)) { var attributes = { id: model.get('id') }; var params = { dashboard_module: model.get('dashboard_module'), dashboard: 'portal-home' }; var url = app.api.buildURL('Dashboards', 'restore-metadata', attributes, params); app.api.call('update', url, null, { success: _.bind(function(response) { var dashboard = this.getComponent('dashboard'); dashboard.model.set(response); dashboard.model.setSyncedAttributes(response); }, this) }); } }, /** * Handles the event 'portal:config:preview' * * Triggers 'data:preview' on the preview component to let the component * handle it's own preview behavior * * Expects the data event argument to look like: * { * preview_components: [ * ... * { * layout: 'layout-name', * view: 'view-name', * fields: [...], * properties: [...], * preview_data: '...' * }, * ... * ] * } * * @param data */ handleConfigPreview: function(data) { _.each(data.preview_components, function(def) { var component = this.getPreviewComponent(def); if (!component) { return; } component.trigger('data:preview', { fields: def.fields && !_.isEmpty(def.fields) ? def.fields : [], properties: def.properties && !_.isEmpty(def.properties) ? def.properties : [], preview_data: data.preview_data }); }, this); }, /** * Get the specified preview component and cache it (if found) * * Requires def.layout and def.view to prevent expensive recursive * searching for a specific component * { * layout: 'layout-name', * view: 'view-name' * } * * @param def * @return {View.View} the view component */ getPreviewComponent: function(def) { if (!def.layout || !def.view) { return null; } if (this.componentsCache[def.layout] && this.componentsCache[def.layout][def.view]) { return this.componentsCache[def.layout][def.view]; } var layout = this.getLayoutChain(def.layout); var component = layout ? layout.getComponent(def.view) : null; if (component) { if (!this.componentsCache[def.layout]) { this.componentsCache[def.layout] = {}; } this.componentsCache[def.layout][def.view] = component; } return component; }, /** * Get nested layout if it's needed * @param layouts * @return {View.Layout} */ getLayoutChain: function(layouts) { var components = layouts.split('.'); var self = this; _.each(components, function(item) { if (self) { self = self.getComponent(item); } }); return self; }, /** * Unset config-layout on context when this is disposed * @private */ _dispose: function() { this.componentsCache = null; this.context.unset('config-layout'); this.context.off('portal:config:preview', this.handleConfigPreview, this); this._super('_dispose'); } }) }, "portaltheme-module-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @inheritdoc * * @class View.Views.Base.PortalThemeModuleListLayout * @alias SUGAR.App.view.layouts.BasePortalThemeModuleListLayout * @extends View.Layouts.Base.ModuleListLayout */ ({ // Portaltheme-module-list Layout (base) extendsFrom: 'ModuleListLayout', /** * @inheritdoc * @param options */ initialize: function(options) { // Skip parent initialize method, as the app:sync:complete listener and // app:view:change listeners are not needed app.view.Layout.prototype.initialize.call(this, options); // Replace template with module-list template so appearance matches megamenu this.template = app.template.getLayout('module-list'); this._resetMenu(); }, /** * @inheritdoc * @override * * Override parent to add portal-enabled modules rather than main app * modules * @private */ _addDefaultMenus: function() { var url = app.api.buildURL('Administration/portalmodules', 'read'); var successCallback = _.bind(this._addMenus, this); app.api.call('read', url, null, { success: successCallback }); }, /** * Util to serve as a callback once API returns portal-enabled modules. Adds * a menu dropdown for each module. * * @param moduleList List of modules to add to the megamenu * @private */ _addMenus: function(moduleList) { _.each(moduleList, function(module) { this._addMenu(module, true); }, this); // Because this is called as an API success callback, we need to re-render // after adding each module-menu to the list this.render(); }, /** * @override * * Use list template from module-list layout so portal preview megamenu * matches its base component * @param component * @return {Object} module-list's 'list' template * @private */ _getListTemplate: function(component) { return app.template.getLayout('module-list.list', component.module) || app.template.getLayout('module-list.list'); }, /** * @inheritdoc * @override * * Overloading this because for portal theme preview we do not want to * add the active module to the megamenu, and we do not want to set any * module as "active" in the preview */ _setActiveModule: function(module) { } }) }, "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.AdministrationConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseAdministrationConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'ConfigDrawerLayout', checkAccess: function() { this._super('checkAccess'); return this.accessUserOK && this.accessModuleOK && this.accessConfigOK; }, loadConfig: function() { // loading is not required } }) }, "maps-module-setup": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for maps module setup * * @class View.Layouts.Base.AdministrationMapsModuleSetupLayout * @alias SUGAR.App.view.layouts.BaseAdministrationMapsModuleSetupLayout */ ({ // Maps-module-setup Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Property initialization */ _initProperties: function() { this._configView = null; }, /** * Register context event handlers * */ _registerEvents: function() { this.listenTo(this.context, 'display:map:module:config', this.updateUI, this); }, /** * Update the UI elements from user action * * @param {Object} data */ updateUI: function(data) { const viewName = data.viewName; const module = data.widgetModule; const $container = this.$('[data-container=config-container]'); const moduleData = data.moduleData; $container.empty(); this._createConfigView(module, moduleData, viewName, $container); }, /** * Initialize and render inner config * * @param {string} module * @param {Object} moduleData * @param {string} viewName * @param {jQuery} $container */ _createConfigView: function(module, moduleData, viewName, $container) { this._disposeConfigView(); var configView = app.view.createView({ name: viewName, context: this.context, model: this.context.get('model'), layout: this, moduleData: moduleData, widgetModule: module }); this._configView = configView; $container.append(configView.$el); configView.render(); }, /** * Dispose loaded component * */ _disposeConfigView: function() { if (this._configView) { this._configView.dispose(); } this._configView = null; }, /** * @inheritdoc */ _dispose: function() { this._disposeConfigView(); this._super('_dispose'); }, }) }, "content-grid": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.AdministrationContentGridLayout * @alias SUGAR.App.view.layouts.BaseAdministrationContentGridLayout * @extends View.Views.Base.ContentGridLayout */ ({ // Content-grid Layout (base) extendsFrom: 'ContentGridLayout', /** * The admin layout */ adminLayout: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.adminLayout = this.closestComponent('administration'); if (this.adminLayout) { this.adminLayout.on('admin:panel-defs:fetched', function() { this.initGrid(); this.resizeGrid(); }, this); } this.debouncedResizeGrid = _.debounce(_.bind(this.resizeGrid, this), 100); $(window).on('resize', this.debouncedResizeGrid); }, /** * Change grid column mode if window was resized */ resizeGrid: function() { let columns = ($(window).width() <= 960) ? 1 : 12; if (this.grid && this.grid.opts.column !== columns) { this.grid.column(columns); this.grid.compact(); } }, /** * @inheritdoc */ getGridstackOptions: function() { return { staticGrid: true, // removes drag|drop|resize disableOneColumnMode: true, // will manually do 1 column, rtl: app.lang.direction === 'rtl' }; }, /** * @inheritdoc */ getGridstackWidgetOptions: function(component) { return { minHeight: 2, // widget occupies a min of 2 rows width: 6, // widget occupies half of the grid columns minWidth: 6, height: this.getGridstackWidgetHeight(component), }; }, /** * Get the height of a Gridstack widget based on the outer * height of the wrapper element * * @param component * @return int */ getGridstackWidgetHeight: function(component) { let $wrapper = component.$el.find('.content-container-items'); return Math.ceil($wrapper.outerHeight(true) / this.pixelsPerGridstackRow); }, /** * @inheritdoc * @override */ getDefsForComponents: function() { let defs = []; let adminPanelDefs = this.adminLayout && _.isFunction(this.adminLayout.getAdminPanelDefs) ? this.adminLayout.getAdminPanelDefs() : []; _.each(adminPanelDefs, def => { defs.push(this.getContentContainerComponentDef(def)); }, this); return defs; }, /** * Get the content-container layout definition * * @param def * @return {Object} */ getContentContainerComponentDef: function(def) { let items = []; if (def.options) { _.each(def.options, option => { items.push({ label: option.label, tooltip: option.description, icon: option.icon, customIcon: option.customIcon, href: option.link, onclick: option.onclick, target: option.target }); }); } return { layout: { name: 'content-container', css_class: 'grid-stack-item-content', label: def.label || '', description: def.description || '', components: [ { view: { name: 'action-items', items: items } } ] } }; }, /** * @inheritdoc */ _dispose: function() { $(window).off('resize', this.resizeGrid, this); this._super('_dispose'); } }) }, "actionbutton-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for a single action configuration * * @class View.Layouts.Base.AdministrationActionbuttonActionLayout * @alias SUGAR.App.view.layouts.BaseAdministrationActionbuttonActionLayout * @extends View.Layout */ ({ // Actionbutton-action Layout (base) /** * Actions available only on SELL/SERVE * * @var Object */ selServeActions: { 'document-merge': 'LBL_ACTIONBUTTON_DOCUMENT_MERGE', }, events: { 'click [data-action="remove"]': 'removeAction', 'click [data-action="add"]': 'addNewAction', 'change .ab-admin-action-selector select': 'actionChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * @param {Object} options */ _beforeInit: function(options) { this.isSellServe = app.user.hasSellServeLicense(); let actions = options.context.get('model').get('actions'); // we still need this into the actions object. // they wil be disabled into the select if (!this.isSellServe) { _.extend(actions, this.selServeActions); } this._actionId = options.actionId; this._actions = { label: 'LBL_ACTIONBUTTON_ACTION', id: 'actionsDropdown', value: options.actionType, options: actions, disabled: this.selServeActions[options.actionType] && !this.isSellServe ? true : false, }; this._buttonData = this._getActiveButtonData(options); this._actionData = options.actionData; if (Object.keys(this._actionData).length === 0) { this._actionData = { actionType: options.actionType, orderNumber: Object.keys(this._buttonData.actions).length, properties: {} }; } }, /** * Clear out default functionality * @inheritdoc */ _initProperties: function() { }, /** * Clear out default functionality * @inheritdoc */ _registerEvents: function() { }, /** * Message parent to add a new action */ addNewAction: function() { this.context.get('model').trigger('button:action:added'); }, /** * Message parent to remove current action */ removeAction: function() { if (this.layout._canAddDeleteAction) { app.alert.show('alert-actionbutton-delete', { level: 'confirmation', messages: app.lang.get('LBL_ACTIONBUTTON_DELETE_ACTION'), autoClose: false, onConfirm: _.bind(function deletebutton() { // remove the action from the button data delete this._buttonData.actions[this._actionId]; var ctxModel = this.context.get('model'); var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonData.buttonId] = this._buttonData; ctxModel.set('data', buttonsData); // notify listeners ctxModel.trigger('button:action:removed', this._actionId); }, this), }); } }, /** * Further initialization/update of layout/context properties */ setup: function() { this._actionData.properties = this._createActionView(); this._buttonData.actions[this._actionId] = this._actionData; var ctxModel = this.context.get('model'); // as the actions changed, we have to store them into the main container var buttonsData = ctxModel.get('data'); buttonsData.buttons[this._buttonData.buttonId] = this._buttonData; ctxModel.set('data', buttonsData); this._actions.value = this._actionData.actionType; this.$('.ab-admin-action-selector select').select2(); }, /** * Update properties based on action selection * @param {UIEvent} e */ actionChanged: function(e) { this._actions.value = e.currentTarget.value; this._actionData = { actionType: this._actions.value, orderNumber: this._actionData.orderNumber, properties: {} }; this.setup(); }, /** * Initialize and render inner action view * @return {Object} */ _createActionView: function() { this._disposeSubComponents(); var container = this.$('.ab-admin-action-view'); container.empty(); var actionView = app.view.createView({ name: 'actionbutton-' + this._actionData.actionType, context: this.context, model: this.context.get('model'), layout: this, actionId: this._actionId, buttonId: this._buttonData.buttonId, actionData: this._actionData, }); this._subComponents.push(actionView); container.append(actionView.$el); actionView.setup(); actionView.render(); return actionView.getProperties(); }, /** * Return active button properties * @param {Object} options */ _getActiveButtonData: function(options) { var buttons = options.context.get('model').get('data').buttons; var activeButton = _.filter(buttons, function getActiveButtonData(buttonData) { return buttonData.active === true; })[0]; return activeButton; }, /** * Dispose any loaded components */ _disposeSubComponents: function() { _.each(this._subComponents, function(component) { component.$('.select2-container').select2('close'); component.dispose(); }, this); this._subComponents = []; }, /** * @inheritdoc */ _dispose: function() { this._disposeSubComponents(); this._super('_dispose'); }, }) }, "administration": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.AdministrationAdministrationLayout * @alias SUGAR.App.view.layouts.BaseAdministrationAdministrationLayout * @extends View.Layout */ ({ // Administration Layout (base) /** * Admin Panels metadata */ adminPanelDefs: null, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.fetchAdminPanelDefs(); }, /** * Fetch Admin Panels metadata */ fetchAdminPanelDefs: function() { var url = app.api.buildURL('Administration/adminPanelDefs'); app.api.call('read', url, null, { success: _.bind(function(data) { this.handleFetchAdminPanelDefsSuccess(data); }, this) }); }, /** * Handle a successful fetch of Admin Panels metadata * * @param data */ handleFetchAdminPanelDefsSuccess: function(data) { this.adminPanelDefs = data; this.trigger('admin:panel-defs:fetched'); }, /** * A helper function to get adminPanelDefs so child components * do not access the property directly * * @return array */ getAdminPanelDefs: function() { return this.adminPanelDefs || []; }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.$('input[data-action="search"]').focus(); }, }) }, "config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Base layout for Config Framework. * * @class View.Layouts.Base.AdministrationConfigLayout * @alias SUGAR.App.view.layouts.BaseAdministrationConfigLayout * @extends View.Layouts.Base.Layout */ ({ // Config Layout (base) /** * Append config view and header based on category * @inheritdoc */ _addComponentsFromDef: function(components) { var category = this.context.get('category'); if (category && components) { var viewName = category + '-config'; var viewController = { extendsFrom: 'AdministrationConfigView' }; app.view.declareComponent('view', viewName, 'Administration', viewController, false, 'base'); var headerName = category + '-config-header'; var headerController = { extendsFrom: 'AdministrationConfigHeaderView' }; app.view.declareComponent('view', headerName, 'Administration', headerController, false, 'base'); var layout = components[0].layout.components[0].layout.components; layout.push({ view: viewName }, { view: headerName }); } this._super('_addComponentsFromDef', [components, this.context, this.context.get('module')]); } }) }, "actionbutton-actions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for entire list of actions * * @class View.Layouts.Base.AdministrationActionbuttonActionsLayout * @alias SUGAR.App.view.layouts.BaseAdministrationActionbuttonActionsLayout * @extends View.Layout */ ({ // Actionbutton-actions Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initial setup of `buttonData` property */ _initProperties: function() { this.buttonData = this._getActiveButtonData(); this._canAddDeleteAction = true; }, /** * Context model event registration */ _registerEvents: function() { var ctxModel = this.context.get('model'); this.listenTo(ctxModel, 'update:button:view', this.refreshActions, this); this.listenTo(ctxModel, 'button:action:added', this.addNewAction, this); this.listenTo(ctxModel, 'button:action:removed', this.removeAction, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.refreshActions(); this._applyCustomStyle(); this._makeActionsSortable(); }, /** * Recreate action layouts */ refreshActions: function() { // remove the actions of the previosuly selected button // create the actions of the current selected button this.buttonData = this._getActiveButtonData(); if (Object.keys(this.buttonData.actions).length === 0) { this.$('.ab-admin-actions-container').empty(); this._createAction({}, app.utils.generateUUID()); } else { this._createActions(); } }, /** * Handles adding a new action */ addNewAction: function() { if (this._canAddDeleteAction) { this._createAction({}, app.utils.generateUUID()); } }, /** * Handles re-rendering the actions when removing an action from the data object */ removeAction: function() { this.refreshActions(); }, /** * Apply custom style class to parent layout */ _applyCustomStyle: function() { this.layout.$el.addClass('ab-admin-main-left-pane'); const mainPane = this.closestComponent('main-pane'); if (mainPane) { mainPane.$el.addClass('overflow-y-auto'); } }, /** * Creating the sidecar layouts for each action */ _createActions: function() { this._disposeSubComponents(); this.$('.ab-admin-actions-container').empty(); _.each(this.buttonData.actions, function setActionID(actionData, id) { actionData.id = id; }); const _orderedActions = _.sortBy(this.buttonData.actions, 'orderNumber'); _.each(_orderedActions, function createAction(actionData) { this._createAction(actionData, actionData.id); }, this); }, /** * Create a specific layout for a given action * * @param {Object} actionData * @param {string} actionId * */ _createAction: function(actionData, actionId) { if (!this._subComponents) { this._subComponents = []; } var defaultAction = 'create-record'; var actionLayout = app.view.createLayout({ name: 'actionbutton-action', context: this.context, model: this.context.get('model'), layout: this, actionId: actionId, actionType: actionData.actionType ? actionData.actionType : defaultAction, actionData: actionData, }); this._subComponents.push(actionLayout); this.$('.ab-admin-actions-container').append(actionLayout.$el); actionLayout.setup(); }, /** * Adds the sortability feature to created actions */ _makeActionsSortable: function() { this.$('.ab-admin-actions-container').sortable({ revert: true, start: _.bind(function blockRemoval(event, ui) { // if we drag buttons we need to block the delete functions this._canAddDeleteAction = false; var initialIndex = ui.item.index(); ui.item.data('initialIndex', initialIndex); }, this), stop: _.bind(function allowRemoval(event, ui) { // when we release the button we can remove buttons once again this._canAddDeleteAction = true; this._reorderActions(ui.item.data('initialIndex'), ui.item.index()); }, this) }); }, /** * Reorders actions list * * @param {number} initialOrderNumber * @param {number} finalOrderNumber */ _reorderActions: function(initialOrderNumber, finalOrderNumber) { this._unsetActionOrder(initialOrderNumber); _.each(this._subComponents, function orderActions(action) { if (action._actionData.orderNumber !== -1) { if ( initialOrderNumber > finalOrderNumber && action._actionData.orderNumber >= finalOrderNumber && action._actionData.orderNumber <= initialOrderNumber ) { action._actionData.orderNumber = action._actionData.orderNumber + 1; } if ( initialOrderNumber < finalOrderNumber && action._actionData.orderNumber >= initialOrderNumber && action._actionData.orderNumber <= finalOrderNumber ) { action._actionData.orderNumber = action._actionData.orderNumber - 1; } } }); _.each(this._subComponents, function orderButtons(action) { if (action._actionData.orderNumber === -1) { action._actionData.orderNumber = finalOrderNumber; } action.setup(); }); }, /** * Sets the orderNumber to -1 * @param {number} orderNumber */ _unsetActionOrder: function(orderNumber) { _.each(this._subComponents, function unsetOrder(action) { if (action._actionData.orderNumber === orderNumber) { action._actionData.orderNumber = -1; } }); }, /** * Return configuration for active button * @return {Object} */ _getActiveButtonData: function() { var buttons = this.context.get('model').get('data').buttons; var activeButton = _.filter(buttons, function getActiveButtonData(buttonData) { return buttonData.active === true; })[0]; return activeButton; }, /** * Dispose any loaded components */ _disposeSubComponents: function() { _.each(this._subComponents, function(component) { component.dispose(); }, this); this._subComponents = []; }, /** * @inheritdoc */ _dispose: function() { this._disposeSubComponents(); this._super('_dispose'); }, }) }, "portaltheme-megamenu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * PortalPreviewMegamenu contains the components that make up the preview megamenu * on the portal config page * * @class View.Layouts.Base.AdministrationPortalThemeMegaMenu * @alias SUGAR.App.view.layouts.PortalThemeMegaMenu * @extends View.Layouts.Base.HeaderLayout */ ({ // Portaltheme-megamenu Layout (base) extendsFrom: 'HeaderLayout', /** * @inheritdoc * @override * Add button with portal help text to header-help component * @param components * @param options * @param context */ initComponents: function(components, options, context) { this._super('initComponents', [components, options, context]); var helpComponent = this.getComponent('header-help'); // overwrite header help buttons with our portal header help button if (_.isObject(helpComponent) && _.isObject(helpComponent.meta)) { helpComponent.meta.buttons = this._getPortalHelpButton(); } }, /** * Create metadata for the portal preview megamenu button * with the correct label * * @return {Array} An array with a single button's metadata for portal megamenu * @private */ _getPortalHelpButton: function() { return [{ type: 'button', name: 'help_button', css_class: 'btn-primary', label: this._getHelpButtonLabel(), events: { click: 'button:help_button:click' } }]; }, /** * Get the appropriate label string for the megamenu help button. Defaults * to 'New Case' * * @return {string} Translated label for portal megamenu button * @private */ _getHelpButtonLabel: function() { return app.lang.get( 'LBL_PORTALTHEME_NEW_CASE_BUTTON_TEXT_DEFAULT', 'Administration' ); } }) }, "maps-controls": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Layout for maps configuration * * @class View.Layouts.Base.AdministrationMapsControlsLayout * @alias SUGAR.App.view.layouts.BaseAdministrationMapsControlsLayout */ ({ // Maps-controls Layout (base) /** * Event listeners */ events: { 'change [data-fieldname=log-level]': 'logLevelChanged', 'change [data-fieldname=unit-type]': 'unitTypeChanged', }, /** * @inheritdoc */ initialize: function(options) { this._beforeInit(options); this._super('initialize', [options]); this._initProperties(); this._registerEvents(); }, /** * Initialization of properties needed before calling the sidecar/backbone initialize method * * @param {Object} options * */ _beforeInit: function(options) { this._select2Data = this._getSelect2Data(); }, /** * Property initialization */ _initProperties: function() { this._modulesWidgets = []; this._availableModulesForCurrentLicense = []; this._deniedModules = this._getDeniedModules(); }, /** * Check if default data is setup */ _initDefaultData: function() { if (!this.model.get('maps_logLevel')) { this.model.set('maps_logLevel', 'fatal'); } if (!this.model.get('maps_unitType')) { this.model.set('maps_unitType', 'miles'); } if (!this.model.get('maps_enabled_modules')) { this.model.set('maps_enabled_modules', ['Accounts']); } this.setAvailableSugarModules(); }, /** * Register context event handlers * */ _registerEvents: function() { this.listenTo(this.context, 'retrived:maps:config', this.configRetrieved, this); this.listenTo(this.model, 'change', this.refreshAvailableModules, this); }, /** * Called when config is being retrieved from DB * * @param {Object} data */ configRetrieved: function(data) { this._initDefaultData(); this._updateUI(data); }, /** * Update the UI elements from config * * @param {Object} data */ _updateUI: function(data) { this._updateGeneralSettingsUI(data); this._updateModulesWidgets(data); }, /** * Refresh available modules */ refreshAvailableModules: function() { this.setAvailableSugarModules(); this._updateModulesWidgets(this.model.toJSON()); }, /** * Update the module widget * * @param {Object} data */ _updateModulesWidgets: function(data) { const availableModules = this.model.get('maps_enabled_modules'); const $container = this.$('[data-container=modules-widgets-container]'); const availableModulesForCurrentLicense = _.keys(this._availableModulesForCurrentLicense); $container.empty(); this._disposeModulesWidgets(); if (_.isEmpty(availableModules)) { this.$('.maps-missing-modules').show(); } else { this.$('.maps-missing-modules').hide(); } _.chain(availableModules) .filter(function filter(currentModule) { return _.contains(availableModulesForCurrentLicense, currentModule); }, this) .each(function createModuleWidget(module) { let moduleData = {}; if (_.has(data, 'modulesData') && _.has(data.modulesData, module)) { moduleData = data.modulesData[module]; } this._createModuleWidgetView(module, moduleData, $container); }, this); }, /** * Update Log Level and Measuremenet Unit from config * * @param {Object} data */ _updateGeneralSettingsUI: function(data) { this._updateSelect2El('log-level', data); this._updateSelect2El('unit-type', data); }, /** * Update select2 value * * @param {string} elId * @param {Object} data */ _updateSelect2El: function(elId, data) { const dataKey = app.utils.kebabToCamelCase(elId); if (_.has(data, dataKey)) { let id = data[dataKey]; let text = app.lang.getModString(this._getSelect2Label(dataKey, data[dataKey]), this.module); this.$('[data-fieldname=' + elId + ']').select2('data', { id: id, text: text }); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); let select2Options = this._getSelect2Options({ 'minimumResultsForSearch': -1, sortResults: function(results, container, query) { results = _.sortBy(results, 'text'); return results; } }); this.$('[data-fieldname=log-level]').select2(select2Options); this.$('[data-fieldname=unit-type]').select2(select2Options); this.select2('add-new-module', '_queryAvailableModules', true, _.bind(this.addNewModuleChanged, this)); }, /** * Populate select2 component * * @param {Object} query * * @return {Function} */ _queryAvailableModules: function(query) { return this._query(query, '_availableModules'); }, /** * Generic select2 selection list builder * * @param {Object} query * @param {string} list * */ _query: function(query, list) { var listElements = this[list]; var data = { results: [], more: false }; if (_.isObject(listElements)) { _.each(listElements, function pushValidResults(element, index) { if (query.matcher(query.term, element)) { data.results.push({id: index, text: element}); } }); data.results = _.sortBy(data.results, 'text'); } else { listElements = null; } query.callback(data); }, /** * Event handler for log level selection change * * @param {UIEvent} e * */ logLevelChanged: function(e) { const logLevel = e.currentTarget.value; const key = 'maps_logLevel'; this.model.set(key, logLevel); }, /** * Event handler for unit type selection change * * @param {UIEvent} e * */ unitTypeChanged: function(e) { const unitType = e.currentTarget.value; const key = 'maps_unitType'; this.model.set(key, unitType); }, /** * Add a new module to geocoded module list * * @param {Object} data */ addNewModuleChanged: function(data) { let availableModules = this.model.get('maps_enabled_modules'); availableModules.push(data.id); this.model.set('maps_enabled_modules', availableModules); this.model.trigger('change', this.model); this.setAvailableSugarModules(); }, /** * Create generic Select2 options object * * @return {Object} */ _getSelect2Options: function(additionalOptions) { var select2Options = {}; select2Options.placeholder = app.lang.get('LBL_MAPS_SELECT_NEW_MODULE_TO_GEOCODE', 'Administration'); select2Options.dropdownAutoWidth = true; select2Options = _.extend({}, additionalOptions); return select2Options; }, /** * Data for select2 * * @return {Object} */ _getSelect2Data: function() { const data = { 'logLevel': { 'fatal': 'LBL_MAPS_LOG_LVL_FATAL', 'debug': 'LBL_MAPS_LOG_LVL_DEBUG', 'error': 'LBL_MAPS_LOG_LVL_ERROR' }, 'unitType': { 'miles': 'LBL_MAPS_UNIT_TYPE_MILES', 'km': 'LBL_MAPS_UNIT_TYPE_KM' }, 'availableModules': this._availableModules, }; return data; }, /** * Get dropdown label * * @param {string} select2Id * @param {string} key * @return {string} */ _getSelect2Label: function(select2Id, key) { return this._select2Data[select2Id][key]; }, /** * Initialize and render inner module item * * @param {string} module * @param {Object} moduleData * @param {jQuery} $container */ _createModuleWidgetView: function(module, moduleData, $container) { var widgetView = app.view.createView({ name: 'maps-module-widget', context: this.context, model: this.context.get('model'), layout: this, moduleData: moduleData, widgetModule: module }); this._modulesWidgets.push(widgetView); $container.append(widgetView.$el); widgetView.render(); }, /** * Create generic Select2 component or return a cached select2 element * * @param {string} fieldname * @param {string} queryFunc * @param {boolean} reset * @param {Function} callback */ select2: function(fieldname, queryFunc, reset, callback) { if (this._select2 && this._select2[fieldname]) { return this._select2[fieldname]; }; this._disposeSelect2(fieldname); let additionalOptions = {}; if (queryFunc && this[queryFunc]) { additionalOptions.query = _.bind(this[queryFunc], this); } var el = this.$('[data-fieldname=' + fieldname + ']') .select2(this._getSelect2Options(additionalOptions)) .data('select2'); this._select2 = this._select2 || {}; this._select2[fieldname] = el; if (reset) { el.onSelect = (function select(fn) { return function returnCallback(data, options) { if (callback) { callback(data); } if (arguments) { arguments[0] = { id: 'select', text: app.lang.get('LBL_MAPS_SELECT_NEW_MODULE_TO_GEOCODE', 'Administration') }; } return fn.apply(this, arguments); }; })(el.onSelect); } return el; }, /** * Get a list of available modules */ setAvailableSugarModules() { this._availableModules = {}; _.each(app.metadata.getModules(), function getAvailableModules(moduleData, moduleName) { if (!_.contains(this._deniedModules, moduleName)) { let moduleLabel = app.lang.getModString('LBL_MODULE_NAME', moduleName); if (!moduleLabel) { moduleLabel = app.lang.getModuleName(moduleName, { plural: true }); } this._availableModulesForCurrentLicense[moduleName] = moduleLabel; if (!_.contains(this.model.get('maps_enabled_modules'), moduleName)) { this._availableModules[moduleName] = moduleLabel; } } }, this); }, /** * Get the list of denied modules * * @return {Array} */ _getDeniedModules: function() { return [ 'ACLActions', 'ACLFields', 'ACLRoles', 'Activities', 'Administration', 'ArchiveRuns', 'Audit', 'CampaignLog', 'CampaignTrackers', 'Charts', 'Configurator', 'Connectors', 'ConsoleConfiguration', 'ContractTypes', 'Currencies', 'CustomFields', 'CustomQueries', 'Dashboards', 'DataArchiver', 'DataPrivacy', 'DataSet_Attribute', 'DataSets', 'DocumentRevisions', 'DynamicFields', 'Emails', 'EAPM', 'EditCustomFields', 'EmailAddresses', 'EmailMan', 'EmailMarketing', 'EmailParticipants', 'EmailTemplates', 'EmbeddedFiles', 'Employees', 'Exports', 'Expressions', 'FAQ', 'ForecastManagerWorksheets', 'ForecastWorksheets', 'Groups', 'HealthCheck', 'History', 'Holidays', 'Home', 'Import', 'InboundEmail', 'KBArticles', 'KBDocuments', 'KBOLDContents', 'KBOLDDocumentKBOLDTags', 'KBOLDDocumentRevisions', 'KBOLDDocuments', 'KBOLDTags', 'Library', 'Login', 'Manufacturers', 'MergeRecords', 'MobileDevices', 'ModuleBuilder', 'MySettings', 'OAuthKeys', 'OAuthTokens', 'OptimisticLock', 'OutboundEmailConfiguration', 'PdfManager', 'ProductBundles', 'ProductBundleNotes', 'ProductTypes', 'Project', 'ProjectTask', 'PushNotifications', 'QueryBuilder', 'Quotas', 'Relationships', 'Releases', 'ReportMaker', 'Reports', 'SNIP', 'Shifts', 'SavedSearch', 'Schedulers', 'SchedulersJobs', 'Shippers', 'Studio', 'Styleguide', 'Subscriptions', 'SugarFavorites', 'SugarLive', 'Sugar_Favorites', 'Sync', 'TaxRates', 'TeamMemberships', 'TeamNotices', 'TeamSetModules', 'TeamSets', 'Teams', 'TimePeriods', 'TrackerPerfs', 'TrackerQueries', 'TrackerSessions', 'Trackers', 'UpgradeWizard', 'UserPreferences', 'UserSignatures', 'Versions', 'VisualPipeline', 'WebLogicHooks', 'Words', 'Worksheet', 'WorkFlow', 'WorkFlowActionShells', 'WorkFlowActions', 'WorkFlowAlertShells', 'WorkFlowAlerts', 'WorkFlowTriggerShells', 'ShiftExceptions', 'iCals', 'iFrames', 'pmse_Business_Rules', 'pmse_Emails_Templates', 'pmse_Inbox', 'pmse_Project', 'vCals', 'vCards', 'ReportSchedules', 'ProductCategories', 'OutboundEmail', 'Notifications', 'Newsletters', 'KBContents', 'KBContentTemplates', 'Geocode', 'Filters', 'Feeds', 'Feedbacks', 'DocumentTemplates', 'DocumentMerges', 'CommentLog', 'Comments', 'Categories', 'Queues', 'Error', 'ChangeTimers', 'HintAccountsets', 'HintEnrichFieldConfigs', 'HintNewsNotifications', 'HintNotificationTargets', 'CloudDrivePaths' ]; }, /** * Dispose any loaded components * */ _disposeModulesWidgets: function() { _.each(this._modulesWidgets, function(component) { component.dispose(); }, this); this._modulesWidgets = []; }, /** * Dispose a select2 element */ _disposeSelect2: function(name) { this.$('[data-fieldname=' + name + ']').select2('destroy'); }, /** * Dispose all select2 elements */ _disposeSelect2Elements: function() { this._disposeSelect2('log-level'); this._disposeSelect2('unit-type'); this._disposeSelect2('add-new-module'); }, /** * @inheritdoc */ _dispose: function() { this._disposeSelect2Elements(); this._disposeModulesWidgets(); this._super('_dispose'); }, }) } }} , "datas": {} }, "ACLRoles":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "InboundEmail":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Releases":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Prospects":{"fieldTemplates": {} , "views": { "base": { "convert-results": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Convert-results View (base) extendsFrom: 'ConvertResultsView', /** * Fetches the data for the leads model */ populateResults: function() { if (!_.isEmpty(this.model.get('lead_id'))){ var leads = app.data.createBean('Leads', { id: this.model.get('lead_id')}); leads.fetch({ success: _.bind(this.populateLeadCallback, this) }); } }, /** * Success callback for retrieving associated lead model * @param leadModel */ populateLeadCallback: function (leadModel) { var rowTitle; this.associatedModels.reset(); rowTitle = app.lang.get('LBL_CONVERTED_LEAD',this.module); leadModel.set('row_title', rowTitle); this.associatedModels.push(leadModel); app.view.View.prototype.render.call(this); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary']); this._super('initialize', [options]); }, delegateButtonEvents: function() { this.context.on('button:convert_button:click', this.convertProspectClicked, this); this._super('delegateButtonEvents'); }, convertProspectClicked: function() { var prefill = app.data.createBean('Leads'); prefill.copy(this.model); app.drawer.open({ layout: 'create', context: { create: true, model: prefill, module: 'Leads', prospect_id: this.model.get('id') } }, _.bind(function(context, model) { //if lead is created, grab the new relationship to the target so the convert-results will refresh if (model && model.id && !this.disposed) { this.model.fetch(); _.each(this.context.children, function(child) { if (child.get('isSubpanel') && !child.get('hidden')) { if (child.get('collapsed')) { child.resetLoadFlag({recursive: false}); } else { child.reloadData({recursive: false}); } } }); } }, this)); prefill.trigger('duplicate:field', this.model); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Queues":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "EmailMarketing":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "EmailTemplates":{"fieldTemplates": { "base": { "show-plain-text": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.EmailTemplates.ShowPlaintextField * @alias SUGAR.App.view.fields.BaseEmailTemplatesShowPlaintextField * @extends View.Fields.Base.BaseField */ ({ // Show-plain-text FieldTemplate (base) extendsFrom: 'BaseField', events: { 'click [name="plaintext"]': 'buttonClicked' }, plainTextField: 'body', plainTextExpanded: false, /** * Bind event listeners */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change:text_only', this.resetState, this); }, /** * If the user checks "text only" we toggle the editor, so this button will * be hidden. We want to reset it so when it is shown again it has the proper * text. */ resetState: function() { if ($('input[type=checkbox]').prop('checked')) { this.toggleExpandPlainText(false); } else { this.toggleExpandPlainText(!this.plainTextExpanded); } this.render(); }, /** * The body and body_html are toggled based on `text_only` using SugarLogic * Dependencies. Using SugarLogic's SetVisibilityAction here ensures we toggle * them in the same way as the checkbox to avoid conflicts. */ buttonClicked: function() { this.plainTextExpanded = !this.plainTextExpanded; this.toggleExpandPlainText(!this.plainTextExpanded); this.render(); }, /** * Toggles the expanded plain text field styling based on button press * @param {boolean} toggle value to determine whether its collapsed (true) or expanded (false) */ toggleExpandPlainText: function(toggle) { $('textarea[name=body]').parent().toggleClass('collapsed-plain-text', toggle); }, /** * Remove event listeners * @private */ _dispose: function() { this.model.off('change:text_only', this.resetState, this); this._super('_dispose'); } }) }, "insert-variable": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.EmailTemplates.InsertVariableField * @alias SUGAR.App.view.fields.BaseEmailTemplatesInsertVariableField * @extends View.Fields.Base.BaseField */ ({ // Insert-variable FieldTemplate (base) extendsFrom: 'BaseField', /** * List of fields blacklisted from inclusion in Email Template variables */ badFields: [ 'team_id', 'account_description', 'contact_id', 'lead_id', 'opportunity_amount', 'opportunity_id', 'opportunity_name', 'opportunity_role_id', 'opportunity_role_fields', 'opportunity_role', 'campaign_id', // User objects 'id', 'date_entered', 'date_modified', 'user_preferences', 'accept_status', 'user_hash', 'authenticate_id', 'sugar_login', 'reports_to_id', 'reports_to_name', 'is_admin', 'receive_notifications', 'modified_user_id', 'modified_by_name', 'created_by', 'created_by_name', 'accept_status_id', 'accept_status_name', 'acl_role_set_id', 'sync_key' ], /** * List of field types blacklisted from inclusion in Email Template Variables */ badFieldTypes: [ 'assigned_user_name', 'link', 'bool' ], /** * Initial empty object to hold our data structure used for variable lookup. * After loading this will be in the form of: * * { * ModuleDropdown: [ * // list of options for variable dropdown * {name: '...', 'value'...}, * ... * ], * ... * } * * This list of field names and variable values is generated based on the * `variable_source` array passed in the moduleList metadata */ variableOptions: {}, /** * Event listeners */ events: { 'click [name="insert_button"]': 'insertClicked' }, /** * @inheritdoc * * Prepare human-readable labels for module dropdown, and generate the * variable option object. * * @param {Object} options */ initialize: function(options) { // We prepare the labels before calling "Super" so everything can be // translated before options.moduleList is bound to this.moduleList. options.moduleList = this._prepareLabels(options); this._super('initialize', [options]); this._generateOptions(); }, /** * On changing into edit mode, we bind dropdown event listeners. On changing * out of edit mode, we hide the record cell. */ bindDomChange: function() { this._super('bindDomChange'); this.$el.closest('.record-cell').toggle(this.action === 'edit'); var $moduleDropdown = this.$el.children('[name="variable_module"]'); var onModuleChange = _.bind(this._updateVariables, this); $moduleDropdown.on('change', onModuleChange); var $variableDropdown = this.$el.children('[name="variable_name"]'); var onVariableChange = _.bind(this._showVariable, this); $variableDropdown.on('change', onVariableChange); this._updateVariables(); }, /** * If moduleList label is a "LBL_" string, translate it. If it is an array * of module names, filter it by ACL access and join module names with '/' * * @param options * @return {Object} * @private */ _prepareLabels: function(options) { return _.map(options.def.moduleList, function(module) { if (_.isArray(module.label)) { module.label = _.filter(module.label, function(module) { return app.acl.hasAccess('view', module); }, this); module.label = _.map(module.label, function(label) { return app.lang.getModuleName(label); }).join('/'); } else { module.label = app.lang.get(module.label); } return module; }); }, /** * Iterate over list of modules, setting variableOptions by module key. * Delegates heavy lifting to _getVariablesByModule. * * @private */ _generateOptions: function() { _.each(this.def.moduleList, function(module) { this.variableOptions[module.value] = this._getVariablesByModule(module); }, this); }, /** * Generate a list of variables available based on the provided module metadata. * Metadata comes in the form of * { * 'value': 'Contacts', * 'variable_source': ['Contacts', 'Leads', 'Prospects'] * 'variable_prefix': 'contact_' * } * * For each module in the `variable_source` array, all of the fields not * blacklisted are added to the array of available dropdown options. * * For `variable_source` modules where the user lacks ACL access, * app.data.createBean returns a model with no fields defined, so no * variables are added. * * @param module Module metadata telling us which modules' fields to load, * and what prefix to use when inserting fields from the second * dropdown * @return {Array} Array of {name, value} options for the provided module * dropdown value * @private */ _getVariablesByModule: function(module) { // Cache with fast insertion and lookup to avoid repeat // fields var variableCache = new Set(); var variables = []; _.each(module.variable_source, function(moduleKey) { var bean = app.data.createBean(moduleKey); _.each(bean.fields, function(field) { if (variableCache.has(field.name) || this._shouldOmitField(field)) { // If we should omit this field, store it in our cache so // the next time we see it we can skip it faster variableCache.add(field.name); return; } // prepend our variable_prefix to the field name var key = module.variable_prefix + field.name; key = key.toLowerCase(); var label = app.lang.get(field.vname, moduleKey); variables.push({ name: key, value: label }); variableCache.add(field.name); }, this); }, this); return variables; }, /** * Util for determining if a field should be omitted. This is intended to * improve readability over short-circuit evaluation while still performing * the cheapest checks up front. * * @param field Field metadata from module bean. * @return {boolean} True if field should be omitted * @private */ _shouldOmitField: function(field) { if (_.isEmpty(field.name) || _.isEmpty(field.type)) { return true; } if (field.type === 'relate' && _.isEmpty(field.custom_type)) { return true; } // badFieldTypes is smaller, so check it first if (_.contains(this.badFieldTypes, field.type)) { return true; } // Finally the most expensive check return _.contains(this.badFields, field.name); }, /** * Callback for when the module dropdown changes. This empties the variable * dropdown, and appends a new list of options created during _generateOptions. * @private */ _updateVariables: function() { var selection = this.$el.children('[name="variable_module"]'); var options = this.variableOptions[selection.val()]; var variableDropdown = selection.siblings('[name="variable_name"]'); variableDropdown.empty(); _.each(options, function(option) { var newOption = document.createElement('option'); newOption.value = '$' + option.name; newOption.text = option.value; variableDropdown.append(newOption); }); this._showVariable(); }, /** * Show selected variable in the `variable` input box * @private */ _showVariable: function() { var selection = this.$el.children('[name="variable_name"]'); var variableInput = selection.siblings('[name="variable_text"]'); variableInput.val(selection.val()); }, /** * Trigger `insertClicked` event on the view with variable input value so * listeners can handle the actual variable insertion */ insertClicked: function() { var variableInput = this.$el.children('[name="variable_text"]'); this.view.trigger('insertClicked', variableInput.val()); } }) }, "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.EmailTemplates.Htmleditable_tinymceField * @alias SUGAR.App.view.fields.BaseEmailTemplatesHtmleditable_tinymceField * @extends View.Fields.Base.Htmleditable_tinymceField */ ({ // Htmleditable_tinymce FieldTemplate (base) extendsFrom: 'Htmleditable_tinymceField', /** * Email Template specific parameters. * @private */ _tinyMCEConfig: { 'height': '430', }, /** * @inheritdoc * * Adds buttons for uploading a local file and selecting a Sugar Document * to attach to the email. * * @fires email_attachments:file on the view when the user elects to attach * a local file. */ addCustomButtons: function(editor) { var attachmentButtons = []; // Attachments can only be added if the user has permission to create // Notes records. Only add the attachment button(s) if the user is // allowed. if (app.acl.hasAccess('create', 'Notes')) { attachmentButtons.push({ text: app.lang.get('LBL_EMAIL_ATTACHMENTS', this.module), type: 'menuitem', onAction: (event) => { // Track click on the file attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_file_button', event); this.view.trigger('email_attachments:file'); }, }); // The user can only select a document to attach if he/she has // permission to view Documents records in the selection list. // Don't add the Documents button if the user can't view and select // documents. if (app.acl.hasAccess('view', 'Documents')) { attachmentButtons.push({ text: app.lang.get('LBL_EMAIL_ATTACHMENTS2', this.module), type: 'menuitem', onAction: (event) => { // Track click on the document attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_doc_button', event); this._selectDocument(); }, }); } editor.ui.registry.addMenuButton('sugarattachment', { tooltip: app.lang.get('LBL_ATTACHMENTS', this.module), icon: 'plus', onAction: (event) => { // Track click on the attachment button. app.analytics.trackEvent('click', 'tinymce_email_attachment_button', event); }, fetch: (callback) => { callback(attachmentButtons); }, }); } }, /** * @override * * Override base field to not return true if the field is readonly, * even if the action is "edit". This occurs when toggling visibility via * SugarLogic. * * @return {boolean} false if the field is readonly, else call parent * @private */ _isEditView: function() { return !this.def.readonly && this._super('_isEditView'); }, /** * Allows the user to select a document to attach. * * @private * @fires email_attachments:document on the view with the selected document * as a parameter. {@link View.Fields.Base.EmailAttachmentsField} attaches * the document to the email. */ _selectDocument: function() { var def = { layout: 'selection-list', context: { module: 'Documents' } }; app.drawer.open(def, _.bind(function(model) { var document; if (model) { // `value` is not a real attribute. document = app.data.createBean('Documents', _.omit(model, 'value')); this.view.trigger('email_attachments:document', document); } }, this)); }, /** * @inheritdoc * * Adds custom TinyMCEConfig values for Email Templates view */ getTinyMCEConfig: function() { // Grab the default config and add/override unique values for creation var config = this._super('getTinyMCEConfig'); config = _.extend(config, this._tinyMCEConfig); return config; }, /** * @inheritdoc */ getEditorContent: function() { var text = this._htmleditor.getContent({format: 'html'}); //We don't need to get empty html, to prevent model changes. if (text !== '') { text = this._super('getEditorContent'); } return text; }, /** * @inheritdoc */ setViewName: function() { this.destroyTinyMCEEditor(); this._super('setViewName', arguments); } }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.EmailTemplates.RecordView * @alias SUGAR.App.view.views.BaseEmailTemplatesRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', initialize: function(options) { this.plugins = _.union(this.plugins, ['EmailTemplates']); this._super('initialize', [options]); }, /** * @override * * Override base record.js for Emails Attachment pills. The user can click * either the pill, or the span within the pill. Either of these should * not trigger entering edit mode * * @param element * @return {boolean} */ hasClickableAction: function(element) { var hasClickableAction = this._super('hasClickableAction', [element]); hasClickableAction = hasClickableAction || this.$(element).parent().attr('data-action'); return hasClickableAction; } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.EmailTemplatesCreatedView * @alias SUGAR.App.view.views.BaseEmailTemplatesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Add the EmailTemplates plugin * @param {Object} options */ initialize: function(options) { this.plugins = _.union(this.plugins, ['EmailTemplates']); this._super('initialize', [options]); }, /** * Call _toggleAttachmentsVisibility on render since Attachments should only * be visible if the field has a value * * @private */ _render: function() { this._super('_render'); this._toggleAttachmentsVisibility(); } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.EmailTemplates.PreviewView * @alias SUGAR.App.view.views.BaseEmailTemplatesPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * @inheritdoc * @param options */ initialize: function(options) { this.plugins = _.union(this.plugins, ['EmailTemplates']); this._super('initialize', [options]); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "SNIP":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ProspectLists":{"fieldTemplates": {} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', delegateButtonEvents: function() { this.context.on('button:export_button:click', this.exportListMembers, this); this._super("delegateButtonEvents"); }, /** * Event to trigger the Export page level action */ exportListMembers: function() { app.alert.show('export_loading', {level: 'process', title: app.lang.get('LBL_LOADING')}); app.api.exportRecords( { module: this.module, uid: [this.model.id], members: true }, this.$el, { complete: function() { app.alert.dismiss('export_loading'); } } ); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "SavedSearch":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "UpgradeWizard":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Trackers":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TrackerPerfs":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TrackerSessions":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TrackerQueries":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "FAQ":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Newsletters":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "SugarFavorites":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "PdfManager":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "DataArchiver":{"fieldTemplates": { "base": { "module-select": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DataArchiver.ModuleSelectField * @alias SUGAR.App.view.fields.BaseDataArchiverModuleSelectField * @extends View.Fields.Base.EnumField */ ({ // Module-select FieldTemplate (base) extendsFrom: 'EnumField', /** * Trigger the archiver:module:change when the user affects the module-select field */ bindDomChange: function() { this.$el.on('change', _.bind(function() { this.model.trigger('archiver:module:change', this.model.get(this.name)); }, this)); } }) }, "filter-field": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DataArchiver.FilterFieldField * @alias SUGAR.App.view.fields.BaseDataArchiverFilterFieldField * @extends View.Fields.Base.BaseField */ ({ // Filter-field FieldTemplate (base) events: { 'click [data-action=add]': 'addRow', 'click [data-action=remove]': 'removeRow', 'change [data-filter=field] input[type=hidden]': 'handleFieldSelected', 'change [data-filter=operator] input[type=hidden]': 'handleOperatorSelected', }, /** * Stores the list of filter field options. Defaults for all filter lists * can be specified here */ fieldList: {}, filterFields: {}, /** * Modules that are marked as needing the field list API call in order to get a more full field list for filtering * Normally we only use the call for non-visible modules since we dont store their filterable fields in metadata. */ needForcedAPICall: [ 'pmse_Inbox', ], /** * fields to be removed from filtering options */ removeProps: [ '$favorite', '$owner', 'tag', 'my_favorite', 'following' ], /** * Stores the mapping of filter operator options */ filterOperators: {}, _operatorsWithNoValues: [], /** * Stores the field tag control */ fieldTag: 'div.controls.filter-selector', /** * Stores the filter definition */ filterDef: [], /** * Stores the template to render a row of the filter list for edit and view views */ rowTemplateEdit: null, rowTemplateView: null, /** * Map of fields types. * * Specifies correspondence between field types and field operator types. */ fieldTypeMap: { 'datetime': 'date', 'datetimecombo': 'date' }, /* Operators that use a text field (instead of datetime/date/...) to enter the amount of days */ amountDaysOperators: [ '$more_x_days_ago', '$last_x_days', '$next_x_days', '$more_x_days_ahead', ], /** * Stores the name of the module this filter refers to */ moduleName: null, /** * @override * @param {Object} opts */ initialize: function(opts) { this._super('initialize', [opts]); // Store partial template this.rowTemplateEdit = app.template.getField('filter-field', 'edit-filter-row', 'DataArchiver'); this.rowTemplateDetail = app.template.getField('filter-field', 'detail-filter-row', 'DataArchiver'); this.moduleName = this.model.get('filter_module_name'); this.filterDef = this.model.get('filter_def'); this.fieldList = {}; this.filterFields = {}; }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset */ bindDataChange: function() { if (this.model) { this.model.on('archiver:module:change', this.handleModuleChange, this); this.model.on('change:' + this.name, function() { if (_.isEmpty(this.$(this.fieldTag).data('field'))) { this.render(); } }, this); } }, /** * Handles what occurs when the module selector is changed * @param module */ handleModuleChange: function(module) { this.filterFields = {}; // handles the clearing of the filter_defs when the module is changed this.model.set('filter_def', undefined); this.moduleName = module; this.model.set('filter_module_name', module); this.render(); }, /** * Loads the list of filter fields for supplied module. * * @param {string} module The module to load the filter fields for. */ loadFilterFields: function(module) { if (_.isUndefined(module)) { return; } if (!_.isUndefined(app.metadata.getModule(module)) && !this.needForcedAPICall.includes(module)) { this.fieldList = app.data.getBeanClass('Filters').prototype.getFilterableFields(module); if (_.isUndefined(this.fieldList.deleted)) { this.fieldList.deleted = app.metadata.getField({module: module, name: 'deleted'}); } this.trimFilterFieldList(module); this.setupRows(); } else { this.fieldList = this.getNonMetadataFields(module); } }, /** * Loads the list of filter operators for supplied module. * * @param {string} [module] The module to load the filters for. */ loadFilterOperators: function(module) { this.filterOperatorMap = app.metadata.getFilterOperators(module); this._operatorsWithNoValues = ['$empty', '$not_empty']; }, /** * Retrieves fields for modules not defined in metadata-manager.js * * @param module */ getNonMetadataFields: function(module) { var self = this; var url = app.api.buildURL('metadata/' + module + '/fields', null, null, {}); app.api.call('read', url, null, { success: function(results) { self.fieldList = results; self.trimFilterFieldList(module); self.setupRows(); }, error: function(e) { // Continue to use Sugar7's default error handler. if (_.isFunction(app.api.defaultErrorHandler)) { app.api.defaultErrorHandler(e); } } }); }, /** * Trims the fieldList variable to include only fields that are filterable * * @param module */ trimFilterFieldList: function(module) { // Clean up filters. These arnt needed in this context this.cleanUpFilters(); // For each field, if it is filterable (or a pre-defined filter), add it // to the filterFields list var nonFilterableTypes = ['id', 'relate']; _.each(this.fieldList, function(fieldDef, fieldName) { var label = app.lang.get(fieldDef.label || fieldDef.vname, module); // If it cant find a valid label, skipt it if (label && label.indexOf('LBL_') != -1) { return; } var isPredefined = fieldDef.predefined_filter; var isFilterable = !_.isEmpty(label) && this.filterOperatorMap[fieldDef.type] && _.indexOf(nonFilterableTypes, fieldDef.type) === -1; if (isPredefined || isFilterable) { this.filterFields[fieldName] = label; } }, this); }, /** * Handles the calling of logic that creates the filter rows for the record view */ setupRows: function() { var filterDef = this.model.get('filter_def'); if (filterDef) { filterDef = JSON.parse(filterDef); // If the filter definition is empty array then set it to undefined if (_.isArray(filterDef) && _.isEmpty(filterDef)) { this.model.set('filter_def', undefined); filterDef = undefined; } } this.populateFilter(filterDef); // If the filter definition is empty, add a fresh row if (this.$('[data-filter=row]').length === 0) { this.addRow(); } }, /** * Add new property to the 'removeProps' array * @param prop */ addPropToRemove: function(prop) { this.removeProps.push(prop); }, /** * Remove property from the 'removeProps' array * @param prop */ removePropToRemove: function(prop) { // Take this out of the list var idx = this.removeProps.indexOf(prop); if (idx > -1) { this.removeProps.splice(idx, 1); } }, /** * Cleans up the fieldList array to remove fields that are not desirable for filtering in this context */ cleanUpFilters: function() { // Predefined props we do not want _.each(this.removeProps, function(prop) { delete this.fieldList[prop]; }, this); // Any props marked as 'non-db' _.each(this.fieldList, function(prop, key) { if (!_.isUndefined(prop.source) && prop.source === 'non-db') { delete this.fieldList[key]; } }, this); }, /** * Loads the list of filter operators for supplied module. * * @param {string} [module] The module to load the filters for. */ loadFilterOperators: function(module) { this.filterOperatorMap = app.metadata.getFilterOperators(module); this._operatorsWithNoValues = ['$empty', '$not_empty']; }, /** * In edit mode, render filter input fields using the edit-filter-row template. * @inheritdoc * @private */ _render: function() { this._super('_render'); this.loadFilterOperators(this.model.get('filter_module_name')); this.loadFilterFields(this.model.get('filter_module_name')); }, /** * Builds the initial elements of the filter for the given filter definition * @param array filterDef the filter definition */ populateFilter: function(filterDef) { filterDef = app.data.getBeanClass('Filters').prototype.populateFilterDefinition(filterDef, true); _.each(filterDef, function(row) { this.populateRow(row); }, this); }, /** * Populates row fields with the row filter definition. * * In case it is a template filter that gets populated by values passed in * the context/metadata, empty values will be replaced by populated * value(s). * * @param {Object} rowObj The filter definition of a row. */ populateRow: function(rowObj) { _.each(rowObj, function(value, key) { var keyValuePair = new app.utils.FilterOptions().keyValueFilterDef(key, value, this.fieldList); key = keyValuePair[0]; value = keyValuePair[1]; _.each(value, function(value, operator) { this.initRow(null, {name: key, operator: operator, value: value}); }, this); }, this); }, /** * Add a row * @param {Event} e * @return {jQuery} $row The added row element. */ addRow: function(e) { var $row; if (e) { // Triggered by clicking the plus sign. Add the row to that point. $row = this.$(e.currentTarget).closest('[data-filter=row]'); if (this.action === 'detail') { $row.after(this.rowTemplateDetail()); } else { $row.after(this.rowTemplateEdit()); } $row = $row.next(); } return this.initRow($row); }, /** * Remove a row * @param {Event} e */ removeRow: function(e) { var $row = this.$(e.currentTarget).closest('[data-filter=row]'); $row.remove(); if (this.$('[data-filter=row]').length === 0) { this.addRow(); } this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Initializes a row either with the retrieved field values or the * default field values. * * @param {jQuery} [$row] The related filter row. * @param {Object} [data] The values to set in the fields. * @return {jQuery} $row The initialized row element. */ initRow: function($row, data) { if (this.action === 'detail') { $row = $row || $(this.rowTemplateDetail()).appendTo(this.$el); } else { $row = $row || $(this.rowTemplateEdit()).appendTo(this.$el); } data = data || {}; var model; var field; // Init the row with the data available. $row.attr('data-name', data.name); $row.attr('data-operator', data.operator); $row.attr('data-value', data.value); $row.data('name', data.name); $row.data('operator', data.operator); $row.data('value', data.value); // Create a blank model for the field selector enum, and set the // field value if we know it. model = app.data.createBean(this.model.get('filter_module_name')); if (data.name) { model.set('filter_row_name', data.name); } // Create the field selector enum and add it to the dom field = this.createField(model, { name: 'filter_row_name', type: 'enum', options: this.filterFields, defaultToBlank: true }); field.render(); $row.find('[data-filter=field]').append(field.$el); // Store the field in the data attributes. $row.data('nameField', field); // If this selector has a value, init the operator field as well if (data.name) { this.initOperatorField($row); } return $row; }, /** * Initializes the operator field. * * @param {jQuery} $row The related filter row. */ initOperatorField: function($row) { var $fieldWrapper = $row.find('[data-filter=operator]'); var data = $row.data(); var fieldName = data.nameField.model.get('filter_row_name'); var previousOperator = data.operator; // Make sure the data attributes contain the right selected field. data.name = fieldName; if (!fieldName) { return; } // For relate fields data.id_name = this.fieldList[fieldName].id_name; // For flex-relate fields data.type_name = this.fieldList[fieldName].type_name; //Predefined filters don't need operators and value field if (this.fieldList[fieldName].predefined_filter === true) { data.isPredefinedFilter = true; return; } // Get operators for this filter type var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var payload = {}; var types = _.keys(this.filterOperatorMap[fieldType]); // For parent field with the operator '$equals', the operator field is // hidden and we need to display the value field directly. So here we // need to assign 'previousOperator' and 'data.operator variables' to let // the value field initialize. //FIXME: We shouldn't have a condition on the parent field. TY-352 will // fix it. if (fieldType === 'parent' && _.isEqual(types, ['$equals'])) { previousOperator = data.operator = types[0]; } fieldType === 'parent' ? $fieldWrapper.addClass('hide').empty() : $fieldWrapper.removeClass('hide').empty(); $row.find('[data-filter=value]').addClass('hide').empty(); _.each(types, function(operand) { payload[operand] = app.lang.get( this.filterOperatorMap[fieldType][operand], [this.moduleName, 'Filters'] ); }, this); // Render the operator field var model = app.data.createBean(this.moduleName); if (previousOperator) { model.set('filter_row_operator', data.operator === '$dateRange' ? data.value : data.operator); } var field = this.createField(model, { name: 'filter_row_operator', type: 'enum', // minimumResultsForSearch set to 9999 to hide the search field, // See: https://github.com/ivaynberg/select2/issues/414 searchBarThreshold: 9999, options: payload, defaultToBlank: true }); field.render(); $fieldWrapper.append(field.$el); data.operatorField = field; var hide = fieldType === 'parent'; this._hideOperator(hide, $row); // We want to go into 'initValueField' only if the field value is known. // We need to check 'previousOperator' instead of 'data.operator' // because even if the default operator has been set, the field would // have set 'data.operator' when it rendered anyway. if (previousOperator) { this.initValueField($row); } }, /** * Initializes the value field. * * @param {jQuery} $row The related filter row. */ initValueField: function($row) { var self = this; var data = $row.data(); var operation = data.operatorField.model.get('filter_row_operator'); // Make sure the data attributes contain the right operator selected. data.operator = operation; if (!operation) { return; } if (_.contains(this._operatorsWithNoValues, operation)) { return; } // Patching fields metadata var moduleName = this.model.get('filter_module_name'); var fields = this.fieldList; // More patch for some field types var fieldName = $row.find('[data-filter=field] input[type=hidden]').select2('val'); var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var fieldDef = Object.assign({}, fields[fieldName]); switch (fieldType) { case 'enum': fieldDef.isMultiSelect = this.isCollectiveValue($row); // Set minimumResultsForSearch to a negative value to hide the search field, // See: https://github.com/ivaynberg/select2/issues/489#issuecomment-13535459 fieldDef.searchBarThreshold = -1; fieldDef.defaultToBlank = true; break; case 'bool': fieldDef.type = 'enum'; fieldDef.options = fieldDef.options || 'filter_checkbox_dom'; fieldDef.defaultToBlank = true; break; case 'int': fieldDef.auto_increment = false; //For $in operator, we need to convert `['1','20','35']` to `1,20,35` to make it work in a varchar field if (operation === '$in') { fieldDef.type = 'varchar'; fieldDef.len = 200; if (_.isArray($row.data('value'))) { $row.attr('data-value', $row.data('value').join(',')); } } break; case 'teamset': fieldDef.type = 'relate'; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'datetimecombo': case 'date': fieldDef.type = _.includes(this.amountDaysOperators, operation) ? 'text' : 'date'; //Flag to indicate the value needs to be formatted correctly data.isDate = true; if (operation.charAt(0) !== '$') { //Flag to indicate we need to build the date filter definition based on the date operator data.isDateRange = true; return; } break; case 'relate': fieldDef.auto_populate = true; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'parent': data.isFlexRelate = true; break; } fieldDef.required = false; fieldDef.readonly = false; // Create new model with the value set var model = app.data.createBean(moduleName); var $fieldValue = $row.find('[data-filter=value]'); $fieldValue.removeClass('hide').empty(); // Add the field type as an attribute on the HTML element so that it // can be used as a CSS selector. $fieldValue.attr('data-type', fieldType); //fire the change event as soon as the user start typing var _keyUpCallback = function(e) { if ($(e.currentTarget).is('.select2-input')) { return; //Skip select2. Select2 triggers other events. } this.value = $(e.currentTarget).val(); // We use "silent" update because we don't need re-render the field. model.set(this.name, this.unformat($(e.currentTarget).val()), {silent: true}); model.trigger('change'); }; //If the operation is $between we need to set two inputs. if (operation === '$between' || operation === '$dateBetween') { var minmax = []; var value = $row.data('value') || []; if (fieldType === 'currency' && $row.data('value')) { value = $row.data('value') || {}; model.set(value); value = value[fieldName] || []; // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). model.set('id', 'not_new'); } model.set(fieldName + '_min', value[0] || ''); model.set(fieldName + '_max', value[1] || ''); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_min'}))); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_max'}))); if (operation === '$dateBetween') { minmax[0].label = app.lang.get('LBL_FILTER_DATEBETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_DATEBETWEEN_TO'); } else { minmax[0].label = app.lang.get('LBL_FILTER_BETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_BETWEEN_TO'); } data.valueField = minmax; _.each(minmax, function(field) { $fieldValue.append(field.$el); this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); field.$('.input-append').prepend('<span class="add-on">' + field.label + '</span>') .addClass('input-prepend') .removeClass('date'); // .date makes .inherit-width on input have no effect field.$('input, textarea').on('keyup', _.debounce(_.bind(_keyUpCallback, field), 400)); }); field.render(); }, this); } else if (data.isFlexRelate) { var values = {}; _.each($row.data('value'), function(value, key) { values[key] = value; }, this); model.set(values); var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); findRelatedName = app.data.createBeanCollection(model.get('parent_type')); data.valueField = field; $fieldValue.append(field.$el); if (model.get('parent_id')) { findRelatedName.fetch({ params: {filter: [{'id': model.get('parent_id')}]}, complete: _.bind(function() { if (!this.disposed) { if (findRelatedName.first()) { model.set(fieldName, findRelatedName.first().get(field.getRelatedModuleField()), {silent: true}); } if (!field.disposed) { field.render(); } } }, this) }); } else { field.render(); } } else { // value is either an empty object OR an object containing `currency_id` and currency amount if (fieldType === 'currency' && $row.data('value')) { // for stickiness & to retrieve correct saved values, we need to set the model with data.value object model.set($row.data('value')); // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). // Mark this one as not_new so that model isn't treated as new model.set('id', 'not_new'); } else { model.set(fieldDef.id_name || fieldName, $row.data('value')); } // Render the value field var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); $fieldValue.append(field.$el); data.valueField = field; this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); // .date makes .inherit-width on input have no effect so we need to remove it. field.$('.input-append').removeClass('date'); field.$('input, textarea').on('keyup',_.debounce(_.bind(_keyUpCallback, field), 400)); }); if ((fieldDef.type === 'relate' || fieldDef.type === 'nestedset') && !_.isEmpty($row.data('value')) ) { var findRelatedName = app.data.createBeanCollection(fieldDef.module); var relateOperator = this.isCollectiveValue($row) ? '$in' : '$equals'; var relateFilter = [{id: {}}]; relateFilter[0].id[relateOperator] = $row.data('value'); findRelatedName.fetch({fields: [fieldDef.rname], params: {filter: relateFilter}, complete: function() { if (!self.disposed) { if (findRelatedName.length > 0) { model.set(fieldDef.id_name, findRelatedName.pluck('id'), {silent: true}); model.set(fieldName, findRelatedName.pluck(fieldDef.rname), {silent: true}); } if (!field.disposed) { field.render(); } } } }); } else { field.render(); } } // When the value changes, update the filter value var updateFilter = function() { self._updateFilterData($row); self.model.set('filter_def', self.buildFilterDef(true), {silent: true}); }; this.listenTo(model, 'change', updateFilter); this.listenTo(model, 'change:' + fieldName, updateFilter); // Manually trigger the filter request if a value has been selected lately // This is the case for checkbox fields or enum fields that don't have empty values. var modelValue = model.get(fieldDef.id_name || fieldName); // To handle case: value is an object with 'currency_id' = 'xyz' and 'likely_case' = '' // For currency fields, when value becomes an object, trigger change if (!_.isEmpty(modelValue) && modelValue !== $row.data('value')) { model.trigger('change'); } }, /** * Check if the selected filter operator is a collective type. * * @param {jQuery} $row The related filter row. */ isCollectiveValue: function($row) { return $row.data('operator') === '$in' || $row.data('operator') === '$not_in'; }, /** * Update filter data for this row * @param $row Row to update * @private */ _updateFilterData: function($row) { var data = $row.data(); var field = data.valueField; var name = data.name; var valueForFilter; //Make sure we use ID for relate fields if (this.fieldList[name] && this.fieldList[name].id_name) { name = this.fieldList[name].id_name; } //If we have multiple fields we have to build an array of values if (_.isArray(field)) { valueForFilter = []; _.each(field, function(field) { var value = !field.disposed && field.model.has(field.name) ? field.model.get(field.name) : ''; value = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; valueForFilter.push(value); }); } else { var value = !field.disposed && field.model.has(name) ? field.model.get(name) : ''; valueForFilter = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; } // Update filter value once we've calculated final value $row.data('value', valueForFilter); $row.attr('data-value', valueForFilter); }, /** * Shows or hides the operator field of the filter row specified. * * Automatically populates the operator field to have value `$equals` if it * is not in midst of populating the row. * * @param {boolean} hide Set to `true` to hide the operator field. * @param {jQuery} $row The filter row of interest. * @private */ _hideOperator: function(hide, $row) { $row.find('[data-filter=value]') .toggleClass('span4', !hide) .toggleClass('span8', hide); }, /** * Utility function that instantiates a field for this form. * * The field action is manually set to `detail` because we want to render * the `edit` template but the action remains `detail` (filtering). * * @param {Data.Bean} model A bean necessary to the field for storing the * value(s). * @param {Object} def The field definition. * @return {View.Field} The field component. */ createField: function(model, def) { var obj = { def: def, view: this.view, nested: true, viewName: 'edit', model: model }; var field = app.view.createField(obj); return field; }, /** * Fired when a user selects a field to filter by * @param {Event} e */ handleFieldSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {field: 'operatorField', value: 'operator'}, {field: 'valueField', value: 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initOperatorField($row); // Update the attributes of the row $row.attr('data-name', $el.val()); $row.attr('data-operator', ''); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Fired when a user selects an operator to filter by * @param {Event} e */ handleOperatorSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {'field': 'valueField', 'value': 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initValueField($row); // Update the attributes of the row $row.attr('data-operator', $el.val()); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Disposes fields stored in the data attributes of the row element. * * @example of an `opts` object param: * [ * {field: 'nameField', value: 'name'}, * {field: 'operatorField', value: 'operator'}, * {field: 'valueField', value: 'value'} * ] * * @param {jQuery} $row The row which fields are to be disposed. * @param {Array} opts An array of objects containing the field object and * value to the data attributes of the row. */ _disposeRowFields: function($row, opts) { var data = $row.data(); var model; if (_.isObject(data) && _.isArray(opts)) { _.each(opts, function(val) { if (data[val.field]) { //For in between filter we have an array of fields so we need to cover all cases var fields = _.isArray(data[val.field]) ? data[val.field] : [data[val.field]]; data[val.value] = ''; _.each(fields, function(field) { model = field.model; if (val.field === 'valueField' && model) { model.clear({silent: true}); this.stopListening(model); } field.dispose(); field = null; }, this); return; } if (data.isDateRange && val.value === 'value') { data.value = ''; } }, this); } //Reset flags data.isDate = false; data.isDateRange = false; data.isPredefinedFilter = false; data.isFlexRelate = false; $row.data(data); }, /** * Build filter definition for all rows. * * @param {boolean} onlyValidRows Set `true` to retrieve only filter * definition of valid rows, `false` to retrieve the entire field * template. * @return {string} Filter definition. */ buildFilterDef: function(onlyValidRows) { var $rows = this.$('[data-filter=row]'); var filter = []; _.each($rows, function(row) { var rowFilter = this.buildRowFilterDef($(row), onlyValidRows); if (rowFilter) { filter.push(rowFilter); } }, this); // If the filter definition is empty array then set it to undefined return _.isEmpty(filter) ? undefined : JSON.stringify(filter); }, /** * Build filter definition for this row. * * @param {jQuery} $row The related row. * @param {boolean} onlyIfValid Set `true` to validate the row and return * `undefined` if not valid, or `false` to build the definition anyway. * @return {Object} Filter definition for this row. */ buildRowFilterDef: function($row, onlyIfValid) { var data = $row.data(); if (onlyIfValid && !this.validateRow($row)) { return; } var operator = data.operator; var value = data.value || ''; var name = data.id_name || data.name; var filter = {}; if (_.isEmpty(name)) { return; } if (data.isPredefinedFilter || !this.fieldList) { filter[name] = ''; return filter; } else { if (!_.isEmpty(data.valueField) && _.isFunction(data.valueField.delegateBuildFilterDefinition)) { filter[name] = {}; filter[name][operator] = data.valueField.delegateBuildFilterDefinition(); } else if (this.fieldList[name] && _.has(this.fieldList[name], 'dbFields')) { var subfilters = []; _.each(this.fieldList[name].dbFields, function(dbField) { var filter = {}; filter[dbField] = {}; filter[dbField][operator] = value; subfilters.push(filter); }); filter.$or = subfilters; } else { if (data.isFlexRelate) { var valueField = data.valueField; var idFilter = {}; var typeFilter = {}; idFilter[data.id_name] = valueField.model.get(data.id_name); typeFilter[data.type_name] = valueField.model.get(data.type_name); filter.$and = [idFilter, typeFilter]; // Creating currency filter. For all but `$between` operators we use // type property from data.valueField. For `$between`, data.valueField // is an array and therefore we check for type==='currency' from // either of the elements. } else if (data.valueField && (data.valueField.type === 'currency' || (_.isArray(data.valueField) && data.valueField[0].type === 'currency')) ) { // initially value is an array which we later convert into an object for saving and retrieving // purposes (stickiness structure constraints) var amountValue; if (_.isObject(value) && !_.isUndefined(value[name])) { amountValue = value[name]; } else { amountValue = value; } var amountFilter = {}; amountFilter[name] = {}; amountFilter[name][operator] = amountValue; // for `$between`, we use first element to get dataField ('currency_id') since it is same // for both elements and also because data.valueField is an array var dataField; if (_.isArray(data.valueField)) { dataField = data.valueField[0]; } else { dataField = data.valueField; } var currencyId; currencyId = dataField.getCurrencyField().name; var currencyFilter = {}; currencyFilter[currencyId] = dataField.model.get(currencyId); filter.$and = [amountFilter, currencyFilter]; } else if (data.isDateRange) { //Once here the value is actually a key of date_range_selector_dom and we need to build a real //filter definition on it. filter[name] = {}; filter[name].$dateRange = operator; } else if (operator === '$in' || operator === '$not_in') { // IN/NOT IN require an array filter[name] = {}; //If value is not an array, we split the string by commas to make it an array of values if (_.isArray(value)) { filter[name][operator] = value; } else if (!_.isEmpty(value)) { filter[name][operator] = (value + '').split(','); } else { filter[name][operator] = []; } } else { filter[name] = {}; filter[name][operator] = value; } } return filter; } }, /** * Verify the value of the row is not empty. * * @param {Element} $row The row to validate. * @return {boolean} `true` if valid, `false` otherwise. */ validateRow: function(row) { var $row = $(row); var data = $row.data(); if (_.contains(this._operatorsWithNoValues, data.operator)) { return true; } // for empty value in currency we dont want to validate if (!_.isUndefined(data.valueField) && !_.isArray(data.valueField) && data.valueField.type === 'currency' && (_.isEmpty(data.value) || (_.isObject(data.value) && _.isEmpty(data.valueField.model.get(data.name))))) { return false; } //For date range and predefined filters there is no value if (data.isDateRange || data.isPredefinedFilter) { return true; } else if (data.isFlexRelate) { return data.value ? _.reduce(data.value, function(memo, val) { return memo && !_.isEmpty(val); }, true) : false; } //Special case for between operators where 2 values are needed if (_.contains(['$between', '$dateBetween'], data.operator)) { if (!_.isArray(data.value) || data.value.length !== 2) { return false; } switch (data.operator) { case '$between': // FIXME: the fields should set a true number (see SC-3138). return !(_.isNaN(parseFloat(data.value[0])) || _.isNaN(parseFloat(data.value[1]))); case '$dateBetween': return !_.isEmpty(data.value[0]) && !_.isEmpty(data.value[1]); default: return false; } } return _.isNumber(data.value) || !_.isEmpty(data.value); }, }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataArchiver.RecordView * @alias SUGAR.App.view.views.BaseDataArchiverRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', moduleRequirements: { 'pmse_Inbox': [ 'cas_status', ], }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [name="perform_button"]': 'performClicked', }); }, /** * Function that defines behavior when the Archive Now button is clicked on the record view */ performClicked: function() { if (this.disposed) { return; } var self = this; var url = app.api.buildURL('DataArchiver/' + this.model.id + '/run', null, null, null); var data = this.model.attributes; app.alert.dismissAll(); app.alert.show('delete_warning', { level: 'confirmation', title: app.lang.get('LBL_ARCHIVER_WARNING_TITLE', 'DataArchiver'), messages: app.lang.get('LBL_ARCHIVER_WARNING', 'DataArchiver'), autoclose: false, onConfirm: function() { self.disposed = true; app.api.call('create', url, {}, { success: function(results) { app.alert.show('success', { level: 'success', autoClose: true, autoCloseDelay: 10000, title: app.lang.get('LBL_ARCHIVE_SUCCESS_TITLE', 'DataArchiver') + ':', messages: data.process_type === 'archive' ? app.lang.get('LBL_ARCHIVE_SUCCESS', 'DataArchiver') : app.lang.get('LBL_DELETE_SUCCESS', 'DataArchiver') }); self.layout.trigger('subpanel_refresh'); }, error: function(e) { if (e.code === 'ModuleReqError') { self.showModuleRequirementsError(e.message, self.model.get('filter_module_name')); } else { app.alert.show('error', { level: 'error', title: app.lang.get('LBL_ARCHIVE_ERROR', 'DataArchiver') + ':', messages: ['ERR_HTTP_500_TEXT_LINE1', 'ERR_HTTP_500_TEXT_LINE2'] }); } }, complete: function() { self.disposed = false; } }); } }); }, /** * @override */ saveClicked: function() { let canSave = true; const filterModule = this.model.get('filter_module_name'); if (this.model.get('filter_def') && filterModule in this.moduleRequirements) { const filters = JSON.parse(this.model.get('filter_def')).map(f => Object.keys(f)[0]); const reqsNotMet = this.moduleRequirements[filterModule].filter(f => !filters.includes(f)); if (reqsNotMet.length > 0) { // If there are many reqs not met, just show the first in error me this.showModuleRequirementsError(reqsNotMet[0], filterModule); canSave = false; } } canSave && this._super('saveClicked'); }, /** * Show the error message that will occur when module requirements are not met. Will only show the first of any * requirements not met */ showModuleRequirementsError(field, module) { app.alert.dismissAll(); const fieldName = app.lang.get(app.metadata.getModule(module).fields[field].vname, module) || field; const args = {fieldName: fieldName, moduleName: module}; app.alert.show('req_not_met', { level: 'error', messages: app.lang.get('TPL_PMSE_INBOX_ERROR_MESSAGE', this.module, args), autoClose: true, autoCloseDelay: 5000 }); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataArchiver.CreateView * @alias SUGAR.App.view.views.DataArchiverCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', moduleRequirements: { 'pmse_Inbox': [ 'cas_status', ], }, /** * @override */ save() { let canSave = true; const filterModule = this.model.get('filter_module_name'); if (this.model.get('filter_def') && filterModule in this.moduleRequirements) { const filters = JSON.parse(this.model.get('filter_def')).map(f => Object.keys(f)[0]); const reqsNotMet = this.moduleRequirements[filterModule].filter(f => !filters.includes(f)); if (reqsNotMet.length > 0) { // If there are many reqs not met, just show the first in error me this.showModuleRequirementsError(reqsNotMet[0], filterModule); canSave = false; } } canSave && this._super('save'); }, /** * Show the error message that will occur when module requirements are not met. Will only show the first of any * requirements not met */ showModuleRequirementsError(field, module) { app.alert.dismissAll(); const fieldName = app.lang.get(app.metadata.getModule(module).fields[field].vname, module) || field; const args = {fieldName: fieldName, moduleName: module}; app.alert.show('req_not_met', { level: 'error', messages: app.lang.get('TPL_PMSE_INBOX_ERROR_MESSAGE', this.module, args), autoClose: true, autoCloseDelay: 5000 }); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ArchiveRuns":{"fieldTemplates": { "base": { "filter-def": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ArchiveRunsFilterdefField * @alias SUGAR.App.view.fields.BaseArchiveRunsFilterdefField * @extends View.Fields.Base.FilterDefField */ ({ // Filter-def FieldTemplate (base) /** * @param {Object} options * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // For this instance of the filter-def field, we want the module used to be the source_module field this.module = this.model.get('source_module'); } }) } }} , "views": { "base": { "subpanel-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ArchiveRunsSubpanelListView * @alias SUGAR.App.view.views.BaseArchiveRunsSubpanelListView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-list View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this view is re-rendered * when the config is reset */ bindDataChange: function() { this._super('bindDataChange'); var component = this.closestComponent('main-pane'); if (component) { component.on('subpanel_refresh', function() { this.refreshCollection(); }, this); } }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "OAuthKeys":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "OAuthTokens":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Filters":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": { "base": { "collection": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * This `FiltersCollection` is designed to be used like: * * filters = app.data.createBeanCollection('Filters'); * filters.setModuleName('Accounts'); * filters.setFilterOptions(filterOptions); // Optional * filters.load({ // Collection Data (base) * success: _.bind(function() { * // You can start using `filters.collection` * }, this) * }); * * Even though {@link #load} and {@link #collection} are the recommended way to * manipulate the filters of a module, you can still use this `BeanCollection` * in a more traditional way. * * filters = app.data.createBeanCollection('Filters'); * filters.fetch({ * filter: [ * {'created_by': app.user.id}, * {'module_name': module} * ], * success: _.bind(function() { * // You can start using the collection * }, this) * }); * * **Note** that in this case you're not taking advantage of the internal cache * memory. * * @class Data.Base.FiltersBeanCollection * @extends Data.BeanCollection */ ({ /** * This is the public and recommended API for manipulating the collection * of filters of a module. * * {@link #load} will create this collection provided that you set the * module name with {@link #setModuleName}. * * @property {Backbone.Collection|null} */ collection: null, /** * The filter options used by {@link #load} when retrieving and building the * filter collection. * * See {@link #setFilterOptions}. * * @property {Object} * @private */ _filterOptions: {}, /** * Clears the filter options for the next call to {@link #load}. * * @chainable */ clearFilterOptions: function() { this._filterOptions = {}; return this; }, /** * Maintains the filters collection in sorted order. * * User's filters will be positioned first in the collection, the * predefined filters will be positioned second. * * @param {Data.Bean} model1 The first model. * @param {Data.Bean} model2 The second model. * @return {Number} If `-1`, the first model goes before the second model, * if `1`, the first model goes after the second model. */ comparator: function(model1, model2) { if (model1.get('editable') === false && model2.get('editable') !== false) { return 1; } if (model1.get('editable') !== false && model2.get('editable') === false) { return -1; } if (this._getTranslatedFilterName(model1).toLowerCase() < this._getTranslatedFilterName(model2).toLowerCase()) { return -1; } return 1; }, /** * Retrieves the list of filters of a module. * * The list includes predefined filters (defined in the metadata) as well as * the current user's filters. * * The collection is saved in memory the first time filters are retrieved, * so next calls to {@link #load} will just return the cached version. * * **Note:** Template filters are retrieved and saved in memory but are not * in the collection unless you pass the `initial_filter` options. See * {@link #setFilterOptions}. Only one template filter can be available at * a time. * * @param {Object} [options] * @param {Function} [options.success] Callback function to execute code * once the filters are successfully retrieved. * @param {Function} [options.error] Callback function to execute code * in case an error occured when retrieving filters. */ load: function(options) { options = options || {}; var module = this.moduleName, prototype = this._getPrototype(), collection; if (!module) { app.logger.error('This Filters collection has no module defined.'); return; } if (this.collection) { this.collection.off(); } // Make sure only one request is sent for each module. prototype._request = prototype._request || {}; if (prototype._request[module]) { prototype._request[module].xhr.done(_.bind(function() { this._onSuccessCallback(options.success); }, this)); return; } // Try to retrieve cached filters. prototype._cache = prototype._cache || {}; if (prototype._cache[module]) { this._onSuccessCallback(options.success); return; } this._initFiltersModuleCache(); // No cache found, retrieve filters. this._loadPredefinedFilters(); var requestObj = { showAlerts: false, filter: [ {'created_by': app.user.id}, {'module_name': module} ], success: _.bind(function(models) { this._cacheFilters(models); this._onSuccessCallback(options.success); }, this), complete: function() { delete prototype._request[module]; }, error: function() { if (_.isFunction(options.error)) { options.error(); } else { app.logger.error('Unable to get filters from the server.'); } } }; prototype._request[module] = prototype.fetch.call(this, requestObj); }, /** * Defines the module name of the filter collection. This is mandatory in * order to use {@link #load}. * * @param {String} module The module name. * @chainable */ setModuleName: function(module) { this.moduleName = module; return this; }, /** * Defines the filter options used by {@link #load}. * * **Options supported:** * * - `{String} [initial_filter]` The id of the template filter. * * - `{String} [initial_filter_lang_modules]` The list of modules to look up * the filter label string. * * - `{String} [filter_populate]` The populate hash in case we want to * create a relate template filter on the fly. * * Filter options can be cleared with {@link #clearFilterOptions}. * * @param {String|Object} key The name of the option, or an hash of * options. * @param {Mixed} [val] The default value for the `key` argument. * @chainable */ setFilterOptions: function(key, val) { var options; if (_.isObject(key)) { options = key; } else { (options = {})[key] = val; } this._filterOptions = _.extend({}, this._filterOptions, options); return this; }, /** * Saves the list of filters in memory. * * This allows us not to parse the metadata everytime in order to get the * predefined and template filters, and not to fetch the API everytime in * order to get the user's filters. * * @param {Mixed} models A list of filters (`Backbone.Collection` or * `Object[]`) or one filter (`Data.Base.FiltersBean` or `Object`). * @private */ _cacheFilters: function(models) { if (!models) { return; } var filters = _.isFunction(models.toJSON) ? models.toJSON() : models; filters = _.isArray(filters) ? filters : [filters]; var prototype = this._getPrototype(); _.each(filters, function(filter) { if (filter.editable === false) { prototype._cache[this.moduleName].predefined[filter.id] = filter; } else if (filter.is_template) { prototype._cache[this.moduleName].template[filter.id] = filter; } else { prototype._cache[this.moduleName].user[filter.id] = filter; } }, this); }, /** * Create the collection of filters. * * The collection contains predefined filters and the current user's * filters. * * @return {Backbone.Collection} The collection of filters. * @private */ _createCachedCollection: function() { var prototype = app.data.getCollectionClasses().Filters.prototype, module = this.moduleName, collection; // Creating the collection class. prototype._cachedCollection = prototype._cachedCollection || Backbone.Collection.extend({ model: app.data.getBeanClass('Filters'), _setInitialFilter: this._setInitialFilter, comparator: this.comparator, _getPrototype: this._getPrototype, _getTranslatedFilterName: this._getTranslatedFilterName, _cacheFilters: this._cacheFilters, _updateFilterCache: this._updateFilterCache, _removeFilterCache: this._removeFilterCache, initialize: function(models, options) { this.on('add', this._cacheFilters, this); this.on('cache:update', this._updateFilterCache, this); this.on('remove', this._removeFilterCache, this); } }); collection = new prototype._cachedCollection(); collection.moduleName = module; collection._filterOptions = this._filterOptions; collection.defaultFilterFromMeta = prototype._cache[module].defaultFilterFromMeta; // Important to pass silent `true` to avoid saving in memory again. collection.add(_.toArray(prototype._cache[module].predefined), {silent: true}); collection.add(_.toArray(prototype._cache[module].user), {silent: true}); return collection; }, /** * Gets the translated name of a filter. * * If the model is not editable or is a template, the filter name must be * defined as a label that is internationalized. * We allow injecting the translated module name into filter names. * * @param {Data.Bean} model The filter model. * @return {String} The translated filter name. * @private */ _getTranslatedFilterName: function(model) { var name = model.get('name') || ''; if (model.get('editable') !== false && !model.get('is_template')) { return name; } var module = model.get('module_name') || this.moduleName; var fallbackLangModules = model.langModules || [module, 'Filters']; var moduleName = app.lang.getModuleName(module, {plural: true}); var text = app.lang.get(name, fallbackLangModules) || ''; return app.utils.formatString(text, [moduleName]); }, /** * Loads predefined filters from metadata and stores them in memory. * * Also determines the default filter. The default filter will be the last * `default_filter` property found in the filters metadata. * * @private */ _loadPredefinedFilters: function() { var cache = this._getPrototype()._cache[this.moduleName], moduleMeta = app.metadata.getModule(this.moduleName); if (!moduleMeta) { app.logger.error('The module "' + this.moduleName + '" has no metadata.'); return; } var moduleFilterMeta = moduleMeta.filters; if (!moduleFilterMeta) { app.logger.error('The module "' + this.moduleName + '" has no filter metadata.'); return; } _.each(moduleFilterMeta, function(template) { if (!template || !template.meta) { return; } if (_.isArray(template.meta.filters)) { this._cacheFilters(template.meta.filters); } if (template.meta.default_filter) { cache.defaultFilterFromMeta = template.meta.default_filter; } }, this); }, /** * Success callback applied once filters are retrieved in order to prepare * the bean collection. * * @param {Function} [callback] Custom success callback. The collection is * readily available as the first argument to this callback function. * @private */ _onSuccessCallback: function(callback) { this.collection = this._createCachedCollection(); if (this._filterOptions.initial_filter) { this.collection._setInitialFilter(); } if (_.isFunction(callback)) { callback(this.collection); } }, /** * Sets an initial/template filter to the collection. * * Filter options: * * If the `initial_filter` id is `$relate`, a new filter will be created for * you, and will be populated by `filter_populate` definition. * * If you pass any other `initial_filter` id, the function will look up for * this template filter in memory and create it. * * @private */ _setInitialFilter: function() { var filterId = this._filterOptions.initial_filter; if (!filterId) { return; } if (filterId === '$relate') { var filterDef = {}; _.each(this._filterOptions.filter_populate, function(value, key) { filterDef[key] = ''; }); this.add([ { 'id': '$relate', 'editable': true, 'is_template': true, 'filter_definition': [filterDef] } ], {silent: true}); } else { var prototype = this._getPrototype(); var filter = prototype._cache[this.moduleName].template[filterId]; if (!filter) { return; } this.add(filter, {silent: true}); } this.get(filterId).set('name', this._filterOptions.initial_filter_label); this.get(filterId).langModules = this._filterOptions.initial_filter_lang_modules; }, /** * Saves the list of filters in memory. * * Only user's filters are refreshed. We want to ignore changes to template * filters and predefined filters. * * @param {Data.Base.FiltersBean|Object} model The filter model to update in * memory. * @param {String} model.id The filter id. * @private */ _updateFilterCache: function(model) { if (!model) { return; } var attributes = _.isFunction(model.toJSON) ? model.toJSON() : model; if (attributes.is_template || attributes.editable === false) { return; } this._cacheFilters(model); }, /** * Removes a filter stored in memory. * * @param {Data.Base.FiltersBean|Object} model The filter model to remove * from memory. * @param {String} model.id The filter id. * @private */ _removeFilterCache: function(model) { var prototype = this._getPrototype(); delete prototype._cache[this.moduleName].predefined[model.id]; delete prototype._cache[this.moduleName].template[model.id]; delete prototype._cache[this.moduleName].user[model.id]; }, /** * Initializes the filter cache for this module. * * @private */ _initFiltersModuleCache: function() { var prototype = this._getPrototype(); prototype._cache = prototype._cache || {}; prototype._cache[this.moduleName] = { defaultFilterFromMeta: null, predefined: {}, template: {}, user: {} }; }, /** * Clears all the filters and their associated HTTP requests from the cache. */ resetFiltersCacheAndRequests: function() { var prototype = this._getPrototype(); prototype._cache = {}; _.each(prototype._request, function(request, module) { request.xhr.abort(); }); prototype._request = {}; }, /** * Gets the prototype object of this class. * * @return {Object} The prototype. * @private */ _getPrototype: function() { return app.data.getCollectionClasses().Filters.prototype; }, /** * Removes all the listeners. */ dispose: function() { if (this.collection) { this.collection.off(); } this.off(); } }) }, "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Data.Base.FiltersBean * @extends Data.Bean */ ({ // Model Data (base) /** * @inheritdoc */ defaults: { editable: true }, /** * Maps field types and field operator types. * * @property {Object} */ fieldTypeMap: { 'datetime': 'date', 'datetimecombo': 'date' }, /** * Gets the filter definition based on quick search metadata. * * The filter definition that is built is based on the `basic` filter * metadata. By default, modules will make a search on the `name` field, but * this is configurable. For instance, the `person` type modules * (e.g. Contacts or Leads) will perform a search on the first name and the * last name (`first_name` and `last_name` fields). * * For these modules whom the search is performed on two fields, you can * also configure to split the terms. In this case, the terms are split such * that different combinations of the terms are search against each search * field. * * There is a special case if the `moduleName` is `all_modules`: the * function will always return an empty filter definition (empty `array`). * * There is another special case with the `Users` and `Employees` module: * the filter will be augmented to retrieve only the records with the * `status` set to `Active`. * * @param {string} moduleName The filtered module. * @param {string} searchTerm The search term. * @param {string} operation To check any match instead of starts with * @return {Array} This search term filter. * @static */ buildSearchTermFilter: function(moduleName, searchTerm, operation) { if (moduleName === 'all_modules' || !searchTerm) { return []; } searchTerm = searchTerm.trim(); var splitTermFilter; var filterList = []; var searchMeta = app.data.getBeanClass('Filters').prototype.getModuleQuickSearchMeta(moduleName); var fieldNames = searchMeta.fieldNames; // Iterate through each field and check if the field is a simple // or complex field, and build the filter object accordingly _.each(fieldNames, function(name) { if (!_.isArray(name)) { var filter = this._buildFilterDef(name, operation || '$starts', searchTerm); if (filter) { // Simple filters are pushed to `filterList` filterList.push(filter); } return; } if (splitTermFilter) { app.logger.error('Cannot have more than 1 split term filter'); return; } splitTermFilter = this._buildSplitTermFilterDef(name, '$starts', searchTerm); }, this); // Push the split term filter if (splitTermFilter) { filterList.push(splitTermFilter); } // If more than 1 filter was created, wrap them in `$or` if (filterList.length > 1) { var filter = this._joinFilterDefs('$or', filterList); if (filter) { filterList = [filter]; } } // FIXME [SC-3560]: This should be moved to the metadata if (moduleName === 'Users' || moduleName === 'Employees') { filterList = this._simplifyFilterDef(filterList); filterList = [{ '$and': [ {'status': {'$not_equals': 'Inactive'}}, filterList ] }]; } return filterList; }, /** * Combines two filters into a single filter definition. * * @param {Array|Object} [baseFilter] The selected filter definition. * @param {Array} [searchTermFilter] The filter for the quick search terms. * @return {Array} The filter definition. * @static */ combineFilterDefinitions: function(baseFilter, searchTermFilter) { var isBaseFilter = _.size(baseFilter) > 0, isSearchTermFilter = _.size(searchTermFilter) > 0; baseFilter = _.isArray(baseFilter) ? baseFilter : [baseFilter]; if (isBaseFilter && isSearchTermFilter) { baseFilter.push(searchTermFilter[0]); return [ {'$and': baseFilter } ]; } else if (isBaseFilter) { return baseFilter; } else if (isSearchTermFilter) { return searchTermFilter; } return []; }, /** * Gets filterable fields from the module metadata. * * The list of fields comes from the metadata but is also filtered by * user acls (`detail`/`read` action). * * @param {string} moduleName The name of the module. * @return {Object} The filterable fields. * @static */ getFilterableFields: function(moduleName) { var moduleMeta = app.metadata.getModule(moduleName), operatorMap = app.metadata.getFilterOperators(moduleName), fieldMeta = moduleMeta.fields, fields = {}; if (moduleMeta.filters) { _.each(moduleMeta.filters, function(templateMeta) { if (templateMeta.meta && templateMeta.meta.fields) { fields = _.extend(fields, templateMeta.meta.fields); } }); } _.each(fields, function(fieldFilterDef, fieldName) { var fieldMetaData = app.utils.deepCopy(fieldMeta[fieldName]); if (_.isEmpty(fieldFilterDef)) { fields[fieldName] = fieldMetaData || {}; } else { fields[fieldName] = _.extend({name: fieldName}, fieldMetaData, fieldFilterDef); } delete fields[fieldName]['readonly']; }); var validFields = {}; _.each(fields, function(value, key) { // Check if we support this field type. var type = this.fieldTypeMap[value.type] || value.type; var hasAccess = app.acl.hasAccess('detail', moduleName, null, key); // Predefined filters don't have operators defined. if (hasAccess && (operatorMap[type] || value.predefined_filter === true)) { validFields[key] = value; } }, this); return validFields; }, /** * Retrieves and caches the quick search metadata. * * @param {string} [moduleName] The filtered module. Only required when the * function is called statically. * @return {Object} Quick search metadata (with highest priority). * @return {string[]} return.fieldNames The fields to be used in quick search. * @return {boolean} return.splitTerms Whether to split the search terms * when there are multiple search fields. * @static */ getModuleQuickSearchMeta: function(moduleName) { moduleName = moduleName || this.get('module_name'); var prototype = app.data.getBeanClass('Filters').prototype; prototype._moduleQuickSearchMeta = prototype._moduleQuickSearchMeta || {}; prototype._moduleQuickSearchMeta[moduleName] = prototype._moduleQuickSearchMeta[moduleName] || this._getQuickSearchMetaByPriority(moduleName); return prototype._moduleQuickSearchMeta[moduleName]; }, /** * Populates empty values of a filter definition. * * @param {Object} filterDef The filter definition. * @param {Object} populateObj Populate object containing the * `filter_populate` metadata definition. * @return {Object} The filter definition. * @static */ populateFilterDefinition: function(filterDef, populateObj) { if (!populateObj) { return filterDef; } filterDef = app.utils.deepCopy(filterDef); _.each(filterDef, function(row) { _.each(row, function(filter, field) { var hasNoOperator = (_.isString(filter) || _.isNumber(filter)); if (hasNoOperator) { filter = {'$equals': filter}; } var operator = _.keys(filter)[0], value = filter[operator], isValueEmpty = !_.isNumber(value) && _.isEmpty(value); if (isValueEmpty && populateObj && !_.isUndefined(populateObj[field])) { value = populateObj[field]; } if (hasNoOperator) { row[field] = value; } else { row[field][operator] = value; } }); }); return filterDef; }, /** * Retrieves the quick search metadata. * * The metadata returned is the one that has the highest * `quicksearch_priority` property. * * @param {string} searchModule The filtered module. * @return {Object} * @return {string[]} return.fieldNames The list of field names. * @return {boolean} return.splitTerms Whether to split search terms or not. * @private * @static */ _getQuickSearchMetaByPriority: function(searchModule) { var meta = app.metadata.getModule(searchModule), filters = meta ? meta.filters : [], fieldNames = [], priority = 0, splitTerms = false; _.each(filters, function(value) { if (value && value.meta && value.meta.quicksearch_field && priority < value.meta.quicksearch_priority) { fieldNames = value.meta.quicksearch_field; priority = value.meta.quicksearch_priority; if (_.isBoolean(value.meta.quicksearch_split_terms)) { splitTerms = value.meta.quicksearch_split_terms; } } }); return { fieldNames: fieldNames, splitTerms: splitTerms }; }, /** * Returns the first filter from `filterList`, if the length of * `filterList` is 1. * * The *simplified* filter is in the form of the one returned by * {@link #_buildFilterDef} or {@link #_joinFilterDefs}. * * @param {Array} filterList An array of filter definitions. * * @return {Array|Object} First element of `filterList`, if the * length of the array is 1, otherwise, the original `filterList`. * @private */ _simplifyFilterDef: function(filterList) { return filterList.length > 1 ? filterList : filterList[0]; }, /** * Builds a filter definition object. * * A filter definition object is in the form of: * * { fieldName: { operator: searchTerm } } * * @param {string} fieldName Name of the field to search by. * @param {string} operator Operator to search by. As found in `FilterApi#addFilters`. * @param {string} searchTerm Search input entered. * * @return {Object} The search filter definition for quick search. * @private */ _buildFilterDef: function(fieldName, operator, searchTerm) { var def = {}; var filter = {}; filter[operator] = searchTerm; def[fieldName] = filter; return def; }, /** * Joins a list of filter definitions under a logical operator. * * Supports logical operators such as `$or` and `$and`. Ultimately producing * a filter definition structured as: * * { operator: filterDefs } * * @param {string} operator Logical operator to join the filter definitions by. * @param {Array|Object} filterDefs Array of filter definitions or individual * filter definition objects. * * @return {Object|Array} Filter definitions joined under a logical operator, * or a simple filter definition if `filterDefs` is of length 1, * otherwise an empty `Array`. * @private */ _joinFilterDefs: function(operator) { var filterDefs = Array.prototype.slice.call(arguments, 1); if (_.isEmpty(filterDefs)) { return []; } if (_.isArray(filterDefs[0])) { filterDefs = filterDefs[0]; } // if the length of the `filterList` is less than 2, then just return the simple filter if (filterDefs.length < 2) { return filterDefs[0]; } var filter = {}; filter[operator] = filterDefs; return filter; }, /** * Builds a filter object by using unique combination of the * searchTerm delimited by spaces. * * @param {Array} fieldNames Field within `quicksearch_field` * in the metadata to perform split term filtering. * @param {string} operator Operator to search by. As found in `FilterApi#addFilters`. * @param {string} searchTerm Search input entered. * * @return {Object|undefined} The search filter definition for * quick search or `undefined` if no filter to apply or supported. * @private */ _buildSplitTermFilterDef: function(fieldNames, operator, searchTerm) { if (fieldNames.length > 2) { app.logger.error('Cannot have more than 2 fields in a complex filter'); return; } // If the field is a split-term field, but only composed of single item // return the simple filter if (fieldNames.length === 1) { return this._buildFilterDef(fieldNames[0], operator, searchTerm); } var filterList = []; var tokens = searchTerm.split(' '); // When the searchTerm is composed of at least 2 terms delimited by a space character, // Divide the searchTerm in 2 unique sets // e.g. For the name "Jean Paul Durand", // first = "Jean", rest = "Paul Durand" (1st iteration) // first = "Jean Paul", rest = "Durand" (2nd iteration) for (var i = 1; i < tokens.length; ++i) { var first = _.first(tokens, i).join(' '); var rest = _.rest(tokens, i).join(' '); // FIXME the order of the filters need to be reviewed (TY-547) var tokenFilter = [ this._buildFilterDef(fieldNames[0], operator, first), this._buildFilterDef(fieldNames[1], operator, rest) ]; filterList.push(this._joinFilterDefs('$and', tokenFilter)); } // Try with full search term in each field // e.g. `first_name: Sangyoun Kim` or `last_name: Sangyoun Kim` _.each(fieldNames, function(name) { filterList.push(this._buildFilterDef(name, operator, searchTerm)); }, this); return this._joinFilterDefs('$or', filterList); } }) } }} }, "UserSignatures":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.UserSignaturesModel * @alias SUGAR.App.model.datas.BaseUserSignaturesModel * @extends Data.Bean */ ({ // Model Data (base) /** * @inheritdoc * * Wraps the success callback and makes a ping call when the default signature * attribute has changed since we are making changes to the user * preferences which requires a metadata refresh. */ save: function(attributes, options) { var success; var syncedAttrs = this.getSynced(); var changedAttrs = this.changedAttributes(syncedAttrs); if (_.has(changedAttrs, 'is_default')) { options = options || {}; success = options.success; options.success = function() { app.api.call('read', app.api.buildURL('ping')); if (_.isFunction(success)) { success.apply(options.context, arguments); } }; } return app.Bean.prototype.save.call(this, attributes, options); } }) } }} }, "Shippers":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Styleguide":{"fieldTemplates": { "base": { "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Styleguide.DateField * @alias SUGAR.App.view.fields.BaseStyleguideDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * @inheritdoc */ _dispose: function() { // FIXME: new date picker versions have support for plugin removal/destroy // we should do the upgrade in order to prevent memory leaks // FIXME: the base date field has a bug in disposing a datepicker field // that has been instantiated but not rendered. if (this._hasDatePicker && !_.isUndefined(this.$(this.fieldTag).data('datepicker'))) { $(window).off('resize', this.$(this.fieldTag).data('datepicker').place); } } }) } }} , "views": { "base": { "views-dashlet-toolbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Views-dashlet-toolbar View (base) plugins: ['Prettify'], className: 'container-fluid', initialize: function(options) { this._super('initialize', [options]); this.request = this.context.get('request'); }, _render: function() { this._super('_render'); this.example = app.view.createView({ context: this.context, type: 'dashlet-toolbar', module: 'Base', layout: this.layout, model: this.layout.model, readonly: true, meta: { label: 'Example dashlet title' } }); // override view function that relies on the dashlet layout this.example.toggleMinify = function(evt) { var $el = this.$('.dashlet-toggle > i'), collapsed = $el.is('.sicon-chevron-up'); this.$('.dashlet-toggle > i').toggleClass('sicon-chevron-down', collapsed); this.$('.dashlet-toggle > i').toggleClass('sicon-chevron-up', !collapsed); }; this.$('#example_view').append(this.example.el); this.example.render(); } }) }, "docs-components-tooltips": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-tooltips View (base) className: 'container-fluid', //components tooltips _renderHtml: function () { this._super('_renderHtml'); this.$('#tooltips').tooltip({ selector: '[rel=tooltip]' }); } }) }, "docs-components-progress": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-progress View (base) className: 'container-fluid', }) }, "docs-base-edit": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-edit View (base) className: 'container-fluid', }) }, "docs-forms-range": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-range View (base) className: 'container-fluid', // forms range _renderHtml: function () { this._super('_renderHtml'); var fieldSettings = { view: this, def: { name: 'include', type: 'range', view: 'edit', sliderType: 'connected', minRange: 0, maxRange: 100, 'default': true, enabled: true }, viewName: 'edit', context: this.context, module: this.module, model: this.model, }, rangeField = app.view.createField(fieldSettings); this.$('#test_slider').append(rangeField.el); rangeField.render(); rangeField.sliderDoneDelegate = function(minField, maxField) { return function(value) { minField.val(value.min); maxField.val(value.max); }; }(this.$('#test_slider_min'), this.$('#test_slider_max')); } }) }, "views-index": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Views-index View (base) plugins: ['Prettify'], extendsFrom: 'StyleguideDocsIndexView', className: 'container-fluid', }) }, "docs-dashboards-dashlets": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-dashboards-dashlets View (base) className: 'container-fluid', // dashboard dashlets _renderHtml: function() { this._super('_renderHtml'); // define event listeners app.events.on('preview:close', _.bind(function() { this.toggleSidebar(false); }, this)); app.events.on('app:dashletPreview:close', _.bind(function() { this.toggleSidebar(false); }, this)); app.events.on('app:dashletPreview:open', _.bind(function() { this.toggleSidebar(true); }, this)); this.$('.dashlet-example').on('click.styleguide', _.bind(function(event) { var button = this.$(event.currentTarget); var dashlet; var module; var metadata; if (button.hasClass('active')) { this.toggleSidebar(false); return; } this.$('.dashlet-example').removeClass('active'); button.addClass('active'); app.events.trigger('app:dashletPreview:open'); dashlet = button.data('dashlet'); module = button.data('module') || 'Styleguide'; metadata = app.metadata.getView(module, dashlet).dashlets[0]; metadata.type = dashlet; metadata.component = dashlet; this.previewDashlet(metadata); }, this)); }, _dispose: function() { this.$('.dashlet-example').off('click.styleguide'); this._super('_dispose'); }, toggleSidebar: function(state) { var defaultLayout = this.layout.getComponent('sidebar'); if (defaultLayout) { defaultLayout.trigger('sidebar:toggle', state); } if (!state) { this.$('.dashlet-example').removeClass('active'); } }, /** * Load dashlet preview by passing preview metadata * * @param {Object} metadata Preview metadata. */ previewDashlet: function(metadata) { var layout = this.layout.getComponent('sidebar'); var previewLayout; var previousComponent; var index; var contextDef; var component; while (layout) { if (layout.getComponent('preview-pane')) { previewLayout = layout.getComponent('preview-pane').getComponent('dashlet-preview'); break; } layout = layout.layout; } if (!previewLayout) { return; } previewLayout.showPreviewPanel(); // If there is no preview property, use the config property if (!metadata.preview) { metadata.preview = metadata.config; } previousComponent = _.last(previewLayout._components); if (previousComponent.name !== 'dashlet-preview' && previousComponent.name !== 'preview-header') { index = previewLayout._components.length - 1; previewLayout._components[index].dispose(); previewLayout.removeComponent(index); } component = { label: app.lang.get(metadata.label, metadata.preview.module), type: metadata.type, preview: true }; if (metadata.preview.module || metadata.preview.link) { contextDef = { skipFetch: false, forceNew: true, module: metadata.preview.module, link: metadata.preview.link }; } else if (metadata.module) { contextDef = { module: metadata.module }; } component.view = _.extend({module: metadata.module}, metadata.preview, component); if (contextDef) { component.context = contextDef; } previewLayout.initComponents([{ layout: { type: 'dashlet', label: app.lang.get(metadata.preview.label || metadata.label, metadata.preview.module), preview: true, components: [ component ] } }], this.context); previewLayout.loadData(); previewLayout.render(); } }) }, "docs-dashboards-intel": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-dashboards-intel View (base) className: 'container-fluid', // dashboard intel _renderHtml: function () { var self = this; this._super('_renderHtml'); this.$('.dashlet-example').on('click.styleguide', function(){ var dashlet = $(this).data('dashlet'), metadata = app.metadata.getView('Home', dashlet).dashlets[0]; metadata.type = dashlet; metadata.component = dashlet; self.layout.previewDashlet(metadata); }); }, _dispose: function() { this.$('.dashlet-example').off('click.styleguide'); this._super('_dispose'); } }) }, "docs-forms-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-buttons View (base) className: 'container-fluid', _render: function() { this._super('_render'); // button state demo this.$('#fat-btn').click(function () { var btn = $(this); btn.button('loading'); setTimeout(function () { btn.button('reset'); }, 3000); }) } }) }, "docs-forms-jstree": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-jstree View (base) className: 'container-fluid', // forms jstree _renderHtml: function () { var self = this; this._super('_renderHtml'); this.$('#people').jstree({ "json_data" : { "data" : [ { "data" : "Sabra Khan", "state" : "open", "metadata" : { id : 1 }, "children" : [ {"data" : "Mark Gibson","metadata" : { id : 2 }}, {"data" : "James Joplin","metadata" : { id : 3 }}, {"data" : "Terrence Li","metadata" : { id : 4 }}, {"data" : "Amy McCray", "metadata" : { id : 5 }, "children" : [ {"data" : "Troy McClure","metadata" : {id : 6}}, {"data" : "James Kirk","metadata" : {id : 7}} ] } ] } ] }, "plugins" : [ "json_data", "ui", "types" ] }) .bind('loaded.jstree', function () { // do stuff when tree is loaded self.$('#people').addClass('jstree-sugar'); self.$('#people > ul').addClass('list'); self.$('#people > ul > li > a').addClass('jstree-clicked'); }) .bind('select_node.jstree', function (e, data) { data.inst.toggle_node(data.rslt.obj); }); } }) }, "docs-components-dropdowns": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-dropdowns View (base) className: 'container-fluid', // components dropdowns _renderHtml: function () { this._super('_renderHtml'); this.$('#mm001demo *').on('click.styleguide', function(){ /* make this menu frozen in its state */ return false; }); this.$('*').on('click.styleguide', function(){ /* not sure how to override default menu behaviour, catching any click, becuase any click removes class `open` from li.open div.btn-group */ setTimeout(function(){ this.$('#mm001demo').find('li.open .btn-group').addClass('open'); },0.1); }); }, _dispose: function() { this.$('#mm001demo *').off('click.styleguide'); this._super('_dispose'); } }) }, "dashlet-tabbed": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-tabbed View (base) extendsFrom: 'TabbedDashletView', /** * @inheritdoc * * @property {Number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '10'. * @property {String} _defaultSettings.visibility Records visibility * regarding current user, supported values are 'user' and 'group', * defaults to 'user'. */ _defaultSettings: { limit: 10, visibility: 'user' }, /** * @inheritdoc */ initialize: function(options) { options.meta = options.meta || {}; options.meta.template = 'tabbed-dashlet'; this._super('initialize', [options]); }, /** * @inheritdoc * * FIXME: This should be removed when metadata supports date operators to * allow one to define relative dates for date filters. */ _initTabs: function() { this._super("_initTabs"); // FIXME: since there's no way to do this metadata driven (at the // moment) and for the sake of simplicity only filters with 'date_due' // value 'today' are replaced by today's date var today = new Date(); today.setHours(23, 59, 59); today.toISOString(); _.each(_.pluck(_.pluck(this.tabs, 'filters'), 'date_due'), function(filter) { _.each(filter, function(value, operator) { if (value === 'today') { filter[operator] = today; } }); }); }, _renderHtml: function() { if (this.meta.config) { this._super('_renderHtml'); return; } var tab = this.tabs[this.settings.get('activeTab')]; if (tab.overdue_badge) { this.overdueBadge = tab.overdue_badge; } var model1 = app.data.createBean('Tasks'); model1.set("assigned_user_id", "seed_sally_id"); model1.set("assigned_user_name", "Sally Bronsen"); model1.set("name", "Programmatically added task"); model1.set("date_due", "2014-02-07T07:15:00-05:00"); model1.set("date_due_flag", false); model1.set("date_start", null); model1.set("date_start_flag", false); model1.set("status", "Not Started"); this.collection.add(model1); _.each(this.collection.models, function(model) { var pictureUrl = app.api.buildFileURL({ module: 'Users', id: model.get('assigned_user_id'), field: 'picture' }); model.set('picture_url', pictureUrl); }, this); this._super('_renderHtml'); } }) }, "docs-layouts-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-layouts-list View (base) className: 'container-fluid', }) }, "docs-base-typography": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-typography View (base) className: 'container-fluid', }) }, "styleguide": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Styleguide View (base) initialize: function(options) { this._super('initialize', [options]); var request = this.context.get('request'); this.page = request.page_details; } }) }, "fields-index": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Fields-index View (base) className: 'container-fluid', section: {}, useTable: true, parent_link: '', tempfields: [], initialize: function(options) { this._super('initialize', [options]); var request = this.context.get('request'); this.keys = request.keys; this.page = request.page_details; }, _render: function() { var self = this, fieldTypeReq = this.context.get('content_name'), fieldTypes = fieldTypeReq === 'index' ? ['text', 'bool', 'date', 'datetimecombo', 'currency', 'email'] : [fieldTypeReq], fieldStates = ['detail', 'edit', 'error', 'disabled'], fieldLayouts = ['base', 'record', 'list'], fieldMeta = {}; this.useTable = fieldTypeReq === 'index' ? true : false; this.parent_link = fieldTypeReq === 'index' ? 'docs/forms-index' : 'fields/index'; this.tempfields = []; _.each(fieldTypes, function(fieldType) { //build meta data for field examples from model fields fieldMeta = _.find(self.model.fields, function(field) { return field.type === fieldType; }, self); //insert metadata into array for hbs template if (fieldMeta) { var metaData = self.meta['template_values'][fieldType]; if (_.isObject(metaData) && !_.isArray(metaData)) { _.each(metaData, function(value, name) { self.model.set(name, value); }, self); } else { self.model.set(fieldMeta.name, metaData); } self.tempfields.push(fieldMeta); } }); if (fieldTypeReq !== 'index') { self.title = fieldTypeReq + ' field'; var descTpl = app.template.getView('fields-index.' + fieldTypeReq, self.module); if (descTpl) { this.documentation = descTpl(); } else { this.page.description = 'SugarCRM ' + fieldTypeReq + ' field'; } } this._super('_render'); //render example fields into accordion grids //e.g., ['text','bool','date','datetimecombo','currency','email'] _.each(fieldTypes, function(fieldType) { var fieldMeta = _.find(self.tempfields, function(field) { return field.type === fieldType; }, self); //e.g., ['detail','edit','error','disabled'] _.each(fieldStates, function(fieldState) { //e.g., ['base','record','list'] _.each(fieldLayouts, function(fieldLayout) { var fieldTemplate = fieldState; //set field view template name if (fieldLayout === 'list') { if (fieldState === 'edit') { fieldTemplate = 'list-edit'; } else { fieldTemplate = 'list'; } } else if (fieldState === 'error') { fieldTemplate = 'edit'; } var fieldSettings = { view: self, def: { name: fieldMeta.name, type: fieldType, view: fieldTemplate, default: true, enabled: fieldState === 'disabled' ? false : true }, viewName: fieldTemplate, context: self.context, module: self.module, model: self.model, meta: fieldMeta }; var fieldObject = app.view.createField(fieldSettings), fieldDivId = '#' + fieldType + '_' + fieldState + '_' + fieldLayout; //pre render field setup if (fieldState !== 'detail') { if (!fieldObject.plugins || !_.contains(fieldObject.plugins, 'ListEditable') || fieldLayout !== 'list') { fieldObject.setMode('edit'); } else { fieldObject.setMode('list-edit'); } } if (fieldState === 'disabled') { fieldObject.setDisabled(true); } //render field self.$(fieldDivId).append(fieldObject.el); fieldObject.render(); //post render field setup if (fieldType === 'currency' && fieldState === 'edit') { fieldObject.setMode('edit'); } if (fieldState === 'error') { if (fieldType === 'email') { var errors = {email: ['primary@example.info']}; fieldObject.decorateError(errors); } else { fieldObject.setMode('edit'); fieldObject.decorateError('You did a bad, bad thing.'); } } }); }); }); } }) }, "docs-base-responsive": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-responsive View (base) className: 'container-fluid', }) }, "docs-charts-types": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-charts-types View (base) className: 'container-fluid', _renderHtml: function() { this._super('_renderHtml'); var data = { 'properties': { 'title': 'Forecasting for Q1 2017', 'groups': [ {'label': 'Mark Gibson'}, {'label': 'Terence Li'}, {'label': 'James Joplin'}, {'label': 'Amy McCray'}, {'label': 'My Opps'} ], 'xDataType': 'ordinal', 'yDataType': 'currency' }, 'data': [ { 'key': 'Qualified', 'values': [ {'x': 1, 'y': 50}, {'x': 2, 'y': 80}, {'x': 3, 'y': 0}, {'x': 4, 'y': 100}, {'x': 5, 'y': 100} ] }, { 'key': 'Proposal', 'values': [ {'x': 1, 'y': 50}, {'x': 2, 'y': 80}, {'x': 3, 'y': 0}, {'x': 4, 'y': 100}, {'x': 5, 'y': 90} ] }, { 'key': 'Negotiation', 'values': [ {'x': 1, 'y': 10}, {'x': 2, 'y': 50}, {'x': 3, 'y': 0}, {'x': 4, 'y': 40}, {'x': 5, 'y': 40} ] } ] }; var chart = sucrose.charts.multibarChart().colorData('default'); d3.select('#chart svg') .datum(data) .call(chart); } }) }, "docs-components-keybindings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-keybindings View (base) className: 'container-fluid', }) }, "docs-components-popovers": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-popovers View (base) className: 'container-fluid', // components popovers _renderHtml: function () { this._super('_renderHtml'); this.$('[rel=popover]').popover(); this.$('[rel=popoverHover]').popover({trigger: 'hover'}); this.$('[rel=popoverTop]').popover({placement: 'top'}); this.$('[rel=popoverBottom]').popover({placement: 'bottom'}); } }) }, "docs-layouts-tabs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-layouts-tabs View (base) className: 'container-fluid', // layouts tabs _renderHtml: function () { this._super('_renderHtml'); this.$('#nav-tabs-pills') .find('ul.nav-tabs > li > a, ul.nav-list > li > a, ul.nav-pills > li > a') .on('click.styleguide', function(e){ e.preventDefault(); e.stopPropagation(); $(this).tab('show'); }); }, _dispose: function() { this.$('#nav-tabs-pills') .find('ul.nav-tabs > li > a, ul.nav-list > li > a, ul.nav-pills > li > a') .off('click.styleguide'); this._super('_dispose'); } }) }, "docs-layouts-navbar": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-layouts-navbar View (base) className: 'container-fluid', }) }, "docs-base-icons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-icons View (base) className: 'container-fluid', // base icons _renderHtml: function () { this._super('_renderHtml'); this.$('.chart-icon').each(function(){ var svg = svgChartIcon($(this).data('chart-type')); $(this).html(svg); }); this.$('.filetype-thumbnail').each(function(){ $(this).html( '<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="28" height="33"><g><path class="ft-ribbon" d="M 0,15 0,29 3,29 3,13 z" /><path d="M 3,1 20.5,1 27,8 27,32 3,32 z" style="fill:#ececec;stroke:#b3b3b3;stroke-width:1;stroke-linecap:butt;" /><path d="m 20,1 0,7 7,0 z" style="fill:#b3b3b3;stroke-width:0" /></g></svg>' ); }); this.$('.sugar-cube').each(function(){ var svg = svgChartIcon('sugar-cube'); $(this).html(svg); }); } }) }, "docs-forms-datetime": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-datetime View (base) className: 'container-fluid', // forms datetime _renderHtml: function () { var self = this; this._super('_renderHtml'); // sugar7 date field //TODO: figure out how to set the date value when calling createField this.model.start_date = '2000-01-01T22:47:00+00:00'; var fieldSettingsDate = { view: this, def: { name: 'start_date', type: 'date', view: 'edit', enabled: true }, viewName: 'edit', context: this.context, module: this.module, model: this.model, }, dateField = app.view.createField(fieldSettingsDate); this.$('#sugar7_date').append(dateField.el); dateField.render(); // sugar7 datetimecombo field this.model.start_datetime = '2000-01-01T22:47:00+00:00'; var fieldSettingsCombo = { view: this, def: { name: 'start_datetime', type: 'datetimecombo', view: 'edit', enabled: true }, viewName: 'edit', context: this.context, module: this.module, model: this.model, }, datetimecomboField = app.view.createField(fieldSettingsCombo); this.$('#datetimecombo').append(datetimecomboField.el); datetimecomboField.render(); // static examples this.$('#dp1').datepicker(); this.$('#tp1').timepicker(); this.$('#dp2').datepicker({format:'mm-dd-yyyy'}); this.$('#tp2').timepicker({timeformat:'H.i.s'}); this.$('#dp3').datepicker(); var startDate = new Date(2012,1,20); var endDate = new Date(2012,1,25); this.$('#dp4').datepicker() .on('changeDate', function(ev){ if (ev.date.valueOf() > endDate.valueOf()){ self.$('#alert').show().find('strong').text('The start date can not be greater then the end date'); } else { self.$('#alert').hide(); startDate = new Date(ev.date); self.$('#startDate').text(self.$('#dp4').data('date')); } self.$('#dp4').datepicker('hide'); }); this.$('#dp5').datepicker() .on('changeDate', function(ev){ if (ev.date.valueOf() < startDate.valueOf()){ self.$('#alert').show().find('strong').text('The end date can not be less then the start date'); } else { self.$('#alert').hide(); endDate = new Date(ev.date); self.$('#endDate').text(self.$('#dp5').data('date')); } self.$('#dp5').datepicker('hide'); }); this.$('#tp3').timepicker({'scrollDefaultNow': true}); this.$('#tp4').timepicker(); this.$('#tp4_button').on('click', function (){ self.$('#tp4').timepicker('setTime', new Date()); }); this.$('#tp5').timepicker({ 'minTime': '2:00pm', 'maxTime': '6:00pm', 'showDuration': true }); this.$('#tp6').timepicker(); this.$('#tp6').on('changeTime', function() { self.$('#tp6_legend').text('You selected: ' + $(this).val()); }); this.$('#tp7').timepicker({ 'step': 5 }); }, _dispose: function() { this.$('#dp4').off('changeDate'); this.$('#dp5').off('changeDate'); this.$('#tp4_button').off('click'); this.$('#tp6').off('changeTime'); this._super('_dispose'); } }) }, "docs-forms-editable": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-editable View (base) className: 'container-fluid', // forms editable _renderHtml: function() { this._super('_renderHtml'); this.$('.url-editable-trigger').on('click.styleguide', function() { var uefield = $(this).next(); uefield .html(uefield.text()) .editable( function(value, settings) { var nvprep = '<a href="' + value + '">'; var nvapp = '</a>'; value = nvprep.concat(value); return (value); }, {onblur: 'submit'} ) .trigger('click.styleguide'); }); this.$('.text-editable-trigger').on('click.styleguide',function() { var uefield = $(this).next(); uefield .html(uefield.text()) .editable() .trigger('click.styleguide'); }); this.$('.urleditable-field > a').each(function() { if (isEllipsis($(this)) === true) { $(this).attr({'data-original-title': $(this).text(), 'rel': 'tooltip', 'class': 'longUrl'}); } }); function isEllipsis(e) { // check if ellipsis is present on el, add tooltip if so return (e[0].offsetWidth < e[0].scrollWidth); } this.$('.longUrl[rel=tooltip]').tooltip({placement: 'top'}); } }) }, "docs-base-mixins": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-mixins View (base) className: 'container-fluid', }) }, "docs-layouts-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-layouts-drawer View (base) className: 'container-fluid', // layouts drawer _renderHtml: function () { this._super('_renderHtml'); this.$('#sg_open_drawer').on('click.styleguide', function(){ app.drawer.open({ layout: 'create', context: { create: true, model: app.data.createBean('Styleguide') } }); }); }, _dispose: function() { this.$('#sg_open_drawer').off('click.styleguide'); this._super('_dispose'); } }) }, "docs-base-theme": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-theme View (base) className: 'container-fluid', }) }, "docs-forms-file": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-file View (base) className: 'container-fluid', // components dropdowns _renderHtml: function() { this._super('_renderHtml'); /* Custom file upload overrides and avatar widget */ var uobj = []; var onUploadChange = function(e) { var status = $(this); var opts = 'show'; if (this.value) { var thisContainer = $(this).parent('.file-upload').parent('.upload-field-custom'); var valueExplode = this.value.split('\\'); var value = valueExplode[valueExplode.length - 1]; if ($(this).closest('.upload-field-custom').hasClass('avatar') === true) { /* hide status for avatars */ opts = 'hide'; } if (thisContainer.next('.file-upload-status').length > 0) { thisContainer.next('.file-upload-status').remove(); } this.$('<span class="file-upload-status ' + opts + ' ">' + value + '</span>') .insertAfter(thisContainer); } }; var onUploadFocus = function() { $(this).parent().addClass('focus'); }; var onUploadBlur = function() { $(this).parent().addClass('focus'); }; this.$('.upload-field-custom input[type=file]').each(function() { // Bind events $(this) .bind('focus', onUploadFocus) .bind('blur', onUploadBlur) .bind('change', onUploadChange); // Get label width so we can make button fluid, 12px default left/right padding var lblWidth = $(this).parent().find('span strong').width() + 24; $(this) .parent().find('span').css('width', lblWidth) .closest('.upload-field-custom').css('width', lblWidth); // Set current state onUploadChange.call(this); // Minimizes the text input part in IE $(this).css('width', '0'); }); this.$('#photoimg').on('change', function() { $('#preview1').html(''); $('#preview1').html('<span class="loading">Loading...</span>'); $('#imageform').ajaxForm({ target: '#preview1' }).submit(); }); this.$('.preview.avatar').on('click.styleguide', function(e) { $(this).closest('.span10').find('label.file-upload span strong').trigger('click'); }); }, _dispose: function(view) { this.$('#photoimg').off('change'); this.$('.preview.avatar').off('click.styleguide'); } }) }, "docs-dashboards-home": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-dashboards-home View (base) className: 'container-fluid', // dashboards home _renderHtml: function () { var self = this; this._super('_renderHtml'); this.$('.dashlet-example').on('click.styleguide', function(){ var dashlet = $(this).data('dashlet'), metadata = app.metadata.getView('Home', dashlet).dashlets[0]; metadata.type = dashlet; metadata.component = dashlet; self.layout.previewDashlet(metadata); }); this.$('[data-modal]').on('click.styleguide', function(){ var modal = $(this).data('modal'); $(modal).appendTo('body').modal('show'); }); }, _dispose: function() { this.$('.dashlet-example').off('click.styleguide'); this.$('[data-modal]').off('click.styleguide'); this._super('_dispose'); } }) }, "views-list-basic": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Views-list-basic View (base) plugins: ['Prettify'], className: 'container-fluid', initialize: function(options) { this._super('initialize', [options]); this.request = this.context.get('request'); }, _render: function() { this._super('_render'); this.layout.model.set({ full_name: 'Cpt. James Kirk', title: 'SC937-0176 CEC', do_not_call: 1, email: 'kirkjt@starfleet.gov', assigned_user_name: 'Administrator', list_price: 123.45, birthdate: '03/22/2233', date_end: '06/15/2319 7:50:17PM' }); this.example = app.view.createView({ context: this.context, type: 'list', module: 'Styleguide', layout: this.layout, model: this.layout.model, readonly: true }); this.example.collection.add(this.layout.model); this.example._render(); this.$('#example_view').append(this.example.el); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Styleguide.CreateView * @alias SUGAR.App.view.views.StyleguideCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', showHelpText: false, showErrorDecoration: false, showFormHorizontal: false, events: { 'click a[name=show_help_text]:not(.disabled)': 'toggleHelpText', 'click a[name=display_error_state]:not(.disabled)': 'toggleErrorDecoration', 'click a[name=display_form_horizontal]:not(.disabled)': 'toggleFormHorizontal' }, _render: function() { var error_string = 'You did a bad, bad thing.'; _.each(this.meta.panels, function(panel) { if (!panel.header) { panel.labelsOnTop = !this.showFormHorizontal; } }, this); if (this.showErrorDecoration) { _.each(this.fields, function(field) { if (!_.contains(['button', 'rowaction', 'actiondropdown'], field.type)) { field.setMode('edit'); field._errors = error_string; if (field.type === 'email') { var errors = {email: ['primary@example.info']}; field.handleValidationError([errors]); } else { if (_.contains(['image', 'picture', 'avatar'], field.type)) { field.handleValidationError(error_string); } else { field.decorateError(error_string); } } } }, this); } this._super('_render'); }, _renderField: function(field) { app.view.View.prototype._renderField.call(this, field); var error_string = 'You did a bad, bad thing.'; if (!this.showHelpText) { field.def.help = null; field.options.def.help = null; } }, toggleHelpText: function(e) { this.showHelpText = !this.showHelpText; this.render(); e.preventDefault(); e.stopPropagation(); }, toggleErrorDecoration: function(e) { this.showErrorDecoration = !this.showErrorDecoration; this.render(); e.preventDefault(); e.stopPropagation(); }, toggleFormHorizontal: function(e) { this.showFormHorizontal = !this.showFormHorizontal; this.render(); e.preventDefault(); e.stopPropagation(); } }) }, "docs-forms-layouts": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-layouts View (base) className: 'container-fluid', // forms switch _renderHtml: function () { this._super('_renderHtml'); var self = this; this.$('select.select2').each(function(){ var $this = $(this), ctor = self.getSelect2Constructor($this); $this.select2(ctor); }); this.$('table td [rel=tooltip]').tooltip({ container:'body', placement:'top', html:'true' }); this.$('.error input, .error textarea').on('focus', function(){ $(this).next().tooltip('show'); }); this.$('.error input, .error textarea').on('blur', function(){ $(this).next().tooltip('hide'); }); this.$('.add-on') // I cannot find where _this_ tooltip gets initialised with 'hover', so i detroy it first, -f1vlad .tooltip('dispose') .tooltip({ trigger: 'click', container: 'body' }); }, _dispose: function() { this.$('.error input, .error textarea').off('focus'); this.$('.error input, .error textarea').off('blur'); }, getSelect2Constructor: function($select) { var _ctor = {}; _ctor.minimumResultsForSearch = 7; _ctor.dropdownCss = {}; _ctor.dropdownCssClass = ''; _ctor.containerCss = {}; _ctor.containerCssClass = ''; if ( $select.hasClass('narrow') ) { _ctor.dropdownCss.width = 'auto'; _ctor.dropdownCssClass = 'select2-narrow '; _ctor.containerCss.width = '75px'; _ctor.containerCssClass = 'select2-narrow'; _ctor.width = 'off'; } if ( $select.hasClass('inherit-width') ) { _ctor.dropdownCssClass = 'select2-inherit-width '; _ctor.containerCss.width = '100%'; _ctor.containerCssClass = 'select2-inherit-width'; _ctor.width = 'off'; } return _ctor; } }) }, "docs-base-variables": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-variables View (base) className: 'container-fluid', }) }, "docs-forms-select2": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-select2 View (base) className: 'container-fluid', // forms range _renderHtml: function () { this._super('_renderHtml'); var $this, ctor; function select2ChartSelection(chart) { if (!chart.id) return chart.text; // optgroup return svgChartIcon(chart.id); } function select2ChartResult(chart) { if (!chart.id) return chart.text; // optgroup return svgChartIcon(chart.id) + chart.text; } // $('select[name="priority"]').select2({minimumResultsForSearch: 7}); // $('#priority').select2({minimumResultsForSearch: 7, width: '200px'}); // $("#e6").select2({ placeholder: "Search for a movie", minimumInputLength: 1, ajax: { // instead of writing the function to execute the request we use Select2's convenient helper url: "styleguide/content/js/select2.json", dataType: 'json', data: function (term, page) { return {q:term}; }, results: function (data, page) { // parse the results into the format expected by Select2. // since we are using custom formatting functions we do not need to alter remote JSON data return {results: data.movies}; } }, formatResult: function(m) { return m.title; }, formatSelection: function(m) { return m.title; } }); // $this = $('#priority2'); ctor = this.getSelect2Constructor($this); $this.select2(ctor); // $this = $('#state'); ctor = this.getSelect2Constructor($this); ctor.formatSelection = function(state) { return state.id;}; $this.select2(ctor); $('#states3').select2({width: '200px', minimumResultsForSearch: 7, allowClear: true}); // $this = $('#s2_hidden'); ctor = this.getSelect2Constructor($this); ctor.data = [ {id: 0, text: 'enhancement'}, {id: 1, text: 'bug'}, {id: 2, text: 'duplicate'}, {id: 3, text: 'invalid'}, {id: 4, text: 'wontfix'} ]; ctor.placeholder = "Select a issue type..."; $this.select2(ctor); $('#states4').select2({width: '200px', minimumResultsForSearch: 1000, dropdownCssClass: 'select2-drop-bootstrap'}); // $this = $('select[name="chart_type"]'); ctor = this.getSelect2Constructor($this); ctor.dropdownCssClass = 'chart-results select2-narrow'; ctor.width = 'off'; ctor.minimumResultsForSearch = 9; ctor.formatResult = select2ChartResult; ctor.formatSelection = select2ChartSelection; ctor.escapeMarkup = function(m) { return m; }; $this.select2(ctor); // $this = $('select[name="label_module"]'); ctor = this.getSelect2Constructor($this); ctor.width = 'off'; ctor.minimumResultsForSearch = 9; ctor.formatSelection = function(item) { return '<span class="label label-module label-module-mini label-' + item.text + '">' + item.id + '</span>'; }; ctor.escapeMarkup = function(m) { return m; }; ctor.width = '55px'; $this.select2(ctor); // $('#priority3').select2({width: '200px', minimumResultsForSearch: 7, dropdownCssClass: 'select2-drop-error'}); // $('#multi1').select2({width: '100%'}); $('#multi2').select2({width: '300px'}); // $('#states5').select2({ width: '100%', minimumResultsForSearch: 7, containerCssClass: 'select2-choices-pills-close' }); // $('#states4').select2({ width: '100%', minimumResultsForSearch: 7, containerCssClass: 'select2-choices-pills-close', formatSelection: function(item) { return '<span class="select2-choice-type">Link:</span><a href="javascript:void(0)" rel="' + item.id + '">' + item.text + '</a>'; }, escapeMarkup: function(m) { return m; } }); $('.error .select .error-tooltip').tooltip({ trigger: 'click', container: 'body' }); }, getSelect2Constructor: function($select) { var _ctor = {}; _ctor.minimumResultsForSearch = 7; _ctor.dropdownCss = {}; _ctor.dropdownCssClass = ''; _ctor.containerCss = {}; _ctor.containerCssClass = ''; if ( $select.hasClass('narrow') ) { _ctor.dropdownCss.width = 'auto'; _ctor.dropdownCssClass = 'select2-narrow '; _ctor.containerCss.width = '75px'; _ctor.containerCssClass = 'select2-narrow'; _ctor.width = 'off'; } if ( $select.hasClass('inherit-width') ) { _ctor.dropdownCssClass = 'select2-inherit-width '; _ctor.containerCss.width = '100%'; _ctor.containerCssClass = 'select2-inherit-width'; _ctor.width = 'off'; } return _ctor; } }) }, "docs-components-collapse": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-collapse View (base) className: 'container-fluid', }) }, "docs-forms-switch": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-forms-switch View (base) className: 'container-fluid', // forms switch _renderHtml: function () { this._super('_renderHtml'); this.$('#mySwitch').on('switch-change', function (e, data) { var $el = $(data.el), value = data.value; }); }, _dispose: function() { this.$('#mySwitch').off('switch-change'); this._super('_dispose'); } }) }, "docs-index": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-index View (base) className: 'container-fluid', initialize: function(options) { this._super('initialize', [options]); var request = this.context.get('request'); this.keys = request.keys; this.page_data = request.page_data[this.keys[0]]; }, /* RENDER index page *******************/ _renderHtml: function() { var self = this, i = 0, html = '', chapter_key = this.keys[0], section_key = this.keys[1], section; if (section_key === 'index') { // home index call $.each(this.page_data.sections, function(kS, vS) { if (!vS.index) { return; } html += (i % 3 === 0 ? '<div class="row-fluid">' : ''); html += '<div class="span4"><h3>' + '<a class="section-link" href="' + (vS.url ? vS.url : self.fmtLink(kS)) + '">' + vS.title + '</a></h3><p>' + vS.description + '</p><ul>'; if (vS.pages) { $.each(vS.pages, function(kP, vP) { html += '<li ><a class="section-link" href="' + (vP.url ? vP.url : self.fmtLink(kS, kP)) + '">' + vP.title + '</a></li>'; }); } html += '</ul></div>'; html += (i % 3 === 2 ? '</div>' : ''); i += 1; }); } else { section = this.page_data.sections[section_key]; // section index call $.each(section.pages, function(kP, vP) { html += (i % 4 === 0 ? '<div class="row-fluid">' : ''); html += '<div class="span3"><h3>' + (!vP.items ? ('<a class="section-link" href="' + (vP.url ? vP.url : self.fmtLink(section_key, kP)) + '">' + vP.title + '</a>') : vP.title ) + '</h3><p>' + vP.description; html += '</p></div>'; html += (i % 4 === 3 ? '</div>' : ''); i += 1; }); } this._super('_renderHtml'); this.$('#index-content').append('<section id="section-menu"></section>').html(html); }, fmtLink: function(s, p) { return '#Styleguide/' + this.keys[0] + '/' + s + (p ? '-' + p : '-index'); } }) }, "docs-layouts-modals": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-layouts-modals View (base) className: 'container-fluid', // layouts modals _renderHtml: function () { this._super('_renderHtml'); this.$('[rel=popover]').popover(); this.$('.modal').tooltip({ selector: '[rel=tooltip]' }); this.$('#dp1').datepicker({ format: 'mm-dd-yyyy' }); this.$('#dp3').datepicker(); this.$('#tp1').timepicker(); } }) }, "docs-base-labels": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-labels View (base) module_list: [], className: 'container-fluid', _renderHtml: function () { this.module_list = _.without(app.metadata.getModuleNames({filter: 'display_tab', access: 'read'}), 'Home'); this.module_list.sort(); this._super('_renderHtml'); } }) }, "dashlet-chart": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-chart View (base) extendsFrom: 'OpportunityMetricsView', loadData: function (options) { if (this.meta.config) { return; } this.metricsCollection = { "won": { "amount_usdollar": 40000, "count": 4, "formattedAmount": "$30,000", "icon": "caret-up", "cssClass": "won", "dealLabel": "won", "stageLabel": "Won" }, "lost": { "amount_usdollar": 10000, "count": 1, "formattedAmount": "$10,000", "icon": "caret-down", "cssClass": "lost", "dealLabel": "lost", "stageLabel": "Lost" }, "active": { "amount_usdollar": 30000, "count": 3, "formattedAmount": "$30,000", "icon": "minus", "cssClass": "active", "dealLabel": "active", "stageLabel": "Active" } }; this.chartCollection = { "data": [ { "key": "Won", "value": 4, "classes": "won" }, { "key": "Lost", "value": 1, "classes": "lost" }, { "key": "Active", "value": 3, "classes": "active" } ], "properties": { "title": "Opportunity Metrics", "value": 8, "label": 8 } }; this.total = 8; } }) }, "sg-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Sg-headerpane View (base) className: 'headerpane', initialize: function(options) { this._super('initialize', [options]); var request = this.context.get('request'); this.page = request.page_details; this.sections = request.page_data.docs.sections; this.$find = []; }, _render: function() { var self = this, $optgroup = {}; // render view this._super('_render'); // styleguide guide doc search this.$find = $('#find_patterns'); if (this.$find.length) { // build search select2 options $.each(this.sections, function(k, v) { if (!v.index) { return; } $optgroup = $('<optgroup>').appendTo(self.$find).attr('label', v.title); $.each(v.pages, function(i, d) { renderSearchOption(k, i, d, $optgroup); }); }); // search for patterns this.$find.on('change', function(e) { window.location.href = $(this).val(); }); // init select2 control this.$find.select2(); } function renderSearchOption(section, page, d, optgroup) { $('<option>') .appendTo(optgroup) .attr('value', (d.url ? d.url : fmtLink(section, page))) .text(d.title); } function fmtLink(section, page) { return '#Styleguide/docs/' + section + (page ? '-' + page : '-index'); } }, _dispose: function() { this.$find.off('change'); this._super('_dispose'); } }) }, "docs-base-grid": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-base-grid View (base) className: 'container-fluid', }) }, "docs-components-alerts": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-components-alerts View (base) className: 'container-fluid', // components dropdowns _renderHtml: function () { this._super('_renderHtml'); this.$('[data-alert]').on('click', function() { var $button = $(this), level = $button.data('alert'), state = $button.text(), auto_close = ['info','success'].indexOf(level) > -1; app.alert.dismiss('core_meltdown_' + level); if (state !== 'Example') { $button.text('Example'); } else { app.alert.show('core_meltdown_' + level, { level: level, messages: 'The core is in meltdown!!', autoClose: auto_close, onClose: function () { $button.text('Example'); } }); $button.text('Dismiss'); } }); }, _dispose: function() { this.$('[data-alert]').off('click'); app.alert.dismissAll(); this._super('_dispose'); } }) }, "docs-charts-colors": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs-charts-colors View (base) className: 'container-fluid', _renderHtml: function() { this._super('_renderHtml'); // Chart data var dataDefault = { 'properties': { 'title': 'Sales by Section' }, 'data': [ {key: 'Section 1', value: 3}, {key: 'Section 2', value: 5}, {key: 'Section 3', value: 7}, {key: 'Section 4', value: 9} ] }; var dataColors = { 'properties': { 'title': 'Sales by Section' }, 'data': [ {key: 'Section 1', value: 3, color: '#d62728'}, {key: 'Section 2', value: 5, color: '#ff7f0e'}, {key: 'Section 3', value: 7, color: '#bcbd22'}, {key: 'Section 4', value: 9, color: '#2ca02c'} ] }; var dataClasses = { 'properties': { 'title': 'Sales by Section' }, 'data': [ {key: 'Section 1', value: 3, classes: 'sc-fill09'}, {key: 'Section 2', value: 5, classes: 'sc-fill03'}, {key: 'Section 3', value: 7, classes: 'sc-fill12'}, {key: 'Section 4', value: 9, classes: 'sc-fill05'} ] }; // Color options var defaultOptions = {}; var gradientOptions = {gradient: true}; var graduatedOptions = {c1: '#e8e2ca', c2: '#3e6c0a', l: dataDefault.data.length}; var graduatedGradientOptions = {c1: '#e8e2ca', c2: '#3e6c0a', l: dataDefault.data.length, gradient: true}; // Chart models var chartDefault = sucrose.charts.pieChart().colorData('default', defaultOptions); var chartDefaultGradient = sucrose.charts.pieChart().colorData('default', gradientOptions); var chartData = sucrose.charts.pieChart().colorData('data', defaultOptions); var chartDataGradient = sucrose.charts.pieChart().colorData('data', gradientOptions); var chartGraduated = sucrose.charts.pieChart().colorData('graduated', graduatedOptions); var chartGraduatedGradient = sucrose.charts.pieChart().colorData('graduated', graduatedGradientOptions); var chartClasses = sucrose.charts.pieChart().colorData('class', defaultOptions); // Render d3.select('#pie1 svg') .datum(dataDefault) .call(chartDefault); d3.select('#pie2 svg') .datum(dataDefault) .call(chartDefaultGradient); d3.select('#pie3 svg') .datum(dataColors) .call(chartData); d3.select('#pie4 svg') .datum(dataColors) .call(chartDataGradient); d3.select('#pie5 svg') .datum(dataDefault) .call(chartGraduated); d3.select('#pie6 svg') .datum(dataDefault) .call(chartGraduatedGradient); d3.select('#pie7 svg') .datum(dataDefault) .call(chartClasses); d3.select('#pie8 svg') .datum(dataClasses) .call(chartData); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "styleguide": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Styleguide Layout (base) initialize: function(options) { var request = { page_data: {}, keys: [], chapter_details: {}, section_details: {}, page_details: {}, parent_link: '', view: 'index' }; var chapterName; var contentName; var chapter; var section; var page; chapterName = options.context.get('chapter_name'); contentName = options.context.get('content_name'); // load up the styleguide css if not already loaded //TODO: cleanup styleguide.css and add to main file if ($('head #styleguide_css').length === 0) { $('<link>') .attr({ rel: 'stylesheet', href: 'styleguide/assets/css/styleguide.css', id: 'styleguide_css' }) .appendTo('head'); } document.title = $('<span/>').html('Styleguide » SugarCRM').text(); // request.page_data = this.meta.metadata.page_data; request.page_data = app.metadata.getLayout(options.module, 'styleguide').metadata.chapters; request.keys = [chapterName]; if (!_.isUndefined(contentName) && !_.isEmpty(contentName)) { Array.prototype.push.apply(request.keys, contentName.split('-')); } chapter = request.page_data[request.keys[0]]; request.chapter_details = { title: chapter.title, description: chapter.description }; if (chapter.index && request.keys.length > 1 && request.keys[1] !== 'index') { section = chapter.sections[request.keys[1]]; request.section_details = { title: section.title, description: section.description }; if (section.index && request.keys.length > 2 && request.keys[2] !== 'index') { page = section.pages[request.keys[2]]; request.page_details = { title: page.title, description: page.description, url: page.url }; request.view = contentName; request.parent_link = '-' + request.keys[0][request.keys[1]]; window.prettyPrint && prettyPrint(); } else { request.page_details = request.section_details; } } else { request.page_details = request.chapter_details; } request.page_details.css_class = 'container-fluid'; options.context.set('request', request); this._super('initialize', [options]); } }) }, "docs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Docs Layout (base) plugins: ['Prettify'], extendsFrom: 'StyleguideStyleguideLayout', /** * @inheritdoc */ initComponents: function(components, context, module) { var def; var main; var content; var request = this.context.get('request'); this._super('initComponents', [components, context, module]); def = { view: { type: request.keys[0] + '-' + request.view, name: request.keys[0] + '-' + request.view, meta: request.page_details } }; main = this.getComponent('sidebar').getComponent('main-pane'); content = this.createComponentFromDef(def, this.context, this.module); main.addComponent(content); }, /** * @inheritdoc */ _render: function() { var defaultLayout = this.getComponent('sidebar'); if (defaultLayout) { defaultLayout.trigger('sidebar:toggle', false); } this._super('_render'); } }) }, "fields": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Fields Layout (base) plugins: ['Prettify'], extendsFrom: 'StyleguideStyleguideLayout' }) }, "views": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Views Layout (base) plugins: ['Prettify'], extendsFrom: 'StyleguideDocsLayout', }) } }} , "datas": {} }, "Feedbacks":{"fieldTemplates": { "base": { "rating": {"controller": /* * deprecated */ /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Rating field will generate clickable stars that will translate those into * a value for the model. * * Supported properties: * * - {Number} rate How many stars to display * - {Number} default What is the default value of the starts * * Example: * // ... * array( * 'rate' => 3, * 'default' => 3, * ), * //... * * @class View.Fields.Base.Feedbacks.RatingField * @alias SUGAR.App.view.fields.BaseFeedbacksRatingField * @extends View.Fields.Base.BaseField */ ({ // Rating FieldTemplate (base) /** * @inheritdoc * * Initializes default rate and generates stars based on that rate for * template. */ initialize: function(options) { this._super('initialize', [options]); this.def.rate = this.def.rate || 3; this.model.setDefault(this.name, this.def.default); }, /** * @inheritdoc * * Fills all stars up to `this.value`. `true` means fill, `false` means not * filled. */ format: function(value) { this.stars = _.map(_.range(1, this.def.rate + 1), function(n) { return n <= value; }); return value; }, /** * @inheritdoc */ unformat: function(value) { return value + 1; }, /** * @override * This will bind to a different event (`click` instead of `change`). */ bindDomChange: function() { if (!this.model) { return; } var $el = this.$('[data-value]'); $el.on('click', _.bind(function(evt) { var value = $(evt.currentTarget).data('value'); this.model.set(this.name, this.unformat(value)); }, this)); }, /** * @override * This will always render on model change. */ bindDataChange: function() { if (this.model) { this.model.on('change:' + this.name, this.render, this); } } }) } }} , "views": { "base": { "feedback": {"controller": /* * deprecated */ /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * This View allows the user to provide Feedback about SugarCRM platform to a * GoogleDoc spreadsheet. * * The view can stay visible while the user is navigating and will use the * current URL when the user clicks submit. Other fields are mapped into the * spreadsheet and might become metadata driven in the future. * * @class View.Views.Base.Feedbacks.FeedbackView * @alias SUGAR.App.view.views.BaseFeedbacksFeedbackView * @extends View.View * @deprecated Feedback functionality is deprecated */ ({ // Feedback View (base) plugins: ['ErrorDecoration'], events: { 'click [data-action=submit]': 'submit', 'click [data-action=close]': 'close' }, /** * @inheritdoc * * During initialize we listen to model validation and if it is valid we * {@link #send} the Feedback. */ initialize: function(options) { app.logger.warn('Feedback functionality has been deprecated and will be removed in a future release'); options.model = app.data.createBean('Feedbacks'); var fieldsMeta = _.flatten(_.pluck(options.meta.panels, 'fields')); options.model.fields = {}; _.each(fieldsMeta, function(field) { options.model.fields[field.name] = field; }); this._super('initialize', [options]); this.context.set('skipFetch', true); this.model.on('validation:start', function() { app.alert.dismiss('send_feedback'); }); this.model.on('error:validation', function() { app.alert.show('send_feedback', { level: 'error', messages: app.lang.get('LBL_FEEDBACK_SEND_ERROR', this.module) }); this.$('[data-action=submit]').removeAttr('disabled'); }, this); this.model.on('validation:success', this.send, this); // TODO Once the view renders the button, this is no longer needed this.button = $(options.button); /** * The internal state of this view. * By default this view is closed ({@link #toggle} will call render). * * FIXME TY-1798/TY-1800 This is needed due to the bad popover plugin. * * @type {boolean} * @private */ this._isOpen = false; let products = app.user.getProductCodes(); products = products ? products.join(',') : ''; var params = { edition: app.metadata.getServerInfo().flavor, version: app.metadata.getServerInfo().version, lang: app.lang.getLanguage(), module: this.module, route: 'list' }; if (!_.isEmpty(products)) { params.products = products; } var learnMoreUrl = 'https://www.sugarcrm.com/crm/product_doc.php?' + $.param(params); /** * Aside text with all the translated links and strings to easily show * it in the view. * @type {String} */ this.aside = new Handlebars.SafeString(app.lang.get('TPL_FEEDBACK_ASIDE', this.module, { learnMoreLink: new Handlebars.SafeString('<a href="' + learnMoreUrl + '" target="_blank">' + Handlebars.Utils.escapeExpression( app.lang.get('LBL_FEEDBACK_ASIDE_CLICK_MORE', this.module) ) + '</a>'), contactSupportLink: new Handlebars.SafeString('<a href="http://support.sugarcrm.com" target="_blank">' + Handlebars.Utils.escapeExpression( app.lang.get('LBL_FEEDBACK_ASIDE_CONTACT_SUPPORT', this.module) ) + '</a>') })); }, /** * Initializes the popover plugin for the button given. * * @param {jQuery} button the jQuery button; * @private */ _initPopover: function(button) { button.popover({ title: app.lang.get('LBL_FEEDBACK', this.module), content: _.bind(function() { return this.$el; }, this), html: true, placement: 'top', trigger: 'manual', template: '<div class="popover feedback"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' }); // Reposition the modal so all of its contents are within the window. button.on('shown.bs.popover', _.bind(this._positionPopover, this)); }, /** * Sets the horizontal position of the modal. * * @private */ _positionPopover: function() { var $popoverContainer = this.button.data()['bs.popover'].tip(); var left; if (app.lang.direction === 'rtl') { // Leave 16px of space between lhs edge of popover and the screen. left = 16; } else { // Leave 16px of space between rhs edge of popover and the screen. left = $(window).width() - $popoverContainer.width() - 16; } $popoverContainer.css('left', left); }, /** * Close button on the feedback view is pressed. * * @param {Event} evt The `click` event. */ close: function() { this.toggle(false); }, /** * Toggle this view (by re-rendering) and allow force option. * * @param {boolean} [show] `true` to show, `false` to hide, `undefined` * toggles the current state. */ toggle: function(show) { if (_.isUndefined(show)) { this._isOpen = !this._isOpen; } else { this._isOpen = show; } this.button.popover('dispose'); if (this._isOpen) { this.render(); this._initPopover(this.button); this.button.popover('show'); } this.trigger(this._isOpen ? 'show' : 'hide', this, this._isOpen); }, /** * @inheritdoc * During dispose destroy the popover. */ _dispose: function() { if (this.button) { this.button.popover('dispose'); } this._super('_dispose'); }, /** * Submit the form */ submit: function(e) { var $btn = this.$(e.currentTarget); if ($btn.attr('disabled')) { return; } $btn.attr('disabled', 'disabled'); this.model.doValidate(); }, /** * Sends the Feedback to google doc page. * * Populate the rest of the data into the model from different sources of * the app. */ send: function() { this.model.set({ timezone: app.user.getPreference('timezone'), account_type: app.user.get('type'), role: app.user.get('roles').join(', ') || 'n/a', feedback_app_path: window.location.href, feedback_user_browser: navigator.userAgent + ' (' + navigator.language + ')', feedback_user_os: navigator.platform, feedback_sugar_version: _.toArray(_.pick(app.metadata.getServerInfo(), 'product_name', 'version')).join(' '), company: app.config.systemName }); var post_url = 'https://docs.google.com/forms/d/1iIdfeWma_OUUkaP-wSojZW2GelaxMOBgDq05A8PGHY8/formResponse'; $.ajax({ url: post_url, type: 'POST', data: { 'entry.98009013': this.model.get('account_type'), 'entry.1589366838': this.model.get('timezone'), 'entry.762467312': this.model.get('role'), 'entry.968140953': this.model.get('feedback_text'), 'entry.944905780': this.model.get('feedback_app_path'), 'entry.1750203592': this.model.get('feedback_user_browser'), 'entry.1115361778': this.model.get('feedback_user_os'), 'entry.1700062722': this.model.get('feedback_csat'), 'entry.1926759955': this.model.get('feedback_sugar_version'), 'entry.398692075': this.model.get('company') }, dataType: 'xml', crossDomain: true, cache: false, context: this, timeout: 10000, success: this._handleSuccess, error: function(xhr) { if (xhr.status === 0) { // the status might be 0 which is still a success from a // cross domain request using xml as dataType this._handleSuccess(); return; } app.alert.show('send_feedback', { level: 'error', messages: app.lang.get('LBL_FEEDBACK_NOT_SENT', this.module) }); } }); }, /** * Handles the success of Feedback submission. * * Show the success message on top (alert), clears the model and hides the * view. This will allow the user to be ready for yet another feedback. * * @private */ _handleSuccess: function() { app.alert.show('send_feedback', { level: 'success', messages: app.lang.get('LBL_FEEDBACK_SENT', this.module), autoClose: true }); this.model.clear(); this.toggle(false); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Tags":{"fieldTemplates": { "base": { "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.EditablelistbuttonField * @alias SUGAR.App.view.fields.BaseEditablelistbuttonField * @extends View.Fields.Base.ButtonField */ ({ // Editablelistbutton FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // Initialize collection this.collection = app.data.createBeanCollection('Tags'); }, /** * @inheritdoc */ saveClicked: function() { var options = { showAlerts: true, success: _.bind(this.handleTagSuccess, this), error: _.bind(this.handleTagError, this), }; this.checkForTagDuplicate(options); }, /** * Handle fetch error */ handleTagError: function() { app.alert.show('collections_error', { level: 'error', messages: 'LBL_TAG_FETCH_ERROR' }); }, /** * Handle fetch success * @param {array} collection */ handleTagSuccess: function(collection) { if (collection.length > 0) { // duplicate found, warn user and quit app.alert.show('tag_duplicate', { level: 'warning', messages: app.lang.get('LBL_EDIT_DUPLICATE_FOUND', 'Tags') }); } else { // no duplicate found, continue with save this.saveModel(); } }, /** * Check to see if new name is a duplicate * @param tagName * @param options */ checkForTagDuplicate: function(options) { this.collection.filterDef = [{ 'name_lower': {'$equals': this.model.get('name').toLowerCase()} }, { 'id': {'$not_equals': this.model.get('id')} }]; this.collection.fetch(options); } }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Tags.RecordView * @alias SUGAR.App.view.views.BaseTagsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) /** * @inheritdoc */ initialize: function(options) { // Initialize collection this.collection = app.data.createBeanCollection('Tags'); this._super('initialize', [options]); }, /** * @inheritdoc */ saveClicked: function() { var options = { showAlerts: true, success: _.bind(this.handleTagSuccess, this), error: _.bind(this.handleTagError, this) }; this.checkForTagDuplicate(options); }, /** * Handle fetch error */ handleTagError: function() { app.alert.show('collections_error', { level: 'error', messages: 'LBL_TAG_FETCH_ERROR' }); }, /** * Handle fetch success * @param {array} collection */ handleTagSuccess: function(collection) { if (collection.length > 0) { // duplicate found, warn user and quit app.alert.show('tag_duplicate', { level: 'warning', messages: app.lang.get('LBL_EDIT_DUPLICATE_FOUND', 'Tags') }); } else { // no duplicate found, continue with save this._super('saveClicked'); } }, /** * Check to see if new name is a duplicate * @param tagName * @param options */ checkForTagDuplicate: function(options) { this.collection.filterDef = [{ 'name_lower': {'$equals': this.model.get('name').toLowerCase()} }, { 'id': {'$not_equals': this.model.get('id')} }]; this.collection.fetch(options); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Tags.CreateView * @alias SUGAR.App.view.views.TagsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', saveAndCreateAnotherButtonName: 'save_create_button', /** * Add event listener for the save and create another button * @override * @param options */ initialize: function(options) { this._super("initialize", [options]); this.alerts = _.extend({}, this.alerts, { showMessageFromServerError: function(error) { if (!this instanceof app.view.View) { app.logger.error('This method should be invoked by Function.prototype.call(),' + 'passing in as argument an instance of this view.'); return; } var name = 'server-error'; this._viewAlerts.push(name); app.alert.show(name, { level: 'warning', messages: error.message ? error.message : 'ERR_GENERIC_SERVER_ERROR', autoClose: true, autoCloseDelay: 9000 }); } }); }, /** * Create new record * @param callback */ createRecordWaterfall: function(callback) { var success = _.bind(function() { var acls = this.model.get('_acl'); if (!_.isEmpty(acls) && acls.access === 'no' && acls.view === 'no') { //This happens when the user creates a record he won't have access to. //In this case the POST request returns a 200 code with empty response and acls set to no. this.alerts.showSuccessButDeniedAccess.call(this); callback(false); } else { this._dismissAllAlerts(); app.alert.show('create-success', { level: 'success', messages: this.buildSuccessMessage(this.model), autoClose: true, autoCloseDelay: 10000, onLinkClick: function() { app.alert.dismiss('create-success'); } }); callback(false); } }, this); var error = _.bind(function(model, error) { if (error.status == 412 && !error.request.metadataRetry) { this.handleMetadataSyncError(error); } else { if (error.code === 'duplicate_tag') { this.alerts.showMessageFromServerError.call(this, error); } else if (error.status == 403) { this.alerts.showNoAccessError.call(this); } else { this.alerts.showServerError.call(this); } callback(true); } }, this); this.saveModel(success, error); }, /** * Save and reload drawer to allow another save */ saveAndCreateAnother: function() { this.initiateSave(_.bind(function() { //reload the drawer if (app.drawer) { app.drawer.load({ layout: 'create', context: { create: true } }); //Change the context on the cancel button app.drawer.getActiveDrawerLayout().context.on('button:' + this.cancelButtonName + ':click', this.multiSaveCancel, this); } }, this)); }, /** * When cancelling, re-render the Tags listview to show updates from previous save */ multiSaveCancel: function() { if (app.drawer) { var route = app.router.buildRoute('Tags'); app.router.navigate(route, {trigger: true}); app.drawer.close(app.drawer.context); } } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Tags.PreviewView * @alias SUGAR.App.view.views.BaseTagsPreviewView * @extends View.Views.Tags.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // Initialize collection this.collection_preview = app.data.createBeanCollection('Tags'); }, /** * @inheritdoc */ saveClicked: function() { var options = { showAlerts: true, success: _.bind(this.handleTagSuccess, this), error: _.bind(this.handleTagError, this) }; this.checkForTagDuplicate(options); }, /** * Handle fetch error */ handleTagError: function() { app.alert.show('collections_error', { level: 'error', messages: 'LBL_TAG_FETCH_ERROR' }); }, /** * Handle fetch success * @param {array} collection_preview */ handleTagSuccess: function(collection_preview) { if (collection_preview.length > 0) { // duplicate found, warn user and quit app.alert.show('tag_duplicate', { level: 'warning', messages: app.lang.get('LBL_EDIT_DUPLICATE_FOUND', 'Tags') }); } else { // no duplicate found, continue with save this._super('saveClicked'); } }, /** * Check to see if new name is a duplicate * @param options */ checkForTagDuplicate: function(options) { this.collection_preview.filterDef = [{ 'name_lower': {'$equals': this.model.get('name').toLowerCase()} }, { 'id': {'$not_equals': this.model.get('id')} }]; this.collection_preview.fetch(options); } }) }, "merge-duplicates": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * View for merge duplicates. * * @class View.Views.Base.Tags.MergeDuplicatesView * @alias SUGAR.App.view.views.BaseTagsMergeDuplicatesView * @extends View.Views.Base.MergeDuplicatesView */ ({ // Merge-duplicates View (base) extendsFrom: 'MergeDuplicatesView', /** * Saves primary record and triggers `mergeduplicates:primary:saved` event on success. * Before saving triggers also `duplicate:unformat:field` event. * * @override Checks if the tags in the primary record are unique before saving and only saves * if no duplicates are found * @private */ _savePrimary: function() { var self = this; var primaryRecordName = this.primaryRecord.get('name'); var tagCollection = app.data.createBeanCollection('Tags'); tagCollection.filterDef = { 'filter': [{'name_lower': {'$equals': primaryRecordName.toLowerCase()}}] }; //fetch records that have the same name as the primaryRecord name tagCollection.fetch({ success: function(tags) { //throw a warning if the primaryRecord name is in the tagCollection // and it is not one of the merged records if (tags.length > 0 && _.isEmpty(_.intersection(_.keys(self.rowFields), _.pluck(tags.models, 'id')))) { app.alert.show('tag_duplicate', { level: 'warning', messages: app.lang.get('LBL_EDIT_DUPLICATE_FOUND', 'Tags') }); } else { var fields = self.getFieldNames().filter(function(field) { return app.acl.hasAccessToModel('edit', self.primaryRecord, field); }, self); self.primaryRecord.trigger('duplicate:unformat:field'); self.primaryRecord.save({}, { fieldsToValidate: fields, success: function() { // Trigger format fields again, because they can come different // from the server (e.g: only teams checked will be in the // response, and we still want to display unchecked teams on the // view) self.primaryRecord.trigger('duplicate:format:field'); self.primaryRecord.trigger('mergeduplicates:primary:saved'); }, error: function(model, error) { if (error.status === 409) { app.utils.resolve409Conflict(error, self.primaryRecord, function(model, isDatabaseData) { if (model) { if (isDatabaseData) { self.resetRadioSelection(model.id); } else { self._savePrimary(); } } }); } }, lastModified: self.primaryRecord.get('date_modified'), showAlerts: true, viewed: true, params: {verifiedUnique: true} }); } } }); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "subpanels": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Tags.SubpanelsLayout * @alias SUGAR.App.view.layouts.TagsSubpanelsLayout * @extends View.Layout.Base.SubpanelsLayout */ ({ // Subpanels Layout (base) /** * @inheritdoc */ initialize: function(options) { // Create dynamic subpanel metadata var dSubpanels = app.utils.getDynamicSubpanelMetadata(options.module); if (dSubpanels.components) { _.each(dSubpanels.components, function(sub) { if (sub.layout) { sub['override_paneltop_view'] = 'panel-top-readonly'; } }, this); } // Merge dynamic subpanels with existing metadata options.meta = _.extend( options.meta || {}, dSubpanels ); // Call the parent this._super('initialize', [options]); } }) } }} , "datas": {} }, "Categories":{"fieldTemplates": {} , "views": { "base": { "nested-set-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.NestedSetHeaderpaneView * @alias SUGAR.App.view.views.BaseNestedSetHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // Nested-set-headerpane View (base) extendsFrom: 'HeaderpaneView', /** * @inheritdoc */ _renderHtml: function() { var titleTemplate = Handlebars.compile(this.context.get('title') || app.lang.getAppString('LBL_SEARCH_AND_SELECT')), moduleName = app.lang.get('LBL_MODULE_NAME', this.module); this.title = titleTemplate({module: moduleName}); this._super('_renderHtml'); this.layout.on('selection:closedrawer:fire', _.once(_.bind(function() { this.$el.off(); app.drawer.close(); }, this))); } }) }, "tree": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Tree View (base) plugins: ['JSTree', 'NestedSetCollection'], events: { 'keyup [data-name=search]': '_keyHandler', 'click [data-role=icon-remove]': function() { this.trigger('search:clear'); } }, /** * Default settings. * * @property {Object} _defaultSettings * @property {boolean} _defaultSettings.showMenu Display menu or not. * @property {number} _defaultSettings.liHeight Height (pixels) of row. */ _defaultSettings: { showMenu: true, liHeight: 37 }, /** * Aggregated settings. * @property {Object} _settings */ _settings: null, /** * List of overridden callbacks. * @property {Object} _callbacks */ _callbacks: null, /** * @inheritdoc * * Add listener for 'search:clear' and 'click:add_node_button' events. * Init settings. * Init callbacks. */ initialize: function(options) { this.on('search:clear', function() { var el = this.$el.find('input[data-name=search]'); el.val(''); this._toggleIconRemove(!_.isEmpty(el.val())); this.searchNodeHandler(el.val()); }, this); this._super('initialize', [options]); this._initSettings(); this._initCallbacks(); this.layout.on('click:add_node_button', this.addNodeHandler, this); }, /** * @inheritdoc * * @example Call _renderTree function with the following parameters. * <pre><code> * this._renderTree($('.tree-block'), this._settings, { * onToggle: this.jstreeToggle, * onSelect: this.jstreeSelect * }); * </code></pre> */ _renderHtml: function(ctx, options) { this._super('_renderHtml', [ctx, options]); this._renderTree($('.tree-block'), this._settings, this._callbacks); }, /** * Initialize _settings object. * @return {Object} * @private */ _initSettings: function() { this._settings = { settings: _.extend({}, this._defaultSettings, this.context.get('treeoptions') || {}, this.def && this.def.settings || {} ) }; return this; }, /** * Initialize _callbacks object. * @return {Object} * @private */ _initCallbacks: function() { this._callbacks = _.extend({}, this.context.get('treecallbacks') || {}, this.def && this.def.callbacks || {} ); return this; }, /** * Handle submit in search field. * @param {Event} event * @return {boolean} * @private */ _keyHandler: function(event) { this._toggleIconRemove(!_.isEmpty($(event.currentTarget).val())); if (event.keyCode != 13) return false; this.searchNodeHandler(event); }, /** * Append or remove an icon to the search input so the user can clear the search easily. * @param {boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleIconRemove: function(addIt) { if (addIt && !this.$('i[data-role=icon-remove]')[0]) { this.$el.find('div[data-container=filter-view-search]').append( '<i class="sicon sicon-close add-on" data-role="icon-remove"></i>' ); } else if (!addIt) { this.$('i[data-role=icon-remove]').remove(); } }, /** * Custom add handler. */ addNodeHandler: function() { this.addNode(app.lang.get('LBL_DEFAULT_TITLE', this.module), 'last', false, true, false); }, /** * Custom search handler. * @param {Event} event DOM event. */ searchNodeHandler: function(event) { this.searchNode($(event.currentTarget).val()); }, /** * @inheritdoc */ _dispose: function() { this.off('search:clear'); this._super('_dispose'); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "nested-set-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.NestedSetListLayout * @alias SUGAR.App.view.layouts.BaseNestedSetListLayout * @extends View.Layout */ ({ // Nested-set-list Layout (base) plugins: ['ShortcutSession'], shortcuts: [ 'Sidebar:Toggle' ], /** * @inheritdoc */ loadData: function(options) { var fields = _.union(this.getFieldNames(), (this.context.get('fields') || [])); this.context.set('fields', fields); this._super('loadData', [options]); } }) } }} , "datas": {} }, "Dashboards":{"fieldTemplates": { "base": { "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DashboardsNameField * @alias App.view.fields.BaseDashboardsNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) /** * Formats the value to be used in handlebars template and displayed on * screen. We are overriding this method to translate labels in the name * field within the Dashboard module. * @override */ format: function(value) { return app.lang.get(value, this.model.get('dashboard_module')); } }) } }} , "views": { "base": { "dashboard-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Dashboards.DashboardHeaderpaneView * @alias SUGAR.App.view.views.DashboardsDashboardHeaderpaneView * @extends View.Views.Base.HeaderpaneMainView */ ({ // Dashboard-headerpane View (base) extendsFrom: 'DashboardHeaderpaneMainView', }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DashboardsRecordlistView * @alias SUGAR.App.view.views.BaseDashboardsRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initProperties(); }, /** * Property initialization */ _initProperties: function() { this._fieldsToFetch = ['is_template']; }, /** * @inheritdoc */ getDeleteMessages: function(model) { var messages = {}; var modelName = app.lang.get(model.get('name'), model.get('dashboard_module')); messages.confirmation = app.lang.get('LBL_DELETE_DASHBOARD_CONFIRM', this.module, {name: modelName}); messages.success = app.lang.get('LBL_DELETE_DASHBOARD_SUCCESS', this.module, { name: modelName }); return messages; } }) }, "side-drawer-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Dashboards.SideDrawerHeaderpaneView * @alias SUGAR.App.view.views.DashboardsSideDrawerHeaderpaneView * @extends View.Views.Dashboards.DashboardHeaderpaneView */ ({ // Side-drawer-headerpane View (base) /** * This is a special header for side drawers that contain a dashlet. */ extendsFrom: 'DashboardsDashboardHeaderpaneView', events: { 'mousemove .record-edit-link-wrapper, .record-lock-link-wrapper': 'handleMouseMove', 'mouseleave .record-edit-link-wrapper, .record-lock-link-wrapper': 'handleMouseLeave', 'click [name=edit_button]': 'editClicked', 'click [name=save_button]': 'saveClicked', 'click [name=cancel_button]': 'cancelClicked', 'click [name=create_cancel_button]': 'createCancelClicked', 'click [name=edit_overview_tab_button]': 'editOverviewTabClicked' }, /** * @inheritdoc */ _setMaxWidthForEllipsifiedCell: function($ellipsisCell, width) { $ellipsisCell.css({'max-width': width}); }, /** * Adjusts dropdown menu position. */ adjustDropdownMenu: function() { let $title = this.$('.record-cell'); let $menu = this.$('.dropdown-menu'); // dropdown toggle is 28px wide // dropdown menu is shown to the right of the toggle by default if (($title.outerWidth() - 28 + $menu.width()) > this._containerWidth) { if ($menu.width() < $title.width()) { // show dropdown menu to the left of the toggle $menu.css({right: 0, left: 'auto'}); } else { let maxWidth = this._containerWidth - $title.outerWidth() + 28; $menu.css({'max-width': maxWidth}); } } else { $menu.removeAttr('style'); } }, /** * @override */ bindDataChange: function() { if (!this.model) { return; } this.model.on('change', this._updateTabTitle, this); this.context.on('side-drawer-headerpane:empty-tab-title', this._setEmptyTabTitle, this); this.layout.on('headerpane:adjust_fields', this.adjustDropdownMenu, this); if (!_.isEmpty(this.context.parent) && !_.isEmpty(this.context.parent.parent)) { let rowModel = this.context.parent.parent.get('rowModel'); if (rowModel) { this.listenTo(rowModel, 'change', this._updateTabContent); } } }, /** * Refresh tab content. */ _updateTabContent: function() { app.alert.dismiss('data:sync:success'); this.render(); app.sideDrawer.refreshTab(); }, /** * Update dashboard and record name for active tab. */ _updateTabTitle: function() { let activeTab = app.sideDrawer.getActiveTab(); if (activeTab && activeTab.isFocusDashboard && !activeTab.hasTitle) { activeTab.dashboardName = app.lang.get(this.model.get('name'), activeTab.context.module); if (!activeTab.recordName && activeTab.context.model) { activeTab.recordName = activeTab.context.model.get('name'); } if (!activeTab.context.dataTitle) { let moduleMeta = app.metadata.getModule(activeTab.context.module); let labelColor = (moduleMeta) ? `label-module-color-${moduleMeta.color}` : `label-${activeTab.context.module}`; activeTab.context.dataTitle = { module: app.lang.get('LBL_MODULE_NAME_SINGULAR', activeTab.context.module), view: app.lang.get('LBL_RECORD'), name: activeTab.recordName, labelColor: labelColor }; } activeTab.hasTitle = true; app.sideDrawer.renderTabs(); } }, /** * Set default title name if tab is empty */ _setEmptyTabTitle: function() { let activeTab = app.sideDrawer.getActiveTab(); if (activeTab && activeTab.context && activeTab.context.module) { let moduleMeta = app.metadata.getModule(activeTab.context.module); let labelColor = (moduleMeta) ? `label-module-color-${moduleMeta.color}` : `label-${activeTab.context.module}`; activeTab.dashboardName = app.lang.get('LBL_NO_DASHBOARD_CONFIGURED'); activeTab.context.dataTitle = { module: app.lang.get('LBL_MODULE_NAME_SINGULAR', activeTab.context.module), view: app.lang.get('LBL_FOCUS_DRAWER_DASHBOARD'), labelColor: labelColor }; activeTab.hasTitle = true; app.sideDrawer.renderTabs(); } }, /** * @inheritdoc */ _render: function() { if (this.context.get('create') && !this.context.get('emptyDashboard')) { this.createView = true; this.action = 'edit'; } else { this.createView = false; this.dashboardTitle = !this.context.get('emptyDashboard') && app.sideDrawer.getActiveTab() && app.sideDrawer.getActiveTab().isFocusDashboard; this.action = 'view'; } this._super('_render'); }, /** * @inheritdoc */ unbind: function() { this._super('unbind'); this.layout.off('headerpane:adjust_fields', this.adjustDropdownMenu); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Dashboards.RecordView * @alias SUGAR.App.view.views.BaseDashboardsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc * Additionaly it makes the metadata (dashboard components description) available through the record call; * it adds the FilterSharing plugin to the list of plugins used in the view. */ initialize: function(options) { this.options.context.addFields(['metadata']); this.plugins = _.union((this.plugins || []), ['FilterSharing']); this._super('initialize', [options]); }, /** * @inheritdoc * Additionaly it calls the method responsible for sharing the filters used * on a list view dashlet with the teams the dashboard is shared with. */ _saveModel: function() { var options; var successCallback = _.bind(function() { this.triggerListviewFilterUpdate(); // Loop through the visible subpanels and have them sync. This is to update any related // fields to the record that may have been changed on the server on save. _.each(this.context.children, function(child) { if (child.get('isSubpanel') && !child.get('hidden')) { if (child.get('collapsed')) { child.resetLoadFlag({recursive: false}); } else { child.reloadData({recursive: false}); } } }); if (this.createMode) { app.navigate(this.context, this.model); } else if (!this.disposed && !app.acl.hasAccessToModel('edit', this.model)) { //re-render the view if the user does not have edit access after save. this.render(); } }, this); var errorCallBack = _.bind(function(model, error) { if (error.status === 412 && !error.request.metadataRetry) { this.handleMetadataSyncError(error); } else if (error.status === 409) { app.utils.resolve409Conflict(error, this.model, _.bind(function(model, isDatabaseData) { if (model) { if (isDatabaseData) { successCallback(); } else { this._saveModel(); } } }, this)); } else if (error.status === 403 || error.status === 404) { this.alerts.showNoAccessError.call(this); } else { this.editClicked(); } }, this); //Call editable to turn off key and mouse events before fields are disposed (SP-1873) this.turnOffEvents(this.fields); options = { showAlerts: true, success: successCallback, error: errorCallBack, lastModified: this.model.get('date_modified'), viewed: true }; // ensure view and field are sent as params so collection-type fields come back in the response to PUT requests // (they're not sent unless specifically requested) options.params = options.params || {}; if (this.context.has('dataView') && _.isString(this.context.get('dataView'))) { options.params.view = this.context.get('dataView'); } if (this.context.has('fields')) { options.params.fields = this.context.get('fields').join(','); } options = _.extend({}, options, this.getCustomSaveOptions(options)); this.model.save({}, options); }, /** * @inheritdoc */ getDeleteMessages: function() { var messages = {}; var modelName = app.lang.get(this.model.get('name'), this.model.get('dashboard_module')); messages.confirmation = app.lang.get('LBL_DELETE_DASHBOARD_CONFIRM', this.module, {name: modelName}); messages.success = app.lang.get('LBL_DELETE_DASHBOARD_SUCCESS', this.module, { name: modelName }); return messages; } }) }, "dashboard-fab": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Dashboards.DashboardFabView * @alias SUGAR.App.view.views.BaseDashboardsDashboardFabView * @extends View.Views.Base.DashboardFabView */ ({ // Dashboard-fab View (base) /** * View to extend */ extendsFrom: 'DashboardFabView', /** * Are we in a config layout? */ configLayout: false, /** * @param options */ initialize: function(options) { if (this._inConfigLayout(options)) { // Extend for omniconsole config specific events this.events = _.extend({}, this.events, { 'click [name=restore_tab_button]': 'restoreTabClicked', 'click [name=configure_summary_button]': 'openSugarLiveConfig' }); this.configLayout = true; } this.events = _.extend({}, this.events, { 'click [name=restore_dashlets_button]': 'handleRestoreDashletsClick' }); this._super('initialize', [options]); }, /** * Open the SugarLive Summary configuration drawer. On closing, the * SugarLive configuration panel should reopen. */ openSugarLiveConfig: function() { var drawers = app.drawer._getDrawers() || {}; var $topDrawer = drawers.$top || {}; // get the console config component var configComponent = this.closestComponent('omnichannel-console-config'); // if a drawer is already open in the background if (!_.isEmpty($topDrawer)) { if (!_.isUndefined(configComponent)) { // close the console config component configComponent.boundCloseImmediately(); } } app.drawer.open({ layout: 'config-drawer', context: { module: 'SugarLive' } }, function(model) { if (model && configComponent) { configComponent.inSync(true); } }); }, /** * @override * * This function updates button visibilities when the dashboard metadata * changes, or when switching tabs. Override base view to open the buttons * when switching tabs in the config drawer. */ updateButtonVisibilities: function() { // If not in config layout, call base view function if (!this.configLayout) { return this._super('updateButtonVisibilities'); } // In config drawer, the Add Dashlet button should be visible everywhere // except the search tab var activeTab = this._getActiveDashboardTab(); this.toggleFabButton(['add_dashlet_button'], activeTab !== 0); // Set timeout to allow tab to render before opening buttons var self = this; setTimeout(function() { self.openFABs(); }, 200); }, /** * Util to get the current active dashboard tab. * @return {number} * @private */ _getActiveDashboardTab: function() { return this.context.get('activeTab'); }, /** * Util method to determine if we are in a config layout. Used to allow * dashlet to render an empty record view for config displays * * @return {boolean} Whether we are in a config layout * @private */ _inConfigLayout: function(options) { var context = options.context; while (context) { if (context.get('config-layout')) { return true; } context = context.parent; } return false; }, /** * Get the omnichannel dashboard config component * * @return {Object} the component * @private */ _getOmnichannelDashboardConfigComponent: function() { var component; if (this.configLayout) { component = this.closestComponent('omnichannel-dashboard-config'); } return component; }, /** * Handle restore tab button click */ restoreTabClicked: function() { if (this.configLayout) { var component = this._getOmnichannelDashboardConfigComponent(); if (component) { component.context.trigger('dashboard:restore-tab:clicked', this._getActiveDashboardTab()); } } }, /** * Handle restore dashlets button click */ handleRestoreDashletsClick: function() { app.alert.show('restore_dashlet_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_RESTORE_DEFAULT_PORTAL_DASHLETS_CONFIRM', 'Dashboards'), onConfirm: _.bind(function() { this.restoreDashlets(); }, this) }); }, /** * Restores dashlets on the dashboard */ restoreDashlets: function() { // get the dashboard component var component = this.closestComponent('dashboard'); if (component) { component.layout.trigger('dashboard:restore_dashlets_button:click', component.context); } } }) }, "selection-list-context": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * * This view displays the selected records at the top of a selection list. It * also allows to unselect them. * * @class View.Views.Base.DashboardsSelectionListContextView * @alias SUGAR.App.view.views.DashboardsSelectionListContextView * @extends View.View.BaseSelectionListContext */ ({ // Selection-list-context View (base) extendsFrom: 'SelectionListContext', /** * Adds a pill in the template. * This overrides the base fucntion in order to translate the dashboard name * * @param {Data.Bean|Object|Array} models The model, set of model attributes * or array of those corresponding to the pills to add. */ addPill: function(models) { models = _.isArray(models) ? models : [models]; if (_.isEmpty(models)) { return; } var pillsAttrs = []; var pillsIds = _.pluck(this.pills, 'id'); _.each(models, function(model) { var modelName = app.lang.get(model.get('name'), model.get('dashboard_module')); if (modelName && !_.contains(pillsIds, model.id)) { pillsAttrs.push({id: model.id, name: modelName}); } }); this.pills.push.apply(this.pills, pillsAttrs); this._debounceRender(); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "dashboard": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * The outer layout of the dashboard. * * This layout contains the header view and wraps the daslet-main layout. * The layouts for each dashboard are stored in the server. * * @class View.Layouts.Dashboards.DashboardLayout * @alias SUGAR.App.view.layouts.DashboardsDashboardLayout * @extends View.BaseLayout */ ({ // Dashboard Layout (base) extendsFrom: 'BaseLayout', plugins: [ 'ActionButton', ], className: 'row-fluid', //FIXME We need to remove this. TY-1132 will address it. dashboardLayouts: { 'record': 'record-dashboard', 'records': 'list-dashboard', 'search': 'search-dashboard' }, /** * Mapping of metadata files based on the dashboard names * * @property {Object} */ metaFileNames: { 'da438c86-df5e-11e9-9801-3c15c2c53980': 'renewal-console', 'c108bb4a-775a-11e9-b570-f218983a1c3e': 'agent-dashboard', '1c59e4d8-b54a-11ee-9d94-095590d26ca4': 'bdr-dashboard', '6483f6a4-b54a-11ee-9d94-095590d26ca4': 'sales-rep-dashboard', 'a23e0174-b54a-11ee-9d94-095590d26ca4': 'sales-manager-dashboard', 'cf9dde82-b54a-11ee-9d94-095590d26ca4': 'marketing-dashboard', '00aa861a-b54b-11ee-9d94-095590d26ca4': 'customer-success-dashboard', '1f821616-b54b-11ee-9d94-095590d26ca4': 'executive-dashboard' }, events: { 'click [data-action=create]': 'createClicked' }, error: { //Dashboard is a special case where a 404 here shouldn't break the page, //it should just send us back to the default homepage handleNotFoundError: function(error) { var currentRoute = Backbone.history.getFragment(); if (currentRoute.substr(0, 5) === 'Home/') { app.router.redirect('#Home'); //Prevent the default error handler return false; } }, handleValidationError: function(error) { return false; } }, /** * What is the current Visible State of the dashboard */ dashboardVisibleState: 'open', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union((this.plugins || []), ['FilterSharing']); var context = options.context; var module = context.parent && context.parent.get('module') || context.get('module'); if (options.meta && options.meta.method && options.meta.method === 'record' && !context.get('modelId')) { context.set('create', true); } var hasDashboardModels; // The dashboard can be used to display facets on the search page. // This is a special use case for dashboards. // This checks to see if we're in the search context (i.e. the search page). if (context.parent && context.parent.get('search')) { // Note that dashboard.js is initialized twice because `navigateLayout` will call initComponents directly, // which creates a new context for each dashboard. // See `navigateLayout` for more details. // Also note that the module for the facets dashboard is set to `Home` in the search layout metadata. // Therefore, we have two brother contexts, both of which are in the `Home` module. // One is the initial dashboard that is created when the search layout is created. // The other is instantiated by the dashboard's `navigateLayout` method. var contextBro = context.parent.getChildContext({module: 'Home'}); hasDashboardModels = contextBro.get('collection') && contextBro.get('collection').length; if (hasDashboardModels) { context.set({ // For the search page, we hardcode the facet dashboard index to 0. // This is possible because in search, we only allow the // facets dashboard. // See `loadData` for more details. model: contextBro.get('collection').at(0), collection: this._getNewDashboardObject('collection', context), skipFetch: true }); } } if (!hasDashboardModels) { var model; if (_.contains(['multi-line', 'focus'], context.get('layout'))) { // On the multi-line list and focus view, side drawer/focus drawer, the dashlets need // the correct model context, which is set here. var layout = options && options.layout && options.layout.layout; if (layout) { model = layout.model; model.set('view_name', layout.context.get('layout')); model.dashboardModule = layout.context.get('module'); } } else { model = this._getNewDashboardObject('model', context); } if (context.get('modelId')) { model.set('id', context.get('modelId'), {silent: true}); } context.set({ model: model, collection: this._getNewDashboardObject('collection', context) }); } this._super('initialize', [options]); this._bindButtonEvents(); this.model.on('setMode', function(mode) { if (mode === 'edit' || mode === 'create') { this.$('.dashboard').addClass('edit'); } else { this.$('.dashboard').removeClass('edit'); } }, this); var defaultLayout = this.closestComponent('sidebar'); if (defaultLayout) { this.listenTo(defaultLayout, 'sidebar:state:changed', function(state) { this.dashboardVisibleState = state; }, this); try { this.dashboardVisibleState = defaultLayout.isSidePaneVisible() ? 'open' : 'close'; } catch (error) { // this happens when the dashboard component is initially created because the defaultLayout doesn't // have _hideLastStateKey key set yet. Just ignore this for now as with the way dashboards work // it this code will get run again once the logic below selects which dashboard to show. } } if (module === 'Home' && context.has('modelId')) { // save it as last visit var lastVisitedStateKey = this.getLastStateKey(); app.user.lastState.set(lastVisitedStateKey, context.get('modelId')); } }, /** * Get the dashboard model attributes. * * @return {Object} Dashboard model fields to save. * @private */ _getDashboardModelAttributes: function() { var ctx = this.context && this.context.parent || this.context; var dashboardModule = ctx.get('module'); var viewName = dashboardModule === 'Home' ? '' : ctx.get('layout'); return { 'assigned_user_id': app.user.id, 'dashboard_module': dashboardModule, 'view_name': viewName }; }, /** * Binds the button events that are specific to the record pane. * * @protected */ _bindButtonEvents: function() { this.context.on('button:save_button:click', this.handleSave, this); }, /** * Overrides {@link View.Layout#initComponents} to trigger `change:metadata` * event if we are in the search results page. * * For other dashboards than the facet dashboard, `change:metadata` is * triggered by {@link View.Fields.Base.Home.LayoutbuttonField} but we don't * use this field in the facets dashboard so we need to trigger it here. * * @override */ initComponents: function(components, context, module) { this._super('initComponents', [components, context, module]); if (this.isSearchContext()) { // For non-search dashboards, `change:metadata` is triggered by the // `layoutbutton.js`. We don't use this field in the facets // dashboard, so we need to trigger it here. this.model.trigger('change:metadata'); } }, /** * Indicates if we are in the search page or not. * * @return {boolean} `true` means we are in the search page. */ isSearchContext: function() { return this.context.parent && this.context.parent.get('search'); }, /** * Gets the brother context. * * @param {string} module The module to get the brother context from. * @return {Core.Context} The brother context. */ getContextBro: function(module) { return this.context.parent.getChildContext({module: module}); }, /** * @inheritdoc */ loadData: function(options) { // Dashboards store their own metadata as part of their model. // For search facet dashboard, we do not want to load the dashboard // metadata from the database. Instead, we build the metadata below. if (this.isSearchContext()) { // The model does not have metadata the first time this function // is called. In subsequent calls, the model should have metadata // so we do not need to fetch it. if (this.model.has('metadata')) { return; } this._loadSearchDashboard(); this.context.set('skipFetch', true); this.navigateLayout('search'); return; } if (this.context.parent && !this.context.parent.isDataFetched()) { var parent = this.context.parent.get('modelId') ? this.context.parent.get('model') : this.context.parent.get('collection'); if (parent) { parent.once('sync', function() { this._super('loadData', [options]); }, this); } } else { this._super('loadData', [options]); } }, /** * Loads the facet dashboard for the search page, and add it. * * @private */ _loadSearchDashboard: function() { var dashboardMeta = this._getInitialDashboardMetadata(); var model = this._getNewDashboardObject('model', this.context); // In `dashMeta`, we have a `metadata` property which contains all // the metadata needed for the dashboard. model.set(dashboardMeta); this.collection.add(model); }, /** * Navigate to the create layout when create button is clicked. * * @param {Event} evt Mouse event. */ createClicked: function(evt) { if (this.model.dashboardModule === 'Home') { var route = app.router.buildRoute(this.module, null, 'create'); app.router.navigate(route, {trigger: true}); } else { this.navigateLayout('create'); } }, /** * Places only components that include the Dashlet plugin and places them in the 'main-pane' div of * the dashlet layout. * @param {app.view.Component} component * @private */ _placeComponent: function(component) { var dashboardEl = this.$('[data-dashboard]'); var css = this.context.get('create') ? ' edit' : ''; if (dashboardEl.length === 0) { dashboardEl = $('<div></div>').attr({ 'class': 'cols row-fluid' }); this.$el.append( $('<div></div>') .addClass('dashboard bg-[--background-base] w-full absolute' + css) .attr({'data-dashboard': 'true'}) .append(dashboardEl) ); } else { dashboardEl = dashboardEl.children('.row-fluid'); } dashboardEl.append(component.el); }, /** * If current context doesn't contain dashboard model id, * it will trigger set default dashboard to create default metadata */ bindDataChange: function() { if (this.isSearchContext()) { return; } var modelId = this.context.get('modelId'); if (!(modelId && this.context.get('create')) && this.collection) { // On the search page, we don't want to save the facets dashboard // in the database, so we don't need to listen to changes on the // collection nor do we need to call `setDefaultDashboard`. this.collection.on('reset', this.setDefaultDashboard, this); } this.context.on('dashboard:restore-dashboard:clicked', this.restoreConsoleDashlets, this); }, /** * Return list of fields for Action Buttons * * @return {Object|null} */ getFieldsForAB: function() { const comp = this.closestComponent('row-model-data'); if (comp && comp.layout) { const model = comp.layout.model; if (model && model.fields) { return model.fields; } } return null; }, /** * Set or render the appropriate dashboard for display. * * The appropriate dashboard is selected using this order of preference: * 1. The last viewed dashboard * 2. The last modified default dashboard * 3. The last modified favorite dashboard * 4. Render dashboard-empty template */ setDefaultDashboard: function() { if (this.disposed) { return; } var lastVisitedStateKey = this.getLastStateKey(); var lastViewed = app.user.lastState.get(lastVisitedStateKey); var model; // FIXME: SC-4915 will change this to rely on the `hidden` context flag // instead. var hasParentContext = this.context && this.context.parent; var parentModule = hasParentContext && this.context.parent.get('module') || 'Home'; // this.collection contains all the default and favorited dashboards // ordered by date modified (descending). if (this.collection.length > 0) { var currentModule = this.context.get('module'); // Use the last viewed dashboard. if (lastViewed) { var lastVisitedModel = this.collection.get(lastViewed); // It should navigate to the last viewed dashboard if available, // and it should clean out the cached record in lastState if (!_.isEmpty(lastVisitedModel)) { app.user.lastState.set(lastVisitedStateKey, ''); model = lastVisitedModel; } } // If there is no dashboard found yet, // use the last modified default dashboard. if (!model) { model = _.find(this.collection.models, function(model) { return model.get('default_dashboard'); }); } // If there is no dashboard found yet, // use the last modified favorite dashboard. if (!model) { // If we get in here, there are no default dashboards in the // collection, so the collection only has favorite dashboards. model = _.first(this.collection.models); } if (currentModule == 'Home' && _.isString(lastViewed) && lastViewed.indexOf('bwc_dashboard') !== -1) { app.router.navigate(lastViewed, {trigger: true}); } else { // use the _navigate helper this._navigate(model); } // There are no favorite or default dashboards, so the collection // is empty. } else { this._renderEmptyTemplate(); if (this.context && this.context.children[0]) { this.context.children[0].trigger('side-drawer-headerpane:empty-tab-title'); } } }, /** * Restore tab metadata for console Dashboards (Service and Renewals) * * @param tabIndex {number} index of the tab for which metadata needs to be reset */ restoreConsoleDashlets: function(tabIndex) { var dashboardId = this.model.get('id'); var metaFileName = this.metaFileNames[dashboardId]; if (metaFileName) { var attributes = { id: dashboardId }; var params = { dashboard: metaFileName, tab_index: tabIndex, dashboard_module: 'Home', }; var url = app.api.buildURL('Dashboards', 'restore-tab-metadata', attributes, params); app.api.call('update', url, null, { success: _.bind(function(response) { var dashboard = this.layout.getComponent('dashboard'); if (dashboard) { var tabbedDash = dashboard.getComponent('tabbed-dashboard'); tabbedDash.model.set(response); tabbedDash.model.setSyncedAttributes({}); } }, this) }); } }, /** * Gets initial dashboard metadata * * @return {Object} dashboard metadata * @private */ _getInitialDashboardMetadata: function() { var layoutName = this.dashboardLayouts[this.context.parent && this.context.parent.get('layout') || 'record']; var initDash = app.metadata.getLayout(this.model.dashboardModule, layoutName) || {}; return initDash; }, /** * Build the cache key for last visited dashboard * Combine parent module and view name to build the unique id * * @return {string} hash key. */ getLastStateKey: function() { if (this._lastStateKey) { return this._lastStateKey; } var model = this.context.get('model'); var view = model.get('view_name'); var module = model.dashboardModule; var key = module + '.' + view; // For side drawers using the row-model-data layout, we need to mock the // component being a "Home" module component so the last state key is // built correctly var sideDrawerLayouts = ['multi-line', 'focus']; if (this.layout && this.layout.context && this.layout.context.parent && _.contains(sideDrawerLayouts, this.layout.context.parent.get('layout'))) { this._lastStateKey = app.user.lastState.key(key, { module: 'Home', meta: this.meta }); } else { this._lastStateKey = app.user.lastState.key(key, this); } return this._lastStateKey; }, /** * Utility method to use when trying to figure out how we need to navigate when switching dashboards * * @param {Backbone.Model} (dashboard) The dashboard we are trying to navigate to * @private */ _navigate: function(dashboard) { if (this.disposed) { return; } var hasParentContext = (this.context && this.context.parent); var hasModelId = (dashboard && dashboard.has('id')); var actualModule = (hasParentContext) ? this.context.parent.get('module') : this.module; var isHomeModule = (actualModule === 'Home'); if (hasParentContext && hasModelId) { // we are on a module and we have an dashboard id this._navigateLayout(dashboard.get('id')); } else if (hasParentContext && !hasModelId) { // we are on a module but we don't have a dashboard id this._navigateLayout('list'); } else if (!hasParentContext && hasModelId && isHomeModule) { // we on the Home module and we have a dashboard id app.navigate(this.context, dashboard); } else if (isHomeModule) { // we on the Home module and we don't have a dashboard var route = app.router.buildRoute(this.module); app.router.navigate(route, {trigger: true}); } }, /** * Intercept the navigateLayout calls to make sure that the dashboard we are currently on didn't change. * If it did, we need to prompt and make sure they want to continue or cancel. * * @param {string} dashboard What dashboard do we want to display * @return {boolean} * @private */ _navigateLayout: function(dashboard) { var onConfirm = _.bind(function() { this.navigateLayout(dashboard); }, this); var headerpane = this.getComponent('dashboard-headerpane'); // if we have a headerpane and it was changed then run the warnUnsavedChanges method if (headerpane && headerpane.changed) { return headerpane.warnUnsavedChanges( onConfirm, undefined, _.bind(function() { // when the cancel button is presses, we need to clear out the collection // because it messes with the add dashlet screen. this.collection.reset([], {silent: true}); }, this) ); } // if we didn't have a headerpane or we did have one, but nothing changed, just run the normal method onConfirm(); }, /** * For the RHS dashboard, this method loads entire dashboard component * * @param {string} id dashboard id. This id can be the dashboard id, or * the following strings: create, list, search. * @param {string} type (Deprecated) the dashboard type. */ navigateLayout: function(id, type) { if (!_.isUndefined(type)) { // TODO: Remove the `type` parameter. This is to be done in TY-654 app.logger.warn('The `type` parameter to `View.Layouts.Dashboards.DashboardLayout.navigateLayout`' + 'has been deprecated since 7.9.0.0. Please update your code to stop using it.'); } var layout = this.layout; var lastVisitedStateKey = this.getLastStateKey(); this.dispose(); //if dashboard layout navigates to the different dashboard, //it should store last visited dashboard id. if (!_.contains(['create', 'list'], id)) { app.user.lastState.set(lastVisitedStateKey, id); } var ctxVars = {}; if (id === 'create') { ctxVars.create = true; } else if (id !== 'list') { ctxVars.modelId = id; } // For search dashboards, use the search-dashboard-headerpane // For multi-line-list and focus dashboards, use the side-drawer-header // Otherwise, use the dashboard-headerpane var headerPane; var actionButtons; var sideDrawerHeaderLayouts = ['multi-line', 'focus']; if (id === 'search') { headerPane = { view: 'search-dashboard-headerpane' }; actionButtons = {}; } else if (layout.context && layout.context.parent && _.contains(sideDrawerHeaderLayouts, layout.context.parent.get('layout'))) { headerPane = { view: 'side-drawer-headerpane', loadModule: 'Dashboards' }; actionButtons = { view: 'dashboard-fab', loadModule: 'Dashboards' }; } else { headerPane = { view: 'dashboard-headerpane', loadModule: 'Dashboards', }; actionButtons = { view: 'dashboard-fab', loadModule: 'Dashboards' }; } var component = { // Note that we reinitialize the dashboard layout itself, creating a new context (forceNew: true) layout: { type: 'dashboard', components: (id === 'list') ? [] : [ headerPane, { layout: 'dashlet-main' }, actionButtons ], last_state: { id: 'last-visit' } }, context: _.extend({ module: 'Home', forceNew: true }, ctxVars), loadModule: 'Dashboards' }; layout.initComponents([component]); layout.removeComponent(0); layout.loadData({}); layout.render(); }, /** * @inheritdoc */ unbindData: function() { if (this.collection) { this.collection.off('reset', this.setDefaultDashboard, this); } if (this.context.parent) { var model = this.context.parent.get('model'); var collection = this.context.parent.get('collection'); if (model) { model.off('sync', null, this); } if (collection) { collection.off('sync', null, this); } } this._super('unbindData'); }, /** * Returns a Dashboard Model or Dashboard Collection based on modelOrCollection * * @param {string} modelOrCollection The return type, 'model' or 'collection' * @param {Object} context * @return {Bean|BeanCollection} * @private */ _getNewDashboardObject: function(modelOrCollection, context) { var obj; var ctx = context && context.parent || context; var module = ctx.get('module') || context.get('module'); var layoutName = ctx.get('layout') || ''; /** * Overrides the datamanager sync with dashboard specific functionality. * * sync overrides {@link Data.DataManager#sync}. */ var sync = function(method, model, options) { var callbacks = app.data.getSyncCallbacks(method, model, options); var getEditableFields = function() { var fieldNames = _.keys(model.attributes); var editableFields = _.filter(fieldNames, function(fieldName) { return app.acl.hasAccess('edit', 'Dashboards', {field: fieldName}); }); if (editableFields.indexOf('id') < 0) { editableFields.push('id'); } return model.toJSON({ fields: editableFields }); }; // When favoriting, use the favorite endpoint to be consistent // with the rest of sidecar. if (options.favorite) { return app.api.favorite( 'Dashboards', model.id, model.isFavorite(), callbacks, options.apiOptions ); } options = app.data.parseOptionsForSync(method, model, options); // There is no max limit for number of dashboards per module view. if (options && options.params) { options.params.max_num = -1; } if (_.isEmpty(options.params)) { options.params = {}; } options.params.filter = [{ 'dashboard_module': module, '$or': [ {'$favorite': ''}, {'default_dashboard': 1} ] }]; options.order_by = {'date_modified': 'DESC'}; if (module !== 'Home') { options.params.filter.push({view_name: layoutName}); } app.data.trigger('data:sync:start', method, model, options); model.trigger('data:sync:start', method, options); // Only update the fields that the current user is allowed to modify app.api.records(method, model.apiModule, getEditableFields(), options.params, callbacks); }; if (module === 'Home') { layoutName = ''; } switch (modelOrCollection) { case 'model': obj = this._getNewDashboardModel(module, layoutName, sync); break; case 'collection': obj = this._getNewDashboardCollection(module, layoutName, sync); break; } return obj; }, /** * Returns a new Dashboard Bean with proper view_name and sync function set. * * @param {string} module The name of the module we're in * @param {string} layoutName The name of the layout * @param {Function} syncFn The sync function to use * @param {boolean} [getNew=true] If you want a new instance or just the * Dashboard definition. * @return {Dashboard} a new Dashboard Bean * @private */ _getNewDashboardModel: function(module, layoutName, syncFn, getNew) { getNew = (_.isUndefined(getNew)) ? true : getNew; var Dashboard = app.Bean.extend({ sync: syncFn, apiModule: 'Dashboards', module: 'Home', dashboardModule: module, maxColumns: (module === 'Home') ? 3 : (layoutName === 'multi-line' ? 2 : 1), minColumnSpanSize: (module === 'Home') ? 4 : (layoutName === 'multi-line' ? 6 : 12), defaults: { view_name: layoutName }, fields: {} }); return (getNew) ? new Dashboard() : Dashboard; }, /** * Returns a new DashboardCollection with proper view_name and sync function set * * @param {string} module The name of the module we're in * @param {string} layoutName The name of the layout * @param {Function} syncFn The sync function to use * @param {boolean} [getNew=true] If you want a new instance or just the * DashboardCollection definition. * @return {DashboardCollection} A new Dashboard BeanCollection * @private */ _getNewDashboardCollection: function(module, layoutName, syncFn, getNew) { getNew = (_.isUndefined(getNew)) ? true : getNew; var Dashboard = this._getNewDashboardModel(module, layoutName, syncFn, false); var DashboardCollection = app.BeanCollection.extend({ sync: syncFn, apiModule: 'Dashboards', module: 'Home', dashboardModule: module, model: Dashboard }); return (getNew) ? new DashboardCollection() : DashboardCollection; }, /** * Collects params for Dashboard model save * * @return {Object} The dashboard model params to pass to its save function * @private */ _getDashboardModelSaveParams: function() { var params = { silent: true, //Don't show alerts for this request showAlerts: false }; params.error = _.bind(this._renderEmptyTemplate, this); params.success = _.bind(function(model) { if (!this.disposed) { this._navigate(model); } }, this); return params; }, /** * Gets the empty dashboard view template from dashboards/clients/base/views * and renders it to <pre><code>this.$el</code></pre> * * @private */ _renderEmptyTemplate: function() { var headerPane = {}; var layout = this.layout; if (layout) { var isSideDrawer = layout.$el.closest('#side-drawer').length > 0; if (isSideDrawer) { headerPane = { view: 'side-drawer-headerpane', loadModule: 'Dashboards' }; } var component = { layout: { type: 'dashboard', components: [ headerPane, { view: 'dashboard-empty', loadModule: 'Dashboards' }, ], last_state: { id: 'last-visit' } }, context: { module: 'Home', forceNew: true, create: true, emptyDashboard: true }, loadModule: 'Dashboards' }; layout.initComponents([component]); layout.removeComponent(0); layout.loadData({}); layout.render(); } }, /** * Saves the dashboard to the server. */ handleSave: function() { var attributes = this._getDashboardModelAttributes(); // Favorite new dashboards by default if (!this.model.get('id')) { attributes.my_favorite = true; } this.model.save(attributes, { showAlerts: true, fieldsToValidate: { 'name': { required: true }, 'metadata': { required: true } }, success: _.bind(function() { this.model.unset('updated'); this.triggerListviewFilterUpdate(); if (this.context.get('create')) { // We have a parent context only for dashboards in the RHS. if (this.context.parent) { this.getContextBro('Home').get('collection').add(this.model); this.navigateLayout(this.model.id); } else { app.navigate(this.context, this.model); } } else { this.context.trigger('record:set:state', 'view'); } }, this), error: function() { app.alert.show('error_while_save', { level: 'error', title: app.lang.get('ERR_INTERNAL_ERR_MSG'), messages: ['ERR_HTTP_500_TEXT_LINE1', 'ERR_HTTP_500_TEXT_LINE2'] }); } }); }, /** * @inheritdoc */ _dispose: function() { var defaultLayout = this.closestComponent('sidebar'); if (defaultLayout) { this.stopListening(defaultLayout); } this.dashboardLayouts = null; this.context.off('dashboard:restore-dashboard:clicked', this.restoreConsoleDashlets, this); this._super('_dispose'); } }) } }} , "datas": {} }, "OutboundEmail":{"fieldTemplates": { "base": { "email-authorize": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.OutboundEmail.EmailAuthorizeField * @alias SUGAR.App.view.fields.BaseOutboundEmailEmailAuthorizeField * @extends View.Fields.Base.ButtonField */ ({ // Email-authorize FieldTemplate (base) extendsFrom: 'ButtonField', /** * email-provider field name */ smtpField: 'mail_smtptype', events: { 'click .btn': 'handleAuthorizeStart' }, /** * SMTP providers requiring OAuth2. * * @property {Object} */ oauth2Types: null, /** * Stores dynamic window message listener to clear it on dispose */ messageListeners: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.oauth2Types = { google_oauth2: { application: 'GoogleEmail', dataSource: 'googleEmailRedirect' }, exchange_online: { application: 'MicrosoftEmail', dataSource: 'microsoftEmailRedirect' } }; }, /** * Handles storing/retrieving OAuth2 values when switching between OAuth2 tabs */ bindDataChange: function() { this.listenTo(this.model, `change:${this.smtpField}`, function() { var previousValue = this.model.previous(this.smtpField); var newValue = this.model.get(this.smtpField); if (!this.oauth2Types[newValue]) { // This is not an OAuth2 tab this.model.set('mail_authtype', null); } else { // This is an OAuth2 tabcheck var oauth2Values = { eapm_id: this.model.get('eapm_id'), authorized_account: this.model.get('authorized_account'), mail_smtpuser: this.model.get('mail_smtpuser') }; if (_.isUndefined(previousValue)) { // When the record first loads on an OAuth2 tab, store the values // for that tab this.storeOauth2Values(newValue, oauth2Values); } else { // When switching from an OAuth2 tab, store the values for that // tab if (this.oauth2Types[previousValue]) { this.storeOauth2Values(previousValue, oauth2Values); } } } this.render(); }); this._super('bindDataChange'); }, /** * Stores the OAuth2 authorization values for an OAuth2 tab * * @param {string} smtpType the unique indicator of the OAuth2 tab * @param {Object} values to store for the OAuth2 tab */ storeOauth2Values: function(smtpType, values) { _.extendOwn(this.oauth2Types[smtpType], values || {}); }, /** * Handles auth when the button is clicked. */ handleAuthorizeStart: function() { var smtpType = this.model.get(this.smtpField); if (this.oauth2Types[smtpType] && this.oauth2Types[smtpType].auth_url) { let authorizationListener = _.bind(function(e) { if (this) { this.handleAuthorizeComplete(e, smtpType); } window.removeEventListener('message', authorizationListener); }, this); window.addEventListener('message', authorizationListener); this.messageListeners.push(authorizationListener); var height = 600; var width = 600; var left = (screen.width - width) / 2; var top = (screen.height - height) / 4; var submitWindow = window.open( '/', '_blank', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',resizable=1' ); submitWindow.location.href = 'about:blank'; submitWindow.location.href = this.oauth2Types[smtpType].auth_url; } }, /** * Handles the oauth completion event. * Note that the EAPM bean has already been saved at this point. * * @param {Object} e * @param {string} smtpType * @return {boolean} True if success, otherwise false */ handleAuthorizeComplete: function(e, smtpType) { var data = JSON.parse(e.data) || {}; if (!data.dataSource || !this.oauth2Types[smtpType] || data.dataSource !== this.oauth2Types[smtpType].dataSource) { return false; } if (data.eapmId && data.emailAddress && data.userName) { // Store the authorization information for the OAuth smtpType this.storeOauth2Values(smtpType, { eapm_id: data.eapmId, authorized_account: data.emailAddress, mail_smtpuser: data.userName }); } else { app.alert.show('error', { level: 'error', messages: app.lang.get('LBL_EMAIL_AUTH_FAILURE', this.module) }); } this.render(); return true; }, /** * Extends the parent _render to hide the field until connector information * is loaded in order to guarantee that OAuth2 tabs show the correct * connector authorization elements * @private */ _render: function() { // Load connector information before rendering if (!this.connectorsLoaded) { this.hide(); if (!this.connectorsAreLoading) { this.connectorsAreLoading = true; this._loadOauth2TypeInformation(_.bind(function() { if (!this.disposed) { this.connectorsAreLoading = false; this.connectorsLoaded = true; this.render(); } }, this)); } } else { var smtpType = this.model.get(this.smtpField); this._loadOauth2Values(smtpType); this._displayAuthorizationElements(smtpType); this.show(); this._super('_render'); } }, /** * Initializes the authorization information for the OAuth2 tabs * * @param callback * @private */ _loadOauth2TypeInformation: function(callback) { _.each(this.oauth2Types, function(properties, smtpType) { if (!_.isUndefined(properties.auth_url)) { return; } var url = app.api.buildURL('EAPM', 'auth', {}, {application: properties.application}); var callbacks = { success: _.bind(function(data) { if (data) { this.oauth2Types[smtpType].auth_url = data.auth_url || false; this.oauth2Types[smtpType].auth_warning = data.auth_warning || ''; } }, this), error: _.bind(function() { this.oauth2Types[smtpType].auth_url = false; this.oauth2Types[smtpType].auth_warning = app.lang.get('LBL_EMAIL_AUTH_API_ERROR'); }, this), complete: _.bind(function() { callback.call(this); }, this) }; var options = { showAlerts: false, bulk: 'loadOauth2TypeInformation', }; app.api.call('read', url, {}, callbacks, options); }, this); app.api.triggerBulkCall('loadOauth2TypeInformation'); }, /** * Loads any existing OAuth2 authorization values for a tab * @param {string} smtpType the unique indicator of the tab */ _loadOauth2Values: function(smtpType) { if (!this.oauth2Types[smtpType]) { return; } this.model.set({ mail_authtype: 'oauth2', eapm_id: this.oauth2Types[smtpType].eapm_id || null, authorized_account: this.oauth2Types[smtpType].authorized_account || '', mail_smtpuser: this.oauth2Types[smtpType].mail_smtpuser || '' }); }, /** * Determines what authorization elements to show based on the selected tab * * @param {string} smtpType */ _displayAuthorizationElements: function(smtpType) { this.authWarning = ''; this.authButton = false; // If this is an OAuth2 tab, display the correct authorization controls if (this.oauth2Types[smtpType] && this.action === 'edit') { this.$el.closest('.record-cell').show(); if (this.oauth2Types[smtpType].auth_url) { // This tab's connector is enabled, so enable the authorization button this.authButton = 'enabled'; } else if (this.oauth2Types[smtpType].auth_url === false) { // This tab's connector is not enabled, so disable the authorization button this.authWarning = this.oauth2Types[smtpType].auth_warning; this.authButton = 'disabled'; } } else { this.$el.closest('.record-cell').hide(); } }, /** * Extends parent _dispose to remove any leftover window listeners * @private */ _dispose: function() { _.each(this.messageListeners, function(listener) { window.removeEventListener('message', listener); }, this); this.messageListeners = []; this.stopListening(); this._super('_dispose'); } }) }, "email-address": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.OutboundEmail.EmailAddressField * @alias SUGAR.App.view.fields.BaseOutboundEmailEmailAddressField * @extends View.Fields.Base.RelateField */ ({ // Email-address FieldTemplate (base) extendsFrom: 'RelateField', /** * @inheritdoc * * This field should only ever be a single-select. */ initialize: function(options) { options = options || {}; options.def.isMultiSelect = false; this._super('initialize', [options]); // Use the RelateField templates. this.type = 'relate'; // Semi-colon can only appear inside quotation marks in an email // address. Such a case is unlikely, so it is safer than a pipe, // which can appear in an email address without quotes. this._separator = ':'; }, /** * @inheritdoc * * Adds a `createSearchChoice` option. * * @see EmailAddressField#_createSearchChoice */ _getSelect2Options: function() { var options = this._super('_getSelect2Options'); options.createSearchChoice = _.bind(this._createSearchChoice, this); return options; }, /** * Adds a new choice to the dropdown when the search term is a valid email * address that doesn't match any search results. This allows the user to * enter a new email address that doesn't yet exist in the database. * * @param {string} term The partial or full email address the user has * entered. * @return {Object|null} Returns `null` when the email address isn't valid * and should not be added to the dropdown. * @private */ _createSearchChoice: function(term) { var $select2 = this._getSelect2(); var hasContext = !!($select2 && $select2.context); var hasChoice = !!(hasContext && $select2.context.findWhere({email_address: term})); // Note: When `hasContext` is false, something went wrong with // associating the search collection with Select2. This leaves open the // possibility that the entered email address already exists. We allow // the user to select the choice anyway, and an attempt will be made to // create the email address. `EmailAddressesApi` will recognize the // duplicate email address and return the ID of the existing email // address. This will yield the same behavior as if searching had // worked as expected. if (!hasChoice && app.utils.isValidEmailAddress(term)) { // Add this choice to the search context so that the Select2 change // event handler can find the option among the results. if (hasContext) { $select2.context.add({ id: term, email_address: term }); } return { id: term, text: term }; } return null; }, /** * @inheritdoc * * When the selection is a new email address, that email address is created * on the server and the result is asynchronously applied to the model such * that the Select2 instance obtains the new ID for the email address. */ _onFormatSelection: function(obj) { var email; var success; var error; var complete; if (obj.id === obj.text) { /** * Update the ID field with the ID of the newly created model. * * @param {Data.Bean} model The created EmailAddresses model. */ success = _.bind(function(model) { this.setValue({ id: model.get('id'), value: model.get('email_address') }); }, this); /** * Clear the selection on an error when creating the model. */ error = _.bind(function() { this.setValue({ id: '', value: '' }); }, this); /** * Remove the choice from the search context so that the Select2 * change event doesn't ever find the option among its results. The * temporary option is replaced by the option that was created on * the server. * * Enables the action buttons once the request is done. */ complete = _.bind(function() { var $select2 = this._getSelect2(); if ($select2 && $select2.context) { $select2.context.remove(obj.id); } if (_.isFunction(this.view.toggleButtons)) { this.view.toggleButtons(true); } }, this); // Disable the action buttons while creating the new email address. if (_.isFunction(this.view.toggleButtons)) { this.view.toggleButtons(false); } email = app.data.createBean(this.getSearchModule(), {email_address: obj.text}); email.save(null, { success: success, error: error, complete: complete }); } return this._super('_onFormatSelection', [obj]); }, /** * Convenience method for getting this field's Select2 instance. * * @return {Select2|undefined} * @private */ _getSelect2: function() { var $el = this.$(this.fieldTag); return $el.data('select2'); } }) }, "email-provider": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.OutboundEmail.EmailProviderField * @alias SUGAR.App.view.fields.BaseOutboundEmailEmailProviderField * @extends View.Fields.Base.RadioenumField */ ({ // Email-provider FieldTemplate (base) extendsFrom: 'RadioenumField', /** * Falls back to the detail template when attempting to load the disabled * template. * * @inheritdoc */ _getFallbackTemplate: function(viewName) { // Don't just return "detail". In the event that "nodata" or another // template should be the fallback for "detail", then we want to allow // the parent method to determine that as it always has. if (viewName === 'disabled') { viewName = 'detail'; } return this._super('_getFallbackTemplate', [viewName]); } }) }, "name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.OutboundEmail.NameField * @alias SUGAR.App.view.fields.BaseOutboundEmailNameField * @extends View.Fields.Base.NameField */ ({ // Name FieldTemplate (base) extendsFrom: 'BaseNameField', /** * Adds help text (LBL_SYSTEM_ACCOUNT) for the system account. Be aware * that this will replace any help text that is defined in metadata. * * @inheritdoc */ _render: function() { if (this.model.get('type') === 'system') { this.def.help = 'LBL_SYSTEM_ACCOUNT'; } return this._super('_render'); } }) }, "auth-status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.OutboundEmail.AuthStatusField * @alias SUGAR.App.view.fields.OutboundEmailAuthStatusField * @extends View.Fields.Base.BaseField */ ({ // Auth-status FieldTemplate (base) /** * Stores email service connection status */ isConnected: undefined, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.oauth2Types = { google_oauth2: { application: 'GoogleEmail', dataSource: 'googleEmailRedirect' }, exchange_online: { application: 'MicrosoftEmail', dataSource: 'microsoftEmailRedirect' } }; }, /** * @inheritdoc * */ bindDataChange: function() { if (this.model) { this.model.on('change:eapm_id', function(model, value) { this._testConnection(); this.render(); }, this); } }, /** * Uses the detail template. * * @inheritdoc */ _loadTemplate: function() { this._super('_loadTemplate'); this.template = app.template.getField('auth-status', 'detail', this.model.module); }, /** * @inheritdoc */ _render: function() { this.status = app.lang.get(this._getStatus()); this._super('_render'); }, /** * Gets testConnection's result */ _testConnection: function() { let smtpType = this.model.get('mail_smtptype'); let eapmId = this.model.get('eapm_id'); if (!this.oauth2Types[smtpType] || !eapmId) { return; } let url = app.api.buildURL('EAPM', 'test', {}, { application: this.oauth2Types[smtpType].application, eapm_id: eapmId }); let callback = { success: (data) => { if (!data.isConnected) { app.alert.show('info-checking-mail-connection', { level: 'info', title: app.lang.get('LBL_ALERT_TITLE_NOTICE'), messages: [app.lang.get('LBL_EMAIL_RE_AUTHORIZE_ACCOUNT')] }); } this.isConnected = data.isConnected; }, error: (error) => { this.isConnected = false; app.alert.show('error-checking-mail-connection', { level: 'error', messages: error }); }, complete: () => { this.render(); } }; app.api.call('read', url, {}, callback); }, /** * Gets current status */ _getStatus: function() { if (this.model.get('eapm_id') && this.isConnected) { return 'LBL_EMAIL_AUTHORIZED'; } if (this.model.get('eapm_id') && _.isUndefined(this.isConnected)) { return 'LBL_EMAIL_CONNECTING'; } return 'LBL_EMAIL_NOT_AUTHORIZED'; } }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OutboundEmail.RecordView * @alias SUGAR.App.view.views.BaseOutboundEmailRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * Checks if authorized for google oauth2. * * @inheritdoc */ saveClicked: function() { if (this.model.get('mail_authtype') === 'oauth2' && !this.model.get('eapm_id')) { app.alert.show('oe-edit', { level: 'error', title: '', messages: [app.lang.get('LBL_EMAIL_PLEASE_AUTHORIZE', this.module)] }); } else { this._super('saveClicked'); } } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.OutboundEmail.CreateView * @alias SUGAR.App.view.views.OutboundEmailCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Checks if authorized for google oauth2. * * @inheritdoc */ save: function() { if (this.model.get('mail_authtype') === 'oauth2' && !this.model.get('eapm_id')) { app.alert.show('oe-edit', { level: 'error', title: '', messages: [app.lang.get('LBL_EMAIL_PLEASE_AUTHORIZE', this.module)] }); } else { this._super('save'); } } }) }, "selection-list-for-bpm": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Selection list to get Outbound Email accounts for SugarBPM * * @class View.Views.Base.OutboundEmail.SelectionListForBpmView * @alias SUGAR.App.view.views.BaseOutboundEmailSelectionListForBpmView * @extends View.Views.Base.SelectionListView */ ({ // Selection-list-for-bpm View (base) extendsFrom: 'SelectionListView', dataView: 'selection-list-for-bpm', /** * @inheritdoc */ initialize: function(options) { // we need to set the endpoint to hit the pmse api code options.context.loadData = _.wrap(options.context.loadData, function(origFn, opts) { opts = opts || {}; opts.endpoint = function(method, model, urlOpts, callbacks) { var url = app.api.buildURL('pmse_Project/CrmData/outboundEmailsAccounts', null, null, urlOpts.params); return app.api.call('read', url, {}, callbacks, urlOpts.apiOptions); }; origFn.call(options.context, opts); }); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.OutboundEmailModel * @alias SUGAR.App.model.datas.BaseOutboundEmailModel * @extends Data.Bean */ ({ // Model Data (base) /** * @inheritdoc * * Defaults `name` to the current user's full name and `email_address` and * `email_address_id` to the requisite values representing the current * user's primary email address. */ initialize: function(attributes) { var defaults = {}; var email = app.user.get('email'); var privateTeamId = app.user.get('private_team_id'); var privateTeam = _.findWhere(app.user.get('my_teams'), {id: privateTeamId}); var privateTeamName = privateTeam ? privateTeam.name : ''; defaults.name = app.user.get('full_name'); defaults.email_address = app.utils.getPrimaryEmailAddress(app.user); defaults.email_address_id = _.chain(email) .findWhere({email_address: defaults.email_address}) .pick('email_address_id') .values() .first() .value(); defaults.team_name = [{id: privateTeamId, name: privateTeamName, primary: true}]; this._defaults = _.extend({}, this._defaults, defaults); app.Bean.prototype.initialize.call(this, attributes); } }) } }} }, "EmailParticipants":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.EmailParticipantsModel * @alias SUGAR.App.model.datas.BaseEmailParticipantsModel * @extends Data.Bean */ ({ // Model Data (base) /** * @inheritdoc * * Patches the model with the parent's name if the name is empty, but it * can be computed from other attributes. */ set: function(key, val, options) { var result = app.Bean.prototype.set.call(this, key, val, options); var parent = this.getParent(); var name; // If the name field is empty but other fields are present, then it may // be possible to construct the name. We try to do that before assuming // the name can't be found. if (parent && !parent.get('name') && !app.utils.isNameErased(parent)) { name = app.utils.getRecordName(parent); // Replicate the name to all of the fields on the model that // contain the name, so we don't have to construct it again. if (name) { this.get('parent').name = name; this.set('parent_name', name); } } return result; }, /** * Returns a string representing the email participant in the format that * would be used for an address in an email address header. Note that the * name is not surrounded by quotes unless the `surroundNameWithQuotes` * parameter is `true`. * * @example * // With name and email address. * Will Westin <will@example.com> * @example * // Without name. * will@example.com * @example * // Surround name with quotes. * "Will Westin" <will@example.com> * @example * // Name has been erased via a data privacy request. * Value erased <will@example.com> * @example * // Email address has been erased via a data privacy request. * Will Westin <Value erased> * @param {Object} [options] * @param {boolean} [options.quote_name=false] * @return {string} */ toHeaderString: function(options) { var name = this.get('parent_name') || ''; var email = this.get('email_address') || ''; options = options || {}; // The name was erased, so let's use the label. if (_.isEmpty(name) && this.isNameErased()) { name = app.lang.get('LBL_VALUE_ERASED', this.module); } // The email was erased, so let's use the label. if (_.isEmpty(email) && this.isEmailErased()) { email = app.lang.get('LBL_VALUE_ERASED', this.module); } if (_.isEmpty(name)) { return email; } if (_.isEmpty(email)) { return name; } if (options.quote_name) { name = '"' + name + '"'; } return name + ' <' + email + '>'; }, /** * Determines if there is really a parent record. * * The type and id fields are not unset after a parent record is deleted. * So we test for name because the parent record is truly only there if * type and id are non-empty and the parent record can be resolved and has * not been deleted. * * @return {boolean} */ hasParent: function() { var parent = this.getParent(); return !!(parent && (parent.get('name') || app.utils.isNameErased(parent))); }, /** * Returns a bean from the parent data or undefined if no parent exists. * * @return {undefined|Data.Bean} */ getParent: function() { if (this.get('parent') && this.get('parent').type && this.get('parent').id) { // We omit type because it is actually the module name and should // not be treated as an attribute. return app.data.createBean(this.get('parent').type, _.omit(this.get('parent'), 'type')); } }, /** * Returns true of the parent record's name has been erased. * * @return {boolean} */ isNameErased: function() { var parent; if (this.hasParent()) { parent = this.getParent(); if (parent) { return app.utils.isNameErased(parent); } } return false; }, /** * Returns true if the email address has been erased. * * @return {boolean} */ isEmailErased: function() { var link = this.get('email_addresses'); var erasedFields = link && link._erased_fields ? link._erased_fields : []; return _.isEmpty(erasedFields) ? false : _.contains(erasedFields, 'email_address'); } }) } }} }, "DataPrivacy":{"fieldTemplates": {} , "views": { "base": { "mark-for-erasure": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataPrivacy.MarkForErasureView * @alias SUGAR.App.view.views.BaseDataPrivacyMarkForErasureView * @extends View.Views.Base.PiiView * * `MarkForErasureView` handles selecting fields for erasure from a particular record * that contains PII. * * The fields_to_erase field is a JSON field that looks something like this: * * ``` * { * leads: { * lead_id1: [ * 'first_name', * 'last_name', * 'phone' * ], * lead_id2: [ * 'first_name', * 'phone' * ], * ], * contacts: { * contact_id1: [ * 'first_name', * 'last_name', * 'phone' * ], * 'contact_id2': [ * 'first_name', * 'phone' * ] * } * } */ ({ // Mark-for-erasure View (base) extendsFrom: 'PiiView', className: 'flex-list-view left-actions', /** * @inheritdoc * * Initialize and override the Pii collection. */ initialize: function(options) { this._super('initialize', [options]); var modelForErase = this.context.get('modelForErase'); this.baseModule = modelForErase.module; this.baseRecord = modelForErase.id; this.context.set('piiModule', this.baseModule); this.collection.baseModule = this.baseModule; this.collection.baseRecordId = this.baseRecord; this.massCollection = app.data.createBeanCollection('MarkForErasureView'); this.context.set('mass_collection', this.massCollection); this._bindMassCollectionEvents(); this.context.on('markforerasure:mark', this._markForErasure, this); this._setColumnActions(); }, /** * Save just the fields_to_erase for the relevant DataPrivacy record. * * @param {Object} attributes Attributes from the record to save. * @param {Object} attributes.fields_to_erase Fields from related models to * erase. */ saveRecord: function(attributes) { // sync just the fields_to_erase (and the ID to make sure we save the right record) // to ensure we don't save in-progress changes from the DataPrivacy record view var parentModel = this.context.parent.get('model'); var cloneModel = parentModel.clone(); cloneModel.set('fields_to_erase', attributes.fields_to_erase); cloneModel.sync = function(method, model, options) { var callbacks = app.data.getSyncCallbacks(method, model, options); options = app.data.parseOptionsForSync(method, model, options); app.data.trigger('data:sync:start', method, model, options); model.trigger('data:sync:start', method, options); // Only actually save the fields_to_erase (and the ID) app.api.records(method, model.module, attributes, options.params, callbacks); }; cloneModel.save(attributes, { showAlerts: false, // FIXME PX-30: enable custom alert here success: function() { parentModel.set('fields_to_erase', attributes.fields_to_erase); app.drawer.close(); }, error: _.bind(function(model, error) { if (error.status === 412 && !error.request.metadataRetry) { var self = this; // On a metadata sync error, retry the save after the app is synced self.resavingAfterMetadataSync = true; app.once('app:sync:complete', function() { error.request.metadataRetry = true; model.once('sync', function() { self.resavingAfterMetadataSync = false; app.router.refresh(); }); //add a new success callback to refresh the page after the save completes error.request.execute(null, app.api.getMetadataHash()); }); } else { // FIXME: Not handling 409's at this time app.alert.show('error_while_save', { level: 'error', title: app.lang.get('ERR_INTERNAL_ERR_MSG'), messages: ['ERR_HTTP_500_TEXT_LINE1', 'ERR_HTTP_500_TEXT_LINE2'] }); } }, this) }); }, /** * Initialize the collection. * * @private */ _initCollection: function() { // FIXME TY-2169: move this code into the PII view and stop overriding here. var self = this; var PiiCollection = app.BeanCollection.extend({ baseModule: this.baseModule, baseRecordId: this.baseRecord, sync: function(method, model, options) { options.params = _.extend(options.params || {}, {erased_fields: true}); var url = app.api.buildURL(this.baseModule, 'pii', {id: this.baseRecordId}, options.params); var callbacks = app.data.getSyncCallbacks(method, model, options); var defaultSuccessCallback = app.data.getSyncSuccessCallback(method, model, options); callbacks.success = function(data, request) { data.records = _.map(data.fields, function(field) { // Each field having a unique ID is required for using the MassCollection field.id = _.uniqueId(); return field; }); data.records = self.mergePiiFields(data.records); self.applyDataToRecords(data); return defaultSuccessCallback(data, request); }; app.api.call(method, url, options.attributes, callbacks); } }); this.collection = new PiiCollection(); this.collection.on('sync', this._initMassCollection, this); this.context.set('collection', this.collection); }, /** * Get the list of names of fields to erase corresponding to the module and * id of a linked record. * * @return {string[]} List of field names to erase. * @private */ _getFieldsToErase: function() { if (this.context && this.context.parent) { var parentModel = this.context.parent.get('model'); var modelForErase = this.context.get('modelForErase'); if (!parentModel || !modelForErase) { return []; } var fieldsToErase = parentModel.get('fields_to_erase'); var link = modelForErase.link; if (!fieldsToErase || !link || !fieldsToErase[link.name]) { return []; } var modelId = modelForErase.get('id'); var eraseFieldList = fieldsToErase[link.name]; return eraseFieldList[modelId] || []; } return []; }, /** * Retrieve models from the collection given a list of field names. * * @param {string[]} fieldNames List of fields to pick from the collection. * @return {Data.Bean[]} List of field beans. * @private */ _getModelsByName: function(fieldNames) { if (this.collection && this.collection.models) { var models = []; _.each(fieldNames, function(field) { _.each(this.collection.models, function(model) { var fieldName = model.get('field_name'); if (field === fieldName || (!_.isUndefined(field.id) && field.id === model.get('value').id)) { models.push(model); } }); }, this); return models; } return []; }, /** * @inheritdoc * * Patch pii models fields with fielddefs from related module * in order to render properly. */ _renderData: function() { var fields = app.metadata.getModule(this.baseModule).fields; _.each(this.collection.models, function(model) { model.fields = app.utils.deepCopy(this.metaFields); var value = _.findWhere(model.fields, {name: 'value'}); _.extend(value, fields[model.get('field_name')], {name: 'value'}); if (_.contains(['multienum', 'enum'], value.type) && value.function) { value.type = 'base'; } model.fields = app.metadata._patchFields( this.module, app.metadata.getModule(this.baseModule), model.fields ); }, this); this._super('_renderData'); }, /** * Add previously marked fields for erasure to the mass collection. * * @private */ _initMassCollection: function() { var fieldsToErase = this._getFieldsToErase(); var preselectedModels = this._getModelsByName(fieldsToErase); this._addModel(preselectedModels); this.context.trigger('markforerasure:masscollection:init', preselectedModels); }, /** * Mark the selected fields from the drawer to erase. * * @private */ _markForErasure: function() { var selectedModels = this.massCollection.models; var selectedFields; try { selectedFields = _.map(selectedModels, function(model) { var fieldName = model.get('field_name'); // Email addresses and other related fields will have an object containing // the name and id of the record var value = model.get('value'); if (_.isObject(value) && !_.isArray(value)) { if (!fieldName || !value.id) { throw new Error('Unable to mark field ' + fieldName + ' to erase.'); } return { field_name: fieldName, id: value.id }; } return fieldName; }); } catch (e) { app.alert.show('invalid_pii_field', { level: 'error', messages: [e.message], }); return; } var modelForErase = this.context.get('modelForErase'); var link = modelForErase.link; if (!link) { throw new Error('Cannot erase fields on an unlinked record'); } var linkName = link.name; var modelId = modelForErase.get('id'); var parentModel = this.context.parent.get('model'); var fieldsToErase = app.utils.deepCopy(parentModel.get('fields_to_erase')); if (_.isEmpty(fieldsToErase)) { fieldsToErase = {}; } fieldsToErase[linkName] = fieldsToErase[linkName] || {}; if (_.isEmpty(selectedFields)) { fieldsToErase = this._cleanupFieldsToErase(fieldsToErase, linkName, modelId); } else { fieldsToErase[linkName][modelId] = selectedFields; } var attributesToSave = { id: parentModel.id, fields_to_erase: fieldsToErase }; this.saveRecord(attributesToSave); }, /** * Clean up the fields_to_erase so we don't leave empty keys floating in it * * @param {Object} fieldsToErase The fields_to_erase data structure. * @param {string} linkName Name of the linked subpanel to which this view corresponds. * @param {string} modelId ID of the model from which we were erasing fields. * @return {Object} The cleaned fields_to_erase. * @private */ _cleanupFieldsToErase: function(fieldsToErase, linkName, modelId) { // if the list of fields is now empty, wipe out this linked record delete fieldsToErase[linkName][modelId]; // if there are now no fields_to_erase from *any* record from this link type, // wipe out this link if (_.isEmpty(fieldsToErase[linkName])) { delete fieldsToErase[linkName]; // NOTE: do NOT null out this one more level // doing so means sending fields_to_erase as NULL, which is ignored } return fieldsToErase; }, /** * Binds mass collection event listeners. * * @private */ _bindMassCollectionEvents: function() { this.context.on('mass_collection:add', this._addModel, this); this.context.on('mass_collection:add:all', this._addAllModels, this); this.context.on('mass_collection:remove', this._removeModel, this); this.context.on('mass_collection:remove:all mass_collection:clear', this._clearMassCollection, this); }, /** * Adds a model or a list of models to the mass collection. * * @param {Data.Bean|Data.Bean[]} models The model or the list of models * to add. * @private */ _addModel: function(models) { models = _.isArray(models) ? models : [models]; this.massCollection.add(models); if (this._isAllChecked()) { this.massCollection.trigger('all:checked'); } }, /** * Adds all models of the view collection to the mass collection. * * @private */ _addAllModels: function() { this.massCollection.reset(this.collection.models); this.massCollection.trigger('all:checked'); }, /** * Removes a model or a list of models from the mass collection. * * @param {Data.Bean|Data.Bean[]} models The model or the list of models * to remove. * @private */ _removeModel: function(models) { models = _.isArray(models) ? models : [models]; this.massCollection.remove(models); this.massCollection.trigger('not:all:checked'); }, /** * Clears the mass collection. * * @private */ _clearMassCollection: function() { this.massCollection.entire = false; this.massCollection.reset(); this.massCollection.trigger('not:all:checked'); }, /** * Checks if all models of the view collection are in the mass * collection. * * @return {boolean} allChecked `true` if all models of the view * collection are in the mass collection. * @private */ _isAllChecked: function() { if (this.massCollection.length < this.collection.length) { return false; } return _.every(this.collection.models, function(model) { return this.massCollection.get(model.id); }, this); }, /** * Set the checkbox column metadata. * * @private */ _setColumnActions: function() { this.leftColumns = [{ type: 'fieldset', fields: [ { type: 'actionmenu', buttons: [], disable_select_all_alert: true } ], value: false, sortable: false }]; } }) }, "mark-for-erasure-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.MarkForErasureHeaderpaneView * @alias SUGAR.App.view.views.BaseMarkForErasureHeaderpaneView * @extends View.Views.Base.HeaderpaneView */ ({ // Mark-for-erasure-headerpane View (base) extendsFrom: 'HeaderpaneView', events: { 'click a[name=close_button]': 'close', 'click a[name=mark_for_erasure_button]': 'markForErasure' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); app.shortcuts.register({ id: 'MarkForErasureHeaderPanel:Close', keys: ['esc','mod+alt+l'], component: this, description: 'LBL_SHORTCUT_CLOSE_DRAWER', callOnFocus: true, handler: function() { var $closeButton = this.$('a[name=close_button]'); if ($closeButton.is(':visible') && !$closeButton.hasClass('disabled')) { $closeButton.click(); } } }); }, /** * @inheritdoc * * Also bind mass collection events. */ bindDataChange: function() { this._super('bindDataChange'); this.context.once('change:mass_collection', this._addMassCollectionListener, this); this.context.on('markforerasure:masscollection:init', function(models) { this._initialModels = _.clone(models); }, this); }, /** * Closes the drawer. */ close: function() { app.drawer.close(); }, /** * Mark the currently selected fields for erasure. */ markForErasure: function() { this.context.trigger('markforerasure:mark'); }, /** * Set up `add`, `remove` and `reset` listeners on the `mass_collection` so * we can enable/disable the merge button whenever the collection changes. * * @private */ _addMassCollectionListener: function() { var massCollection = this.context.get('mass_collection'); massCollection.on('add remove reset', this._toggleMarkForErasureButton, this); }, /** * Check if we should disable the Mark for Erasure button. * We disable if the list of fields selected differs in any way from the * currently saved list. * * @return {boolean} `true` if we should disable the button; `false` if * we should not. * @private */ _shouldDisable: function() { var massCollection = this.context.get('mass_collection'); if (!this._initialModels) { // No fields to erase were selected; disable by default return true; } else if (this._initialModels.length !== massCollection.length) { return false; } // Mass collection and initial mass collection have same number of models, no choice but // to compare them one-by-one return _.every(this._initialModels, function(model) { return massCollection.get(model.id); }); }, /** * Enable or disable the mark for erasure button as appropriate. */ _toggleMarkForErasureButton: function() { this.$('[name="mark_for_erasure_button"]').toggleClass('disabled', this._shouldDisable()); }, /* * @override * * Overriding to show record name on title header if it is available; * if not, use the standard title. */ _formatTitle: function(title) { var recordName; var model = this.context.get('modelForErase'); // Special case for `Person` type modules if (model.fields && model.fields.name && model.fields.name.type == 'fullname') { recordName = app.utils.formatNameModel(model.module, model.attributes); } else { recordName = app.utils.getRecordName(model); } if (recordName) { return app.lang.get('TPL_DATAPRIVACY_PII_TITLE', model.module, {name: recordName}); } else if (title) { return app.lang.get(title, this.module); } else { return ''; } }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataPrivacy.RecordView * @alias SUGAR.App.view.views.BaseDataPrivacyRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc * */ initialize: function(options) { this._super('initialize', [options]); this.context.on('button:erase_complete_button:click', this.showConfirmEraseAlert, this); this.context.on('button:reject_button:click', this.showRejectEraseAlert, this); this.context.on('button:complete_button:click', this.showConfirmCompleteAlert, this); }, /** * Save status. * * @private */ _setStatus: function(status) { this.model.set('status', status); this.handleSave(); }, /** * Calculates and returns the number of fields on all related records marked for erasure. * * @return {number} The number of records marked for erasure. * @private */ _getNumberOfFieldsToErased: function() { var fieldsNumber = 0; var fieldsToErase = this.model.get('fields_to_erase'); _.each(fieldsToErase, function(module) { fieldsNumber += _.reduce(module, function(memo, fields) { return memo + fields.length; }, 0); }); return fieldsNumber; }, /** * Displays a confirmation warning for erasing all field values for the fields marked for erasure. */ showConfirmEraseAlert: function() { var self = this; var alertText = app.lang.get('LBL_WARNING_ERASE_CONFIRM', 'DataPrivacy'); app.alert.show('confirm_complete:' + this.model.get('id'), { level: 'confirmation', messages: app.utils.formatString(alertText, [this._getNumberOfFieldsToErased()]), onConfirm: function() { self._setStatus('Closed'); } }); }, /** * Displays a confirmation warning for closing the request. */ showConfirmCompleteAlert: function() { var self = this; app.alert.show('confirm_erase_and_complete:' + this.model.get('id'), { level: 'confirmation', messages: app.lang.get('LBL_WARNING_COMPLETE_CONFIRM', 'DataPrivacy'), onConfirm: function() { self._setStatus('Closed'); } }); }, /** * Displays a confirmation warning for rejecting the erasure of field values for all fields marked for erasure. */ showRejectEraseAlert: function() { var self = this; var alertText; if (this.model.get('type') == 'Request to Erase Information') { alertText = app.utils.formatString( app.lang.get('LBL_WARNING_REJECT_ERASURE_CONFIRM', 'DataPrivacy'), [this._getNumberOfFieldsToErased()] ); } else { alertText = app.lang.get('LBL_WARNING_REJECT_REQUEST_CONFIRM', 'DataPrivacy'); } app.alert.show('confirm_reject_erase:' + this.model.get('id'), { level: 'confirmation', messages: alertText, onConfirm: function() { self._setStatus('Rejected'); } }); }, /** * @inheritdoc * */ bindDataChange: function() { this._super('bindDataChange'); this.model.on('change', function() { if (!this.inlineEditMode && this.action !== 'edit') { this.setButtonStates(this.STATE.VIEW); } }, this); }, /** * @inheritdoc * */ setButtonStates: function(state) { this._super('setButtonStates', [state]); this.setButtons(state); }, /** * @inheritdoc * * Depending on the type index, we either show or hide * complete_button, erase_complete_button & reject_button */ setButtons: function(state) { var open = (this.model.get('status') === 'Open'); var erase = (this.model.get('type') === 'Request to Erase Information'); if (state === this.STATE.VIEW && app.acl.hasAccess('admin', this.module)) { this.currentState = state; _.each(this.buttons, function(field) { if (this.shouldHide(open, erase, field)) { field.hide(); } }, this); this.toggleButtons(true); } }, /** * @inheritdoc * * Check whether the button should be hidden */ shouldHide: function(open, erase, field) { var DPActions = [ 'complete_button', 'erase_complete_button', 'reject_button' ]; if ((!open && DPActions.indexOf(field.name) !== -1) || (erase && field.name === 'complete_button') || (!erase && field.name === 'erase_complete_button')) { return true; } return false; }, }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataPrivacy.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseDataPrivacyActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) /** * @inheritdoc */ formatDate: function(date) { return date.formatUser(true); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "marked-for-erasure-dashlet": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DataPrivacy.MarkedForErasureDashlet * @alias SUGAR.App.view.views.BaseDataPrivacyMarkedForErasureDashlet * @extends View.Views.Base.Dashlet * */ (function() { /** * * Given a starting layout will recusively find a child layout with a matching name. * The use of this function is an anti-pattern and SHOULD NOT BE COPIED OR REPLICATED! * * Normally events should be used across the common shared objects (model, collection, context). * Finding a specific layout somewhere on the page to set a value or call a function on creates tight coupling * between the components. Rather than this view doing anything with the subpanel views/layouts, * the subpanel layouts and views should be listening to the proper update/sync events on the collections/contexts * that they are attached to and re-render or update themselves appropriately. * * @param {Layout} layout * @param {string} name * @return {Component|undefined} */ var findLayout = function(layout, name) { if (!layout.getComponent) { return; } var comp = layout.getComponent(name); if (!comp) { _.find(layout._components, function(subComp) { var found = findLayout(subComp, name); if (found) { comp = found; } }); } return comp; }; return { plugins: ['Dashlet'], events: { 'click .more': 'loadMore' }, initialize: function(options) { this._super('initialize', arguments); this.relContexts = {}; this.render = _.bind(_.debounce(this.render, 100), this); }, initDashlet: function() { this.listenTo(this.model, 'change:fields_to_erase', this.formatData); this.listenTo(this.model, 'sync', this.formatData); }, /** * Should be called when a related data set changes. Will run through the related collections and * fields_to_erase from the parent record and format the data in preparation for rendering. * Will also trigger a render. * * This can be optomized later to only update/render the components that changed. * This is a simplistic initial implementation. */ formatData: function() { this.notApplicable = this.model.get('type') !== 'Request to Erase Information'; if (this.notApplicable) { this.values = false; } else { var values = this.model.get('fields_to_erase') || {}; this.values = _.map(values, function(ids, link) { var module = app.data.getRelatedModule(this.model.module, link); var recordCount = _.size(ids); var erased = { link: link, module: module, count: recordCount, models: {}, label: app.lang.getModuleName(module, {plural: true}) }; var ctx = this.listenToRelatedContext(link); if (recordCount > 0 && ctx.get('collapsed')) { // This one is pretty optional. It forces subpanels // open so we can get the names of the models marked for erasure. ctx.set('collapsed', false); } var col = ctx.get('collection'); if (col) { col.each(function(model) { if (model.id && ids[model.id]) { erased.models[model.id] = { model: model, count: _.size(ids[model.id]), nameFieldDef: _.extend(model.fields.name, {link: true}) }; } }); if (!_.isEmpty(_.without(ids, _.keys(erased.models))) && col.next_offset > -1 && col.dataFetched ) { erased.hasMore = true; } } return erased; }, this); } this.render(); }, /** * Given a link name, will find and attach the appropriate listeners to that related context * @param {string} link * @return {Context} */ listenToRelatedContext: function(link) { var context = this.context.parent || this.context; if (!this.relContexts[link] && context.get('module') == 'DataPrivacy') { this.relContexts[link] = context.getChildContext({ // Marked-for-erasure-dashlet View (base) link: link}); if (this.relContexts[link].get('collection')) { this.listenTo(this.relContexts[link].get('collection'), 'sync', this.formatData); } } return this.relContexts[link]; }, /** * Paginate the clicked collection * * None of the below logic should be neccesary other than calling collection paginate. * The subpanel view is triggered instead to allow the required subpanel success callbacks to trigger. * In the future, these should be refactored to listen for collection update events. * @param e * @return {*} */ loadMore: function(e) { e.preventDefault(); var link = $(e.target).data('link'); if (link && this.relContexts[link]) { var subpanel = this.getSubpanelForLink(link); if (subpanel) { var footer = _.find(subpanel._components, function(view) { return view.showMoreRecords; }); if (footer) { return footer.showMoreRecords(); } var listView = _.find(subpanel._components, function(view) { return view.getNextPagination; }); if (listView) { return listView.getNextPagination(); } } } //We couldn't find an appropriate subpanel, paginate ourselves this.relContexts[link].get('collection').paginate({add: true}); }, /** * Because subpanels do not listen for or handle the collection's update event properly * we must trigger the pagination from the subpanel. * @param link * @return {Mixed} */ getSubpanelForLink: function(link) { var topLayout = this.closestComponent('sidebar'); if (topLayout) { var main = topLayout.getComponent('main-pane'); if (main) { var subPanelLayout = findLayout(main, 'subpanels'); if (subPanelLayout) { return _.find(subPanelLayout._components, function(comp) { return comp.context.get('link') === link; }); } } } }, loadData: function() { this.formatData(); } }; })() } }} , "layouts": { "base": { "mark-for-erasure": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.MarkForErasureLayout * @alias SUGAR.App.view.layouts.MarkForErasureLayout * @extends View.Layouts.Base.DefaultLayout */ ({ // Mark-for-erasure Layout (base) extendsFrom: 'DefaultLayout', plugins: ['ShortcutSession'], shortcuts: ['MarkForErasureHeaderPanel:Close'] }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DataPrivacy.RecordLayout * @alias SUGAR.App.view.layouts.BaseDataPrivacyRecordLayout * @extends View.Layouts.Base.RecordLayout */ ({ // Record Layout (base) extendsFrom: 'RecordLayout', /** * @inheritdoc * * Adds handler for invoking Mark for Erasure view */ initialize: function(options) { this._super('initialize', arguments); this.listenTo(this.context, 'mark-erasure:click', this.showMarkForEraseDrawer); }, /** * Open a drawer to mark fields on the given model for erasure. * * @param {Data.Bean} modelForErase Model to mark fields on. */ showMarkForEraseDrawer: function(modelForErase) { var context = this.context.getChildContext({ name: 'Pii', model: app.data.createBean('Pii'), modelForErase: modelForErase, fetch: false }); app.drawer.open({ layout: 'mark-for-erasure', context: context }); } }) }, "subpanels": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DataPrivacy.SubpanelsLayout * @alias SUGAR.App.view.layouts.DataPrivacySubpanelsLayout * @extends View.Layout.Base.SubpanelsLayout */ ({ // Subpanels Layout (base) /** * @inheritdoc * inject the Mark for Erase action link to all subpanels */ initComponents: function(component, def) { this._super('initComponents', arguments); // Add the erase action to all subpanel rowactions _.each(this._components, function(comp) { if (!comp.getComponent) { return; } var viewName = 'subpanel-list'; if (comp.meta && comp.meta.components) { _.find(comp.meta.components, function(def) { var name = ''; var prefix = 'subpanel-for'; if (def.view) { name = _.isObject(def.view) ? def.view.name || def.view.type : def.view; } if (name === 'subpanel-list' || _.isString(name) && name.substr(0, prefix.length) === prefix) { viewName = name; return true; } return false; }); } var subView = comp.getComponent(viewName); if (subView && subView.meta && subView.meta.rowactions && subView.meta.rowactions.actions) { subView.meta.rowactions.actions.push({ 'type': 'dataprivacyerase', 'icon': 'sicon-preview', 'name': 'dataprivacy-erase', 'label': 'LBL_DATAPRIVACY_MARKFORERASE' }); } }); } }) } }} , "datas": {} }, "ReportSchedules":{"fieldTemplates": {} , "views": { "base": { "list-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ReportSchedules.ListHeaderpaneView * @alias SUGAR.App.view.views.BaseReportSchedulesListHeaderpaneView * @extends View.Views.Base.ListHeaderpaneView */ ({ // List-headerpane View (base) extendsFrom: 'ListHeaderpaneView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.context.on('button:create_button:click', this.create, this); }, /** * Pass current report to 'create' view if report filter is applied */ create: function() { var newModel = app.data.createBean('ReportSchedules'); var currentFilter = this.context.get('currentFilterId'); var filterOptions = this.context.get('filterOptions'); // report filter is initially appied and has not been removed if (filterOptions && filterOptions.initial_filter === 'by_report' && currentFilter === 'by_report') { newModel.set({ report_id: filterOptions.filter_populate.report_id[0], report_name: filterOptions.initial_filter_label }); } app.drawer.open({ layout: 'create', context: { create: true, module: 'ReportSchedules', model: newModel } }, function(context, model) { if (model && model.module === app.controller.context.get('module')) { app.controller.context.reloadData(); } }); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.ScheduleReports.CreateView * @alias SUGAR.App.view.views.BaseScheduleReportsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * Check for possible duplicates before creating a new record * @param callback */ initiateSave: function(callback) { this.disableButtons(); async.waterfall([ _.bind(this.validateSubpanelModelsWaterfall, this), _.bind(this.validateModelWaterfall, this), _.bind(this.dupeCheckWaterfall, this), _.bind(this.createRecordWaterfall, this), _.bind(this.linkUserWaterfall, this) ], _.bind(function(error) { this.enableButtons(); if (error && error.status == 412 && !error.request.metadataRetry) { this.handleMetadataSyncError(error); } else if (!error && !this.disposed) { this.context.lastSaveAction = null; callback(); } }, this)); }, /** * Waterfall function * @param callback */ linkUserWaterfall: function(callback) { if (this.context.get('copiedFromModelId')) { this.copyExistingUsers(); } else { this.linkCurrentUser(); } callback(false); }, /** * Link to current user */ linkCurrentUser: function() { var user = app.data.createRelatedBean(this.model, app.user.get('id'), 'users'); user.save(null, {relate: true}); }, /** * Copy existing users */ copyExistingUsers: function() { var bulkRequest; var bulkUrl; var bulkCalls = []; var bean = this.context.parent.get('model'); var collection = bean.getRelatedCollection('users'); _.each(collection.models, function(model) { bulkUrl = app.api.buildURL('ReportSchedules/' + this.model.get('id') + '/link/users/' + model.get('id')); bulkRequest = { url: bulkUrl.substr(4), method: 'POST', data: {} }; bulkCalls.push(bulkRequest); }, this); if (bulkCalls.length) { app.api.call('create', app.api.buildURL(null, 'bulk'), {requests: bulkCalls}, {}); } } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "CommentLog":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Holidays":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "ChangeTimers":{"fieldTemplates": { "base": { "field_name": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.ChangeTimers.Field_nameField * @alias SUGAR.App.view.fields.BaseChangeTimersField_nameField * @extends View.Fields.Base.BaseField */ ({ // Field_name FieldTemplate (base) /** * @inheritdoc * * We want to show the field name's translated value instead of the vardef field name */ format: function(value) { var module = this.context.get('parentModule'); var defs = app.metadata.getModule(module, 'fields'); // No field def found if (!defs[value]) { return value; } var fieldDef = defs[value]; var label = fieldDef.vname || fieldDef.label; return app.lang.get(label, module); } }) } }} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Metrics":{"fieldTemplates": { "base": { "multi-field-label": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.MultiFieldColumnLinkField * @alias SUGAR.App.view.fields.BaseMetricsMultiFieldColumnLinkField * @extends View.Fields.Base.BaseField */ ({ // Multi-field-label FieldTemplate (base) extendsFrom: 'MetricsFieldListField', events: { 'click .multi-field-label': 'multiFieldColumnLinkClicked' }, /** * Create a new empty block and append it to the field list * @param e */ multiFieldColumnLinkClicked: function(e) { var multiRow = app.lang.get('LBL_METRIC_MULTI_ROW', this.module); var multiRowHint = app.lang.get('LBL_METRIC_MULTI_ROW_HINT', this.module); var newMultiField = '<li class="pill outer multi-field-block">' + '<ul class="multi-field-sortable multi-field connectedSortable">' + '<li class="list-header" rel="tooltip" data-original-title="' + multiRow + '">' + multiRow + '<i class="sicon sicon-remove multi-field-column-remove"></i></li><div class="multi-field-hint">' + multiRowHint + '</div></ul></li>'; var columnBox = $(e.currentTarget).closest('div.column').find('ul.field-list:first'); columnBox.append(newMultiField); var newUl = columnBox.find('.multi-field-sortable.multi-field.connectedSortable:last'); this.initMultiFieldDragAndDrop(newUl); } }) }, "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.EnumField * @alias SUGAR.App.view.fields.BaseMetricsEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', orderByFieldNames: ['order_by_primary', 'order_by_secondary'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // If this is an enum for fields to order console multi-line lists by, // populate those field options if (this.orderByFieldNames.indexOf(options.def.name) > -1) { this.populateOrderByValues(); if (this.model) { this.model.on('change:tabContent', function() { this.populateOrderByValues(); }, this); } } }, /** * @inheritdoc */ loadEnumOptions: function(fetch, callback, error) { if (this.orderByFieldNames.indexOf(this.def.name) > -1) { this.populateOrderByValues(); callback.call(this); } else { this._super('loadEnumOptions', [fetch, callback, error]); } }, /** * Populates an "Order By" enum with the proper order by field options */ populateOrderByValues: function() { // Allow a blank field option this.items = { '': '' }; // Get the fields to populate the order-by list with var tabContent = this.model.get('tabContent'); if (!_.isEmpty(tabContent)) { this.items = _.extend(this.items, tabContent.sortFields); } }, }) }, "directions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.DirectionsField * @alias SUGAR.App.view.fields.BaseMetricsDirectionsField * @extends View.Fields.Base.BaseField */ ({ // Directions FieldTemplate (base) events: { 'click .restore-defaults-btn': 'restoreClicked' }, /** * Stores the default attributes for the model */ defaults: {}, /** * Stores a mapping of {value} => {label} used for sort direction fields */ sortDirectionLabels: { desc: 'LBL_METRIC_DIRECTIONS_DESCENDING', asc: 'LBL_METRIC_DIRECTIONS_ASCENDING' }, /** * These store the template strings representing the default field values */ primarySortName: '', primarySortDirection: '', secondarySortName: '', secondarySortDirection: '', filterString: '', /** * Link to detailed instructions */ detailedInstructionsLink: '', /** * @inheritdoc * * @param options */ initialize: function(options) { this._super('initialize', [options]); this._initDefaults(); }, /** * Initializes the template strings that represent the tab model's default * values for the fields on the console configuration view * * @private */ _initDefaults: function() { this.defaults = this.model.get('defaults') || {}; // Build detailedInstructionsLink var serverInfo = app.metadata.getServerInfo(); this.detailedInstructionsLink = 'https://www.sugarcrm.com/crm/product_doc.php?edition=' + serverInfo.flavor + '&version=' + serverInfo.version + '&lang=' + app.lang.getLanguage() + '&module=Metrics'; let products = app.user.getProductCodes(); this.detailedInstructionsLink += products ? '&products=' + encodeURIComponent(products.join(',')) : ''; // Get the tabContent attribute, which includes a mapping of // {sort field value} => {sort field label} var tabContent = this.model.get('tabContent'); var sortFields = tabContent.sortFields || {}; // Initialize the primary sort default template strings this.primarySortName = sortFields[this.defaults.order_by_primary] || ''; var sortDirection = this.defaults.order_by_primary_direction || 'asc'; this.primarySortDirection = app.lang.get(this.sortDirectionLabels[sortDirection], this.module); // Initialize the secondary sort default template strings this.secondarySortName = sortFields[this.defaults.order_by_secondary]; sortDirection = this.defaults.order_by_secondary_direction || 'asc'; this.secondarySortDirection = app.lang.get(this.sortDirectionLabels[sortDirection], this.module); }, /** * Builds a readable string representing a filter definition * * @param {Array} filterDef the filter definition to convert into a string * @private */ _buildFilterString: function(filterDef) { filterDef = filterDef || []; // Make use of the existing filter-field code to help with getting // field and operator labels var tempField = app.view.createField({ def: { name: 'temp_field', type: 'filter-field' }, view: this.view, nested: true, viewName: 'edit', model: this.model }); // Add the rows/rules of the filter definition to the filter string one // at a time this.filterString = ''; _.each(filterDef, function(filter) { _.each(filter, function(conditions, field) { // If this is not the first filter rule, add an "and" to separate them if (!_.isEmpty(this.filterString)) { this.filterString += app.lang.get('LBL_METRIC_DIRECTIONS_FILTER_AND', this.module); } this.filterString += this._buildFilterRowString(field, conditions, tempField); }, this); }, this); tempField.dispose(); }, /** * Helper function to build a string representing a single row/rule of a * filter definition * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} conditions the conditions of the filter rule, typically a map * of operator => value(s) * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the filter row * @private */ _buildFilterRowString: function(field, conditions, filterField) { var rowString = ''; rowString += this._getFilterFieldString(field, filterField); // If this is not a predefined filter, also add the operator if (filterField.fieldList[field] && !filterField.fieldList[field].predefined_filter) { rowString += this._getFilterOperatorAndValueString(field, conditions, filterField); } return rowString; }, /** * Helper function to get the field of a filter row as a readable string * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the field label * @private */ _getFilterFieldString: function(field, filterField) { if (filterField.filterFields && filterField.filterFields[field]) { return filterField.filterFields[field] + ' '; } return ''; }, /** * Helper function to get the operator(s) and value(s) of a filter row as a * readable string * * @param {string} field the field name of the filter rule (ex: 'name' or '$owner') * @param {Object} conditions the conditions of the filter rule, typically a map * of operator => value(s) * @param {Object} filterField an instance of filter-field useful for getting * label information for filter rules * @return {string} a string representing the operator and value(s) labels * @private */ _getFilterOperatorAndValueString: function(field, conditions, filterField) { var operatorAndValueString = ''; _.each(conditions, function(value, operator) { // If there are multiple conditions on the field, separate them with commas if (!_.isEmpty(operatorAndValueString)) { operatorAndValueString += ', '; } // Add the operator label based on the field type var fieldData = app.metadata.getField({ module: this.model.get('metric_module'), name: field }); var hasOperatorLabel = filterField.filterOperatorMap && filterField.filterOperatorMap[fieldData.type]; if (hasOperatorLabel) { var operatorMap = filterField.filterOperatorMap[fieldData.type]; if (operatorMap[operator]) { operatorAndValueString += app.lang.get(operatorMap[operator], 'Filters') + ': '; } } // If the operator requires a value, add the value to the string if (!_.contains(filterField._operatorsWithNoValues, operator)) { operatorAndValueString += this._getFilterValueString(value) + ' '; } }, this); return operatorAndValueString; }, /** * Helper function to get the value(s) of a filter row value as a readable * string * * @param value the value(s) of the filter row * @return {Array|string} the string representing the filter row's value(s) * @private */ _getFilterValueString: function(value) { if (_.isArray(value)) { var valueString = '('; for (var i = 0; i < value.length; i++) { if (i > 0) { if (i === value.length - 1) { valueString += ' ' + app.lang.get('LBL_METRIC_DIRECTIONS_FILTER_OR', this.module); } else { valueString += ', '; } } valueString += value[i]; } valueString += ')'; return valueString; } return value; }, /** * Sets the default values for fields on the model when the reset button is * clicked. Triggers an event to signal to the filter field to re-render properly */ restoreClicked: function() { this.model.set(this.defaults); this.model.trigger('consoleconfig:reset:default'); let data = app.metadata.getView(this.model.get('metric_module'), 'multi-line-list'); this.context.set('defaultViewMeta', data); this.context.trigger('consoleconfig:reset:defaultmetaready'); } }) }, "visible-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.VisibleFieldListField * @alias SUGAR.App.view.fields.BaseVisibleFieldListField * @extends View.Fields.Base.BaseField */ ({ // Visible-field-list FieldTemplate (base) removeFldIcon: '<i class="sicon sicon-remove console-field-remove"></i>', events: { 'click .sicon.sicon-remove.console-field-remove': 'removePill', }, /** * Fields mapped to their subfields. */ visibleFields: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.previewEvent = 'consoleconfig:preview:' + this.model.get('enabled_module'); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset. */ bindDataChange: function() { if (this.model) { this.context.on('consoleconfig:reset:defaultmetarelay', function() { var defaultViewMeta = this.context.get('defaultViewMeta'); var moduleName = this.model.get('enabled_module'); if (defaultViewMeta && defaultViewMeta[moduleName]) { this.context.set('defaultViewMeta', null); this.render(); } }, this); } }, /** * Removes a pill from the selected fields list. * * @param {e} event Remove icon click event. */ removePill: function(event) { var pill = event.target.parentElement; event.target.remove(); pill.setAttribute('class', 'pill outer'); this.getAvailableSortable().append(pill); }, /** * @inheritdoc */ _render: function() { let url = app.api.buildURL('Metrics', 'visible', null, { metric_context: this.context.get('metric_context') || 'service_console', metric_module: this.context.get('metric_module') || 'Cases' }); app.api.call('GET', url, null, { success: _.bind(function(results) { this.visibleFields = []; if (!_.isEmpty(results)) { _.each(results, function(field) { this.visibleFields.push({ 'name': field.id, 'displayName': field.name }); }, this); } this.renderAfterFetch(); }, this), }); }, /** * Render the field and initializes the drag and drop on the pills once the visible metrics are fetched */ renderAfterFetch: function() { this._super('_render'); this.initDragAndDrop(); }, /** * Initialize drag & drop for the selected field (main) list. */ initDragAndDrop: function() { this.$('#columns-sortable').sortable({ items: '.outer.pill', connectWith: '.connectedSortable', receive: _.bind(this.handleDrop, this), }); }, /** * Event handler for the drag & drop. The event is fired when an item is dropped to a list. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleDrop: function(event, ui) { if ('fields-sortable' === ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } }, /** * Return the proper view metadata. If there is a default metadata we restore it, * otherwise we return the view metadata. * * @param {string} moduleName The selected module name from the available modules. * @return {Object} The default view meta or the multi line list metadata. */ getViewMetaData: function(moduleName) { var defaultViewMeta = this.context.get('defaultViewMeta'); return defaultViewMeta && defaultViewMeta[moduleName] ? defaultViewMeta[moduleName] : app.metadata.getView(moduleName, 'multi-line-list'); }, /** * Will cache and return the sortable list with the available fields. * * @return {jQuery} The available fields sortable lost node. */ getAvailableSortable: function() { return this.availableSortable || (this.availableSortable = $('#fields-sortable')); }, }) }, "filter-field": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.FilterFieldField * @alias SUGAR.App.view.fields.BaseMetricsFilterFieldField * @extends View.Fields.Base.BaseField */ ({ // Filter-field FieldTemplate (base) events: { 'click [data-action=add]': 'addRow', 'click [data-action=remove]': 'removeRow', 'change [data-filter=field] input[type=hidden]': 'handleFieldSelected', 'change [data-filter=operator] input[type=hidden]': 'handleOperatorSelected', }, /** * Stores the list of filter field options. Defaults for all filter lists * can be specified here */ fieldList: { $owner: { 'predefined_filter': true, 'label': 'LBL_CURRENT_USER_FILTER' }, $favorite: { 'predefined_filter': true, 'label': 'LBL_FAVORITES_FILTER' } }, filterFields: {}, /** * Stores the mapping of filter operator options */ filterOperators: {}, _operatorsWithNoValues: [], /** * Stores the filter definition */ filterDef: [], /** * Stores the template to render a row of the filter list */ rowTemplate: null, /** * Map of fields types. * * Specifies correspondence between field types and field operator types. */ fieldTypeMap: { 'datetime': 'date', 'datetimecombo': 'date' }, /** * Stores the name of the module this filter refers to */ moduleName: null, /** * @override * @param {Object} opts */ initialize: function(opts) { this._super('initialize', [opts]); this.moduleName = this.model.get('metric_module'); // Store the filter field and operator information for the module this.loadFilterOperators(this.model.get('metric_module')); this.loadFilterFields(this.model.get('metric_module')); this.filterDef = this.model.get('filter_def'); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset */ bindDataChange: function() { if (this.model) { this.model.on('consoleconfig:reset:default', function() { this.render(); }, this); } }, /** * Loads the list of filter fields for supplied module. * * @param {string} module The module to load the filter fields for. */ loadFilterFields: function(module) { // Get the set of filterableFields for the tab, and extend it with // the default fieldList var filterableFields = this.model.get('filterableFields') || {}; this.fieldList = _.extend({}, this.fieldList, filterableFields); // For each field, if it is filterable (or a pre-defined filter), add it // to the filterFields list this.filterFields = {}; _.each(this.fieldList, function(fieldDef, fieldName) { var label = app.lang.get(fieldDef.label || fieldDef.vname, module); var isPredefined = fieldDef.predefined_filter; var isFilterable = !_.isEmpty(label) && this.filterOperatorMap[fieldDef.type]; if (isPredefined || isFilterable) { this.filterFields[fieldName] = label; } }, this); }, /** * Loads the list of filter operators for supplied module. * * @param {string} [module] The module to load the filters for. */ loadFilterOperators: function(module) { this.filterOperatorMap = app.metadata.getFilterOperators(module); this._operatorsWithNoValues = ['$empty', '$not_empty']; }, /** * In edit mode, render filter input fields using the edit-filter-row template. * @inheritdoc * @private */ _render: function() { this._super('_render'); this.action = this.context.get('action'); this.rowTemplate = app.template.getField('filter-field', this.action + '-filter-row', 'Metrics'); this.populateFilter(this.model.get('filter_def')); // If the filter definition is empty, add a fresh row if (this.$('[data-filter=row]').length === 0 && this.action === 'edit') { this.addRow(); } }, /** * Builds the initial elements of the filter for the given filter definition * @param array filterDef the filter definition */ populateFilter: function(filterDef) { filterDef = app.data.getBeanClass('Filters').prototype.populateFilterDefinition(filterDef, true); _.each(filterDef, function(row) { this.populateRow(row); }, this); }, /** * Populates row fields with the row filter definition. * * In case it is a template filter that gets populated by values passed in * the context/metadata, empty values will be replaced by populated * value(s). * * @param {Object} rowObj The filter definition of a row. */ populateRow: function(rowObj) { var moduleMeta = app.metadata.getModule(this.moduleName); var fieldMeta = moduleMeta.fields; _.each(rowObj, function(value, key) { var isPredefinedFilter = (this.fieldList[key] && this.fieldList[key].predefined_filter === true); if (key === '$or') { var keys = _.reduce(value, function(memo, obj) { return memo.concat(_.keys(obj)); }, []); key = _.find(_.keys(this.fieldList), function(key) { if (_.has(this.fieldList[key], 'dbFields')) { return _.isEqual(this.fieldList[key].dbFields.sort(), keys.sort()); } }, this); // Predicates are identical, so we just use the first. value = _.values(value[0])[0]; } else if (key === '$and') { var values = _.reduce(value, function(memo, obj) { return _.extend(memo, obj); }, {}); var def = _.find(this.fieldList, function(fieldDef) { return _.has(values, fieldDef.id_name || fieldDef.name); }, this); var operator = '$equals'; key = def ? def.name : key; // We want to get the operator from our values object only for currency fields if (def && !_.isString(values[def.name]) && def.type === 'currency') { operator = _.keys(values[def.name])[0]; values[key] = values[key][operator]; } value = {}; value[operator] = values; } else if (!fieldMeta[key] && !isPredefinedFilter) { return; } if (!this.fieldList[key]) { //Make sure we use name for relate fields var relate = _.find(this.fieldList, function(field) { return field.id_name === key; }); // field not found so don't create row for it. if (!relate) { return; } key = relate.name; // for relate fields in version < 7.7 we used `$equals` and `$not_equals` operator so for version // compatibility & as per TY-159 needed to fix this since 7.7 & onwards we will be using `$in` & // `$not_in` operators for relate fields if (_.isString(value) || _.isNumber(value)) { value = {$in: [value]}; } else if (_.keys(value)[0] === '$not_equals') { var val = _.values([value])[0]; value = {$not_in: val}; } } if (_.isString(value) || _.isNumber(value)) { value = {$equals: value}; } _.each(value, function(value, operator) { this.initRow(null, {name: key, operator: operator, value: value}); }, this); }, this); }, /** * Add a row * @param {Event} e * @return {jQuery} $row The added row element. */ addRow: function(e) { var $row; if (e) { // Triggered by clicking the plus sign. Add the row to that point. $row = this.$(e.currentTarget).closest('[data-filter=row]'); $row.after(this.rowTemplate()); $row = $row.next(); } return this.initRow($row); }, /** * Remove a row * @param {Event} e */ removeRow: function(e) { var $row = this.$(e.currentTarget).closest('[data-filter=row]'); $row.remove(); if (this.$('[data-filter=row]').length === 0) { this.addRow(); } this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Initializes a row either with the retrieved field values or the * default field values. * * @param {jQuery} [$row] The related filter row. * @param {Object} [data] The values to set in the fields. * @return {jQuery} $row The initialized row element. */ initRow: function($row, data) { $row = $row || $(this.rowTemplate()).appendTo(this.$el); data = data || {}; var model; var field; // Init the row with the data available. $row.attr('data-name', data.name); $row.attr('data-operator', data.operator); $row.attr('data-value', data.value); $row.data('name', data.name); $row.data('operator', data.operator); $row.data('value', data.value); // Create a blank model for the field selector enum, and set the // field value if we know it. model = app.data.createBean(this.model.get('metric_module')); if (data.name) { model.set('filter_row_name', data.name); } // Create the field selector enum and add it to the dom field = this.createField(model, { name: 'filter_row_name', type: 'enum', options: this.filterFields, defaultToBlank: true }); field.render(); $row.find('[data-filter=field]').append(field.$el); // Store the field in the data attributes. $row.data('nameField', field); // If this selector has a value, init the operator field as well if (data.name) { this.initOperatorField($row); } return $row; }, /** * Initializes the operator field. * * @param {jQuery} $row The related filter row. */ initOperatorField: function($row) { var $fieldWrapper = $row.find('[data-filter=operator]'); var data = $row.data(); var fieldName = data.nameField.model.get('filter_row_name'); var previousOperator = data.operator; // Make sure the data attributes contain the right selected field. data.name = fieldName; if (!fieldName) { return; } // For relate fields data.id_name = this.fieldList[fieldName].id_name; // For flex-relate fields data.type_name = this.fieldList[fieldName].type_name; //Predefined filters don't need operators and value field if (this.fieldList[fieldName].predefined_filter === true) { data.isPredefinedFilter = true; return; } // Get operators for this filter type var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var payload = {}; var types = _.keys(this.filterOperatorMap[fieldType]); // For parent field with the operator '$equals', the operator field is // hidden and we need to display the value field directly. So here we // need to assign 'previousOperator' and 'data.operator variables' to let // the value field initialize. //FIXME: We shouldn't have a condition on the parent field. TY-352 will // fix it. if (fieldType === 'parent' && _.isEqual(types, ['$equals'])) { previousOperator = data.operator = types[0]; } fieldType === 'parent' ? $fieldWrapper.addClass('hide').empty() : $fieldWrapper.removeClass('hide').empty(); $row.find('[data-filter=value]').addClass('hide').empty(); _.each(types, function(operand) { payload[operand] = app.lang.get( this.filterOperatorMap[fieldType][operand], [this.moduleName, 'Filters'] ); }, this); // Render the operator field var model = app.data.createBean(this.moduleName); if (previousOperator) { model.set('filter_row_operator', data.operator === '$dateRange' ? data.value : data.operator); } var field = this.createField(model, { name: 'filter_row_operator', type: 'enum', // minimumResultsForSearch set to 9999 to hide the search field, // See: https://github.com/ivaynberg/select2/issues/414 searchBarThreshold: 9999, options: payload, defaultToBlank: true }); field.render(); $fieldWrapper.append(field.$el); data.operatorField = field; var hide = fieldType === 'parent'; this._hideOperator(hide, $row); // We want to go into 'initValueField' only if the field value is known. // We need to check 'previousOperator' instead of 'data.operator' // because even if the default operator has been set, the field would // have set 'data.operator' when it rendered anyway. if (previousOperator) { this.initValueField($row); } }, /** * Initializes the value field. * * @param {jQuery} $row The related filter row. */ initValueField: function($row) { var self = this; var data = $row.data(); var operation = data.operatorField.model.get('filter_row_operator'); // Make sure the data attributes contain the right operator selected. data.operator = operation; if (!operation) { return; } if (_.contains(this._operatorsWithNoValues, operation)) { return; } // Patching fields metadata var moduleName = this.moduleName; var module = app.metadata.getModule(moduleName); var fields = app.metadata._patchFields(moduleName, module, app.utils.deepCopy(this.fieldList)); // More patch for some field types var fieldName = $row.find('[data-filter=field] input[type=hidden]').select2('val'); var fieldType = this.fieldTypeMap[this.fieldList[fieldName].type] || this.fieldList[fieldName].type; var fieldDef = fields[fieldName]; switch (fieldType) { case 'enum': fieldDef.isMultiSelect = this.isCollectiveValue($row); // Set minimumResultsForSearch to a negative value to hide the search field, // See: https://github.com/ivaynberg/select2/issues/489#issuecomment-13535459 fieldDef.searchBarThreshold = -1; break; case 'bool': fieldDef.type = 'enum'; fieldDef.options = fieldDef.options || 'filter_checkbox_dom'; break; case 'int': fieldDef.auto_increment = false; //For $in operator, we need to convert `['1','20','35']` to `1,20,35` to make it work in a varchar field if (operation === '$in') { fieldDef.type = 'varchar'; fieldDef.len = 200; if (_.isArray($row.data('value'))) { $row.attr('data-value', $row.data('value').join(',')); } } break; case 'teamset': fieldDef.type = 'relate'; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'datetimecombo': case 'date': fieldDef.type = 'date'; //Flag to indicate the value needs to be formatted correctly data.isDate = true; if (operation.charAt(0) !== '$') { //Flag to indicate we need to build the date filter definition based on the date operator data.isDateRange = true; return; } break; case 'relate': fieldDef.auto_populate = true; fieldDef.isMultiSelect = this.isCollectiveValue($row); break; case 'parent': data.isFlexRelate = true; break; } fieldDef.required = false; fieldDef.readonly = false; // Create new model with the value set var model = app.data.createBean(moduleName); var $fieldValue = $row.find('[data-filter=value]'); $fieldValue.removeClass('hide').empty(); // Add the field type as an attribute on the HTML element so that it // can be used as a CSS selector. $fieldValue.attr('data-type', fieldType); //fire the change event as soon as the user start typing var _keyUpCallback = function(e) { if ($(e.currentTarget).is('.select2-input')) { return; //Skip select2. Select2 triggers other events. } this.value = $(e.currentTarget).val(); // We use "silent" update because we don't need re-render the field. model.set(this.name, this.unformat($(e.currentTarget).val()), {silent: true}); model.trigger('change'); }; //If the operation is $between we need to set two inputs. if (operation === '$between' || operation === '$dateBetween') { var minmax = []; var value = $row.data('value') || []; if (fieldType === 'currency' && $row.data('value')) { value = $row.data('value') || {}; model.set(value); value = value[fieldName] || []; // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). model.set('id', 'not_new'); } model.set(fieldName + '_min', value[0] || ''); model.set(fieldName + '_max', value[1] || ''); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_min'}))); minmax.push(this.createField(model, _.extend({}, fieldDef, {name: fieldName + '_max'}))); if (operation === '$dateBetween') { minmax[0].label = app.lang.get('LBL_FILTER_DATEBETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_DATEBETWEEN_TO'); } else { minmax[0].label = app.lang.get('LBL_FILTER_BETWEEN_FROM'); minmax[1].label = app.lang.get('LBL_FILTER_BETWEEN_TO'); } data.valueField = minmax; _.each(minmax, function(field) { $fieldValue.append(field.$el); this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); field.$('.input-append').prepend('<span class="add-on">' + field.label + '</span>') .addClass('input-prepend') .removeClass('date'); // .date makes .inherit-width on input have no effect field.$('input, textarea').on('keyup', _.debounce(_.bind(_keyUpCallback, field), 400)); }); field.render(); }, this); } else if (data.isFlexRelate) { var values = {}; _.each($row.data('value'), function(value, key) { values[key] = value; }, this); model.set(values); var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); findRelatedName = app.data.createBeanCollection(model.get('parent_type')); data.valueField = field; $fieldValue.append(field.$el); if (model.get('parent_id')) { findRelatedName.fetch({ params: {filter: [{'id': model.get('parent_id')}]}, complete: _.bind(function() { if (!this.disposed) { if (findRelatedName.first()) { model.set(fieldName, findRelatedName.first().get(field.getRelatedModuleField()), {silent: true}); } if (!field.disposed) { field.render(); } } }, this) }); } else { field.render(); } } else { // value is either an empty object OR an object containing `currency_id` and currency amount if (fieldType === 'currency' && $row.data('value')) { // for stickiness & to retrieve correct saved values, we need to set the model with data.value object model.set($row.data('value')); // FIXME: Change currency.js to retrieve correct unit for currency filters (see TY-156). // Mark this one as not_new so that model isn't treated as new model.set('id', 'not_new'); } else { model.set(fieldDef.id_name || fieldName, $row.data('value')); } // Render the value field var field = this.createField(model, _.extend({}, fieldDef, {name: fieldName})); $fieldValue.append(field.$el); data.valueField = field; this.listenTo(field, 'render', function() { field.$('input, select, textarea').addClass('inherit-width'); // .date makes .inherit-width on input have no effect so we need to remove it. field.$('.input-append').removeClass('date'); field.$('input, textarea').on('keyup',_.debounce(_.bind(_keyUpCallback, field), 400)); }); if ((fieldDef.type === 'relate' || fieldDef.type === 'nestedset') && !_.isEmpty($row.data('value')) ) { var findRelatedName = app.data.createBeanCollection(fieldDef.module); var relateOperator = this.isCollectiveValue($row) ? '$in' : '$equals'; var relateFilter = [{id: {}}]; relateFilter[0].id[relateOperator] = $row.data('value'); findRelatedName.fetch({fields: [fieldDef.rname], params: {filter: relateFilter}, complete: function() { if (!self.disposed) { if (findRelatedName.length > 0) { model.set(fieldDef.id_name, findRelatedName.pluck('id'), {silent: true}); model.set(fieldName, findRelatedName.pluck(fieldDef.rname), {silent: true}); } if (!field.disposed) { field.render(); } } } }); } else { field.render(); } } // When the value changes, update the filter value var updateFilter = function() { self._updateFilterData($row); self.model.set('filter_def', self.buildFilterDef(true), {silent: true}); }; this.listenTo(model, 'change', updateFilter); this.listenTo(model, 'change:' + fieldName, updateFilter); // Manually trigger the filter request if a value has been selected lately // This is the case for checkbox fields or enum fields that don't have empty values. var modelValue = model.get(fieldDef.id_name || fieldName); // To handle case: value is an object with 'currency_id' = 'xyz' and 'likely_case' = '' // For currency fields, when value becomes an object, trigger change if (!_.isEmpty(modelValue) && modelValue !== $row.data('value')) { model.trigger('change'); } }, /** * Check if the selected filter operator is a collective type. * * @param {jQuery} $row The related filter row. */ isCollectiveValue: function($row) { return $row.data('operator') === '$in' || $row.data('operator') === '$not_in'; }, /** * Update filter data for this row * @param $row Row to update * @private */ _updateFilterData: function($row) { var data = $row.data(); var field = data.valueField; var name = data.name; var valueForFilter; //Make sure we use ID for relate fields if (this.fieldList[name] && this.fieldList[name].id_name) { name = this.fieldList[name].id_name; } //If we have multiple fields we have to build an array of values if (_.isArray(field)) { valueForFilter = []; _.each(field, function(field) { var value = !field.disposed && field.model.has(field.name) ? field.model.get(field.name) : ''; value = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; valueForFilter.push(value); }); } else { var value = !field.disposed && field.model.has(name) ? field.model.get(name) : ''; valueForFilter = $row.data('isDate') ? (app.date.stripIsoTimeDelimterAndTZ(value) || '') : value; } // Update filter value once we've calculated final value $row.data('value', valueForFilter); $row.attr('data-value', valueForFilter); }, /** * Shows or hides the operator field of the filter row specified. * * Automatically populates the operator field to have value `$equals` if it * is not in midst of populating the row. * * @param {boolean} hide Set to `true` to hide the operator field. * @param {jQuery} $row The filter row of interest. * @private */ _hideOperator: function(hide, $row) { $row.find('[data-filter=value]') .toggleClass('span4', !hide) .toggleClass('span8', hide); }, /** * Utility function that instantiates a field for this form. * * The field action is manually set to `detail` because we want to render * the `edit` template but the action remains `detail` (filtering). * * @param {Data.Bean} model A bean necessary to the field for storing the * value(s). * @param {Object} def The field definition. * @return {View.Field} The field component. */ createField: function(model, def) { var obj = { def: def, view: this.view, nested: true, viewName: 'edit', model: model }; var field = app.view.createField(obj); return field; }, /** * Fired when a user selects a field to filter by * @param {Event} e */ handleFieldSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {field: 'operatorField', value: 'operator'}, {field: 'valueField', value: 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initOperatorField($row); // Update the attributes of the row $row.attr('data-name', $el.val()); $row.attr('data-operator', ''); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Fired when a user selects an operator to filter by * @param {Event} e */ handleOperatorSelected: function(e) { var $el = this.$(e.currentTarget); var $row = $el.parents('[data-filter=row]'); var fieldOpts = [ {'field': 'valueField', 'value': 'value'} ]; this._disposeRowFields($row, fieldOpts); this.initValueField($row); // Update the attributes of the row $row.attr('data-operator', $el.val()); $row.attr('data-value', ''); this.model.set('filter_def', this.buildFilterDef(true), {silent: true}); }, /** * Disposes fields stored in the data attributes of the row element. * * @example of an `opts` object param: * [ * {field: 'nameField', value: 'name'}, * {field: 'operatorField', value: 'operator'}, * {field: 'valueField', value: 'value'} * ] * * @param {jQuery} $row The row which fields are to be disposed. * @param {Array} opts An array of objects containing the field object and * value to the data attributes of the row. */ _disposeRowFields: function($row, opts) { var data = $row.data(); var model; if (_.isObject(data) && _.isArray(opts)) { _.each(opts, function(val) { if (data[val.field]) { //For in between filter we have an array of fields so we need to cover all cases var fields = _.isArray(data[val.field]) ? data[val.field] : [data[val.field]]; data[val.value] = ''; _.each(fields, function(field) { model = field.model; if (val.field === 'valueField' && model) { model.clear({silent: true}); this.stopListening(model); } field.dispose(); field = null; }, this); return; } if (data.isDateRange && val.value === 'value') { data.value = ''; } }, this); } //Reset flags data.isDate = false; data.isDateRange = false; data.isPredefinedFilter = false; data.isFlexRelate = false; $row.data(data); }, /** * Build filter definition for all rows. * * @param {boolean} onlyValidRows Set `true` to retrieve only filter * definition of valid rows, `false` to retrieve the entire field * template. * @return {Array} Filter definition. */ buildFilterDef: function(onlyValidRows) { var $rows = this.$('[data-filter=row]'); var filter = []; _.each($rows, function(row) { var rowFilter = this.buildRowFilterDef($(row), onlyValidRows); if (rowFilter) { filter.push(rowFilter); } }, this); return filter; }, /** * Build filter definition for this row. * * @param {jQuery} $row The related row. * @param {boolean} onlyIfValid Set `true` to validate the row and return * `undefined` if not valid, or `false` to build the definition anyway. * @return {Object} Filter definition for this row. */ buildRowFilterDef: function($row, onlyIfValid) { var data = $row.data(); if (onlyIfValid && !this.validateRow($row)) { return; } var operator = data.operator; var value = data.value || ''; var name = data.id_name || data.name; var filter = {}; if (_.isEmpty(name)) { return; } if (data.isPredefinedFilter || !this.fieldList) { filter[name] = ''; return filter; } else { if (!_.isEmpty(data.valueField) && _.isFunction(data.valueField.delegateBuildFilterDefinition)) { filter[name] = {}; filter[name][operator] = data.valueField.delegateBuildFilterDefinition(); } else if (this.fieldList[name] && _.has(this.fieldList[name], 'dbFields')) { var subfilters = []; _.each(this.fieldList[name].dbFields, function(dbField) { var filter = {}; filter[dbField] = {}; filter[dbField][operator] = value; subfilters.push(filter); }); filter.$or = subfilters; } else { if (data.isFlexRelate) { var valueField = data.valueField; var idFilter = {}; var typeFilter = {}; idFilter[data.id_name] = valueField.model.get(data.id_name); typeFilter[data.type_name] = valueField.model.get(data.type_name); filter.$and = [idFilter, typeFilter]; // Creating currency filter. For all but `$between` operators we use // type property from data.valueField. For `$between`, data.valueField // is an array and therefore we check for type==='currency' from // either of the elements. } else if (data.valueField && (data.valueField.type === 'currency' || (_.isArray(data.valueField) && data.valueField[0].type === 'currency')) ) { // initially value is an array which we later convert into an object for saving and retrieving // purposes (stickiness structure constraints) var amountValue; if (_.isObject(value) && !_.isUndefined(value[name])) { amountValue = value[name]; } else { amountValue = value; } var amountFilter = {}; amountFilter[name] = {}; amountFilter[name][operator] = amountValue; // for `$between`, we use first element to get dataField ('currency_id') since it is same // for both elements and also because data.valueField is an array var dataField; if (_.isArray(data.valueField)) { dataField = data.valueField[0]; } else { dataField = data.valueField; } var currencyId; currencyId = dataField.getCurrencyField().name; var currencyFilter = {}; currencyFilter[currencyId] = dataField.model.get(currencyId); filter.$and = [amountFilter, currencyFilter]; } else if (data.isDateRange) { //Once here the value is actually a key of date_range_selector_dom and we need to build a real //filter definition on it. filter[name] = {}; filter[name].$dateRange = operator; } else if (operator === '$in' || operator === '$not_in') { // IN/NOT IN require an array filter[name] = {}; //If value is not an array, we split the string by commas to make it an array of values if (_.isArray(value)) { filter[name][operator] = value; } else if (!_.isEmpty(value)) { filter[name][operator] = (value + '').split(','); } else { filter[name][operator] = []; } } else { filter[name] = {}; filter[name][operator] = value; } } return filter; } }, /** * Verify the value of the row is not empty. * * @param {Element} $row The row to validate. * @return {boolean} `true` if valid, `false` otherwise. */ validateRow: function(row) { var $row = $(row); var data = $row.data(); if (_.contains(this._operatorsWithNoValues, data.operator)) { return true; } // for empty value in currency we dont want to validate if (!_.isUndefined(data.valueField) && !_.isArray(data.valueField) && data.valueField.type === 'currency' && (_.isEmpty(data.value) || (_.isObject(data.value) && _.isEmpty(data.valueField.model.get(data.name))))) { return false; } //For date range and predefined filters there is no value if (data.isDateRange || data.isPredefinedFilter) { return true; } else if (data.isFlexRelate) { return data.value ? _.reduce(data.value, function(memo, val) { return memo && !_.isEmpty(val); }, true) : false; } //Special case for between operators where 2 values are needed if (_.contains(['$between', '$dateBetween'], data.operator)) { if (!_.isArray(data.value) || data.value.length !== 2) { return false; } switch (data.operator) { case '$between': // FIXME: the fields should set a true number (see SC-3138). return !(_.isNaN(parseFloat(data.value[0])) || _.isNaN(parseFloat(data.value[1]))); case '$dateBetween': return !_.isEmpty(data.value[0]) && !_.isEmpty(data.value[1]); default: return false; } } return _.isNumber(data.value) || !_.isEmpty(data.value); }, }) }, "rowactions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /* * @class View.Fields.Base.Metrics.RowactionsField * @alias SUGAR.App.view.fields.MetricsRowactionsField * @extends View.Fields.Base.RowactionsField */ ({ // Rowactions FieldTemplate (base) extendsFrom: 'RowactionsField', /** * Checks if the user is an admin * @return {boolean} true if is an admin, false otherwise * @private */ _isAdmin: function() { var acls = app.user.getAcls().Metrics; var isAdmin = !_.has(acls, 'admin'); var isSysAdmin = (app.user.get('type') === 'admin'); return (isSysAdmin || isAdmin); }, /** * @inheritdoc */ _render: function() { if (!this._isAdmin()) { // Don't show if not admin return; } this._super('_render'); } }) }, "sort-order-selector": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.SortOrderSelectorField * @alias SUGAR.App.view.fields.BaseMetricsSortOrderSelectorField * @extends View.Fields.Base.BaseField */ ({ // Sort-order-selector FieldTemplate (base) events: { 'click .sort-order-selector': 'setNewValue' }, /** * Stores the name of the field that this field is conditionally dependent on */ dependencyField: null, /** * @inheritdoc * * Grabs the name of the dependency field from the field options * * @param options */ initialize: function(options) { this._super('initialize', [options]); if (options.def && options.def.dependencyField) { this.dependencyField = options.def.dependencyField; } }, /** * @inheritdoc * * Extends the parent bindDataChange to include a check of the value of * the dependency field */ bindDataChange: function() { this._super('bindDataChange'); if (this.dependencyField) { this.model.on('change:' + this.dependencyField, function() { this._handleDependencyChange(); }, this); this.model.on('change:' + this.name, function() { this._setValue(this.model.get(this.name)); }, this); } }, /** * When this field first renders, check the dependency field to see if we * need to hide this * * @private */ _render: function() { this._super('_render'); this._handleDependencyChange(); }, /** * Checks the value of the dependency field. If it is empty, this field will * be set to 'ascending' and hidden. * * @private */ _handleDependencyChange: function() { if (this.model && this.$el) { if (_.isEmpty(this.model.get(this.dependencyField))) { this._setValue('asc'); this.$el.hide(); } else { this.$el.show(); } } }, /** * Simulates the user clicking on the field to set a value for this field * (both on the model and in the UI) * * @param value the value ('asc' or 'desc') to set the field to * @private */ _setValue: function(value) { this.$el.find('[name="' + value + '"]').click(); }, /** * Sets the value of the selected sort order on the model * * @param event the button click event */ setNewValue: function(event) { if (this.context.get('action') === 'detail') { event.preventDefault(); event.stopPropagation(); return; } this.model.set(this.name, event.currentTarget.name); this.render(); } }) }, "preview-table": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.PreviewTableField * @alias SUGAR.App.view.fields.BaseMetricsPreviewTableField * @extends View.Fields.Base.BaseField */ ({ // Preview-table FieldTemplate (base) /** * The name of the module the fields belong to. * @property {string} */ moduleName: '', /** * A mapping of fields to be rendered on the preview table. * @property {Object} */ fieldList: null, /** * The number of rows to be shown in the preview. * @property {number} */ previewRows: 5, /** * A mapping of class names that describe how the data rows should appear if there are no live data available. * This mapping is based on the fieldList and sent to the template. */ rowDesign: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.bindChangeEvent(options.model); }, /** * Will listen to an event that signals a change in the console configuration. * * @param {Data.Bean} model The model used for the preview. */ bindChangeEvent: function(model) { if (model && model.get('metric_module')) { this.moduleName = model.get('metric_module'); this.eventName = 'consoleconfig:preview:' + this.moduleName; this.context.on(this.eventName, this.renderPreview, this); } }, /** * It will create a mapping of css classes that corresponds to the list of columns to be displayed. * Odd and even rows while having a single sub-field should render alternating long and short placeholders, * while if there is a field with multiple sub-fields, 2 placeholders should be shown (1 long, 1 short). * * @param {Array} list A list of fields that have to appear as columns in the preview. */ setRowDesign: function(list) { var i; var oneRow; var singleClass; var longClass = 'cell-bar--long'; var shortClass = 'cell-bar--short'; this.rowDesign = []; for (i = 1; i <= this.previewRows; i++) { singleClass = i % 2 === 0 ? shortClass : longClass; oneRow = _.reduce(list, function(row, subFields) { row.push(subFields.length > 1 ? [longClass, shortClass] : [singleClass]); return row; }, []); this.rowDesign.push(oneRow); } }, /** * Triggers a render of the prevoew based on a list of field labels. The order of the columns * will be inherited from the the order of strings. * * @param {Array} list A list of fields that have to appear as columns in the preview. */ renderPreview: function(list) { this.fieldList = list; this.setRowDesign(list); this.render(); }, /** * @inheritdoc */ _dispose: function() { this.context.off(this.eventName, this.renderPreview, this); this._super('_dispose'); }, }) }, "available-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.AvailableFieldListField * @alias SUGAR.App.view.fields.BaseMetricsAvailableFieldListField * @extends View.Fields.Base.BaseField */ ({ // Available-field-list FieldTemplate (base) /** * Fields with these names should not be displayed in fields list. */ ignoredNames: ['deleted', 'mkto_id', 'googleplus', 'team_name'], /** * Fields with these types should not be displayed in fields list. */ ignoredTypes: ['id', 'link', 'tag'], /** * Here are stored all available fields for all available tabs. */ availableFieldLists: [], /** * List of fields that are displayed for a given module. */ currentAvailableFields: [], /** * @inheritdoc * * Collects all supported fields for all available modules and sets the module specific fields to be displayed. */ initialize: function(options) { this._super('initialize', [options]); var moduleName = this.model.get('metric_module'); this.setAvailableFields(moduleName); this.currentAvailableFields = this.availableFieldLists; }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset */ bindDataChange: function() { if (this.model) { this.context.on('consoleconfig:reset:defaultmetaready', function() { // the default meta data is ready, use it to re-render var defaultViewMeta = this.context.get('defaultViewMeta'); var moduleName = this.model.get('metric_module'); if (!_.isEmpty(defaultViewMeta)) { this.setAvailableFields(moduleName); this.currentAvailableFields = this.availableFieldLists; this.render(); this.context.trigger('consoleconfig:reset:defaultmetarelay'); } }, this); } }, /** * Return the proper view metadata. * * @param {string} moduleName The selected module name from the available modules. */ getViewMetaData: function(moduleName) { // If defaultViewMeta exists, it means we are restoring the default settings. var defaultViewMeta = this.context.get('defaultViewMeta'); if (!_.isEmpty(defaultViewMeta)) { return defaultViewMeta; } // Not restoring defaults, use the regular view meta data let viewMeta = this.model.get('viewdefs'); if (!_.isEmpty(viewMeta)) { return viewMeta.base.view['multi-line-list']; } else { return app.metadata.getView(moduleName, 'multi-line-list'); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.handleDragAndDrop(); }, /** * Sets the available fields for the requested module. * * @param {string} moduleName The selected module name from the available modules. */ setAvailableFields: function(moduleName) { var allFields = app.metadata.getModule(moduleName, 'fields'); var multiLineList = this.getViewMetaData(moduleName); var multiLineFields = this.getSelectedFields(_.first(multiLineList.panels).fields); this.availableFieldLists = []; _.each(allFields, function(field) { if (this.isFieldSupported(field, multiLineFields)) { this.availableFieldLists.push({ 'name': field.name, 'label': (field.label || field.vname), 'displayName': app.lang.get(field.label || field.vname, moduleName) }); } }, this); // Sort available fields alphabetically this.availableFieldLists = _.sortBy(this.availableFieldLists, 'displayName'); }, /** * Parse metadata and return array of fields that are already defined in the metadata. * * @param {Array} multiLineFields List of fields that appear on the multi-line list view. * @return {Array} True if the field is already in, false otherwise. */ getSelectedFields: function(multiLineFields) { var fields = []; _.each(multiLineFields, function(column) { _.each(column.subfields, function(subfield) { // if widget_name exists, it's a special field, use widget_name instead of name fields.push({'name': subfield.widget_name || subfield.name}); }, this); }, this); return fields; }, /** * Restricts specific fields to be shown in available fields list. * * @param {Object} field Field to be verified. * @param {Array} multiLineFields List of fields that appear on the multi-line list view. * @return {boolean} True if field is supported, false otherwise. */ isFieldSupported: function(field, multiLineFields) { // Specified fields names should be ignored. if (!field.name || _.contains(this.ignoredNames, field.name)) { return false; } // Specified field types should be ignored. if (_.contains(this.ignoredTypes, field.type) || field.dbType === 'id') { return false; } // Multi-line list view fields should not be displayed. if (_.findWhere(multiLineFields, {'name': field.name})) { return false; } return !this.hasNoStudioSupport(field); }, /** * Verify if fields do not have available studio support. * Studio fields have multiple value types (array, bool, string, undefined). * * @param {Object} field Field selected to get verified. * @return {boolean} True if there is no support, false otherwise. */ hasNoStudioSupport: function(field) { // if it's a special field, do not check studio attribute if (!_.isUndefined(field.type) && field.type === 'widget') { return false; } var studio = field.studio; if (!_.isUndefined(studio)) { if (studio === 'false' || studio === false) { return true; } if (!_.isUndefined(studio.listview)) { if (studio.listview === 'false' || studio.listview === false) { return true; } } } return false; }, /** * Handles the dragging of the items from available fields list to the columns list section * But not the way around */ handleDragAndDrop: function() { this.$('#fields-sortable').sortable({ connectWith: '.connectedSortable', update: _.bind(function(event, ui) { var multiRow = app.lang.get('LBL_METRIC_MULTI_ROW', this.module); var multiRowHint = app.lang.get('LBL_METRIC_MULTI_ROW_HINT', this.module); var hint = '<div class="multi-field-hint">' + multiRowHint + '</div>'; if ($(ui.sender).hasClass('multi-field') && ui.sender.children().length > 0) { var header = ''; var headerLabel = ''; var i = 0; _.each(ui.sender.children(), function(field) { if (i > 1) { header += '/'; headerLabel += '/'; } if (i++ > 0 && !_.isUndefined(field) && !_.isUndefined(field.textContent)) { if (field.textContent.trim() === multiRowHint) { // clean hint text, it will be added later $(field).remove(); } else { header += field.textContent.trim(); headerLabel += field.getAttribute('fieldlabel'); } } }, this); if (header.endsWith('/')) { header = header.slice(0, -1); headerLabel = headerLabel.slice(0, -1); } header = header ? header : multiRow; $(ui.sender.children()[0]).text(header) .append(this.removeColIcon); $(ui.sender.children()[0]).attr('data-original-title', header); $(ui.sender.children()[0]).attr('fieldname', header); $(ui.sender.children()[0]).attr('fieldlabel', headerLabel); } }, this), receive: _.bind(function(event, ui) { ui.sender.sortable('cancel'); }, this) }); } }) }, "hidden-field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.HiddenFieldListField * @alias SUGAR.App.view.fields.BaseMetricsHiddenFieldListField * @extends View.Fields.Base.BaseField */ ({ // Hidden-field-list FieldTemplate (base) /** * List of fields that are displayed for a given column. */ hiddenFields: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); }, /** * @inheritdoc */ _render: function() { let url = app.api.buildURL('Metrics', 'hidden', null, { metric_context: this.context.get('metric_context') || 'service_console', metric_module: this.context.get('metric_module') || 'Cases' }); app.api.call('GET', url, null, { success: _.bind(function(results) { this.hiddenFields = []; if (!_.isEmpty(results)) { _.each(results, function(field) { this.hiddenFields.push({ 'name': field.id, 'displayName': field.name }); }, this); } this._super('_render'); this.handleDragAndDrop(); }, this), }); }, /** * Handles the dragging of the items from available fields list to the columns list section * But not the way around */ handleDragAndDrop: function() { this.$('#fields-sortable').sortable({ connectWith: '.connectedSortable', receive: _.bind(function(event, ui) { ui.sender.sortable('cancel'); }, this) }); } }) }, "field-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.FieldListField * @alias SUGAR.App.view.fields.BaseMetricsFieldListField * @extends View.Fields.Base.BaseField */ ({ // Field-list FieldTemplate (base) removeFldIcon: '<i class="sicon sicon-remove console-field-remove"></i>', removeColIcon: '<i class="sicon sicon-remove multi-field-column-remove"></i>', events: { 'click .sicon.sicon-remove.console-field-remove': 'removePill', 'click .sicon.sicon-remove.multi-field-column-remove': 'removeMultiLineField', }, /** * Fields mapped to their subfields. */ mappedFields: {}, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.mappedFields = this.getMappedFields(); this.previewEvent = 'consoleconfig:preview:' + this.model.get('metric_module'); }, /** * @inheritdoc * * Overrides the parent bindDataChange to make sure this field is re-rendered * when the config is reset. */ bindDataChange: function() { if (this.model) { this.context.on('consoleconfig:reset:defaultmetarelay', function() { var defaultViewMeta = this.context.get('defaultViewMeta'); var moduleName = this.model.get('metric_module'); if (defaultViewMeta) { this.mappedFields = this.getMappedFields(); this.context.set('defaultViewMeta', null); this.render(); this.handleColumnsChanging(); } }, this); } }, /** * Removes a pill from the selected fields list. * * @param {e} event Remove icon click event. */ removePill: function(event) { var pill = event.target.parentElement; var container = $(pill.parentElement); event.target.remove(); pill.setAttribute('class', 'pill outer'); this.getAvailableSortable().append(pill); if (container.hasClass('multi-field-sortable')) { this.updateMultiLineField(container); this.addMultiFieldHint(container); } this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Remove a multi line field column and fields inside. * * @param {e} event Remove icon click event. */ removeMultiLineField: function(event) { var multiLineField = event.target.parentElement.parentElement.parentElement; _.each($(multiLineField).find('.pill'), function(pill) { pill.children[0].remove(); pill.setAttribute('class', 'pill outer'); this.getAvailableSortable().append(pill); }, this); multiLineField.remove(); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.initSingleFieldDragAndDrop(); if (this.options.def.type == 'field-list') { this.triggerPreviewUpdate(); } }, /** * Initialize drag & drop for the selected field (main) list. */ initSingleFieldDragAndDrop: function() { var sortableEl = this.$('#columns-sortable'); sortableEl.sortable({ items: '.outer.pill', connectWith: '.connectedSortable', receive: _.bind(this.handleSingleFieldDrop, this), update: _.bind(this.handleSingleFieldStop, this), }); var multiFieldSortables = sortableEl.find('.multi-field-sortable.multi-field.connectedSortable'); _.each(multiFieldSortables, function(multiField) { this.initMultiFieldDragAndDrop($(multiField)); }, this); }, /** * Initialize drag & drop for a multi field container. * * @param {Object} element The multi-field container element. */ initMultiFieldDragAndDrop: function(element) { element.sortable({ items: '.pill', connectWith: '.connectedSortable', receive: _.bind(this.handleMultiLineFieldDrop, this), update: _.bind(this.handleMultiLineFieldStop, this), over: _.bind(this.handleMultiLineFieldOver, this), out: _.bind(this.handleMultiLineFieldOut, this), }); }, /** * Event handler for the single field drag & drop. The event is fired when an item is dropped to a list. * Several actions are performed: * - When moving a field from the right to the left we add the remove icon. * - When moving a field from a multi line field to the outside we change selector. * - The library can't handle the case when the last item from the list is a multi line field. * In such cases we manually insert the moved item after the group; * dropping into a multi-line field is handled in `handleMultiLineFieldDrop`. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleSingleFieldDrop: function(event, ui) { if ('fields-sortable' == ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } if (ui.sender.hasClass('multi-field-sortable')) { ui.item.addClass('outer'); this.addMultiFieldHint(ui.sender); } this.repositionItem(ui); }, /** * Event handler for the single field drag & drop. * The event is fired when drop has been finished and the DOM has been updated. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleSingleFieldStop: function(e, ui) { this.repositionItem(ui); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Event handler for the multi field drag & drop. The event is fired when an item is dropped to a list. * Several actions are performed here: * - If certain conditions are met the drag & drop is cancelled. * - If there is a hint text, it is removed. * - When moving a field from the right to the left we add the remove icon. * - When a field is being moved from the right to the left or from the ouside inside the selector is changed. * * @param {e} event jQuery sortable event handler. * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ handleMultiLineFieldDrop: function(event, ui) { var multiLineFields = $(event.target).find('.pill'); if (this.shouldRejectFieldDrop(ui, multiLineFields)) { ui.sender.sortable('cancel'); this.updateMultiLineField(ui.sender); } else { $(event.target).find('.multi-field-hint').remove(); if ('fields-sortable' == ui.sender.attr('id')) { ui.item.append(this.removeFldIcon); } if (ui.sender.hasClass('multi-field-sortable')) { this.addMultiFieldHint(ui.sender); } else { ui.item.removeClass('outer'); } this.triggerPreviewUpdate(); } }, /** * Event handler for the multi field drag & drop. * The event is fired when drop has been finished and the DOM has been updated. * * @param {e} event jQuery sortable event handler. */ handleMultiLineFieldStop: function(event) { this.updateMultiLineField($(event.target)); this.handleColumnsChanging(); this.triggerPreviewUpdate(); }, /** * Event handler for the multi field drag over * The event is fired when drag over with a draggable element has occurred * * @param {e} event jQuery sortable event handler * @param {Object} jQuery ui object selector */ handleMultiLineFieldOver: function(event, ui) { var eventTarget = $(event.target); var multiLineFields = eventTarget.find('.pill'); if (multiLineFields.length > 2 && !ui.item.parent().hasClass('multi-field-sortable')) { ui.item.css('cursor', 'no-drop'); ui.placeholder.addClass('multi-field-block-placeholder-none'); } else { eventTarget.parent().addClass('multi-field-block-highlight'); } }, /** * Event handler for the multi field drag out * The event is fired when drag out with a draggable element has occurred * * @param {e} event jQuery sortable event handler * @param {Object} jQuery ui object selector */ handleMultiLineFieldOut: function(event, ui) { ui.item.css('cursor', ''); ui.placeholder.removeClass('multi-field-block-placeholder-none'); $(event.target).parent().removeClass('multi-field-block-highlight'); }, /** * Update columns property of the model basing on the selected columns. */ handleColumnsChanging: function() { var fieldName; var columns = {}; var moduleName = this.model.get('metric_module'); var columnsSortable = $('#list-layout-side') .find('#columns-sortable .pill:not(.multi-field-block)'); var fields = app.metadata.getModule(moduleName, 'fields'); _.each(columnsSortable, function(item) { fieldName = $(item).attr('fieldname'); columns[fieldName] = fields[fieldName]; }); this.model.set('columns', columns); }, /** * jQuery UI does not support drag & drop into nested containers. When the last item is a multi line field, * we have to check for the correct drop area and if the library targets the multi line field instead of the * main container as a drop zone, we move the dropped item to the outside container. * * @param {Object} ui jQuery UI's helper object for drag & drop operations. */ repositionItem: function(ui) { var parentContainer = ui.item.parent(); if (parentContainer.hasClass('multi-field')) { var parentStartPos = parentContainer.offset().top; var parentEndPos = parentStartPos + parentContainer.height(); if (ui.offset.top <= parentStartPos || ui.offset.top >= parentEndPos) { parentContainer.parent().after(ui.item); } } }, /** * Checks 4 conditions in which drag & drop into a multi line field should not be allowed. * The 4 conditions are the following: * - When there are already 2 fields in the block. * - When a multi line field block is being dropped. * - When a field that is defined as a multi-line field is dropped into a block with at least 1 item already. * - When the block contains already a field defined as a multi-line field (such fields count as 2 simple fields). * * @param {Object} ui The jQuery UI library sortable action object. * @param {Array} multiLineFields The list of fields inside a multi field block. */ shouldRejectFieldDrop: function(ui, multiLineFields) { var moduleName = this.model.get('metric_module'); var droppedFieldName = ui.item.attr('fieldname'); var fieldDefinitions = app.metadata.getModule(moduleName, 'fields'); var isDefinedAsMultiLine = this.isDefinedAsMultiLine(droppedFieldName, fieldDefinitions); // IMPORTANT NOTE: the placeholder is considered another field present in cases // when we perform operation other than a simple reordering of items. var subFieldLimit = 2; // Reject conditions. var hasAlready2Fields = multiLineFields.length > subFieldLimit; var isMultiLineIntoMultiLineDrop = ui.item.hasClass('multi-field-block'); var isMultiFieldDrop = isDefinedAsMultiLine && multiLineFields.length > (subFieldLimit - 1); var containsAlreadyAMultiLineFieldDef = multiLineFields.length == subFieldLimit && this.containsMultiLineFieldDef(multiLineFields, fieldDefinitions); return hasAlready2Fields || isMultiLineIntoMultiLineDrop || isMultiFieldDrop || containsAlreadyAMultiLineFieldDef; }, /** * Check whether a multi line field contains and fields that are defined as multi line fields. * * @param {jQuery} multiLineFields The pills from a multi line field. * @param {Object} fieldDefinitions The list of field definitions for the current module. * @return {boolean} True or false. */ containsMultiLineFieldDef: function(multiLineFields, fieldDefinitions) { return _.isObject(_.find(multiLineFields, function(field) { var fieldName = field.getAttribute('fieldname'); return fieldName && this.isDefinedAsMultiLine(fieldName, fieldDefinitions); }, this)); }, /** * Will add a text hint about possible drag & drop to a multi line field. * * @param {jQuery} multiLineField The multi field into which the hint text should be inserted. */ addMultiFieldHint: function(multiLineField) { var pills = multiLineField.children('.pill'); var hint = multiLineField.children('.multi-field-hint'); if (!hint.length && pills.length == 0) { multiLineField.append( '<div class="multi-field-hint">' + app.lang.get('LBL_METRIC_MULTI_ROW_HINT', 'Metrics') + '</div>' ); } }, /** * It will create a new aggregated header text and label for a multi line field. * In case there are no fields in a multi line field the default values will be set. * * @param {jQuery} fields The pills found inside a multi line field. * @return {Object} A header title text and custom label. */ getNewHeaderDetails: function(fields) { var lbl = ''; var name = ''; var text = ''; var delimiter = ''; _.each(fields, function(field) { lbl += delimiter + field.getAttribute('fieldlabel'); name += delimiter + field.getAttribute('fieldname'); text += delimiter + field.getAttribute('data-original-title'); delimiter = '/'; }); return { fieldName: name || '', label: lbl || '', text: text || app.lang.get('LBL_METRIC_MULTI_ROW', this.module) }; }, /** * It will update a multi line field depending on the number of pills it contains. * If there are no fields inside, a hint will be displayed. If a field has been added, * the hint text will be removed. Additionally the multi line field header text will be changed. * * @param {jQuery} multiLineField A multi line field to be updated. */ updateMultiLineField: function(multiLineField) { var fields = multiLineField.children('.pill'); var headerDetails = this.getNewHeaderDetails(fields); if (fields.length) { multiLineField.children('.multi-field-hint').remove(); } var header = multiLineField.children('.list-header'); header.text(headerDetails.text).append(this.removeColIcon) .attr('data-original-title', headerDetails.text) .attr('fieldname', headerDetails.fieldName) .attr('fieldlabel', headerDetails.label); }, /** * Checks if a given field is defined as a multi-line field. * * @param {string} fieldName The name of the field to check. * @return {boolean} True if it is a multi line field definition. */ isDefinedAsMultiLine: function(fieldName) { var moduleName = this.model.get('metric_module'); var fieldDefinitions = app.metadata.getModule(moduleName, 'fields'); return _.isObject(_.find(fieldDefinitions, function(field) { return field.multiline && field.type === 'widget' && field.name === fieldName; })); }, /** * Return the proper view metadata. If there is a default metadata we restore it, * otherwise we return the view metadata. * * @param {string} moduleName The selected module name from the available modules. * @return {Object} The default view meta or the multi line list metadata. */ getViewMetaData: function(moduleName) { var defaultViewMeta = this.context.get('defaultViewMeta'); if (!_.isEmpty(defaultViewMeta)) { return defaultViewMeta; } var viewMeta = this.model.get('viewdefs'); if (!_.isEmpty(viewMeta)) { return viewMeta.base.view['multi-line-list']; } else { return app.metadata.getView(moduleName, 'multi-line-list'); } }, /** * Will cache and return the sortable list with the available fields. * * @return {jQuery} The available fields sortable lost node. */ getAvailableSortable: function() { var parentSelector = '#list-layout-side'; return this.availableSortable || (this.availableSortable = $(parentSelector).find('#fields-sortable')); }, /** * Gets the module's multi-line list fields from the model with the parent field mapping * * @return {Object} the fields */ getMappedFields: function() { var tabContentFields = {}; var whitelistedProperties = [ 'name', 'label', 'widget_name', ]; var multiLineMeta = this.getViewMetaData(this.model.get('metric_module')); _.each(multiLineMeta.panels, function(panel) { _.each(panel.fields, function(fieldDefs) { var subfields = []; _.each(fieldDefs.subfields, function(subfield) { var parsedSubfield = _.pick(subfield, whitelistedProperties); // if label does not exist, get it from the parent's vardef if (!_.has(parsedSubfield, 'label')) { parsedSubfield.label = this.model.fields[parsedSubfield.name].label || this.model.fields[parsedSubfield.name].vname; } parsedSubfield.parent_name = fieldDefs.name; parsedSubfield.parent_label = fieldDefs.label; if (_.has(parsedSubfield, 'widget_name')) { parsedSubfield.name = parsedSubfield.widget_name; } subfields = subfields.concat(parsedSubfield); }, this); tabContentFields[fieldDefs.name] = _.has(tabContentFields, fieldDefs.name) ? tabContentFields[fieldDefs.name].concat(subfields) : subfields; }, this); }, this); return tabContentFields; }, /** * It will trigger an update on the multi lint list preview. To trigger the preview it needs a * list of selected fields based on the sortable list. In case the preview is triggered from a * multi field, we have have to climb higher to find the sortable list. */ triggerPreviewUpdate: function() { var domFieldList = this.$el.find('#columns-sortable'); if (!domFieldList.length) { domFieldList = this.$el.parent().parent().parent().find('#columns-sortable'); } this.context.trigger(this.previewEvent, this.getSelectedFieldList(domFieldList)); }, /** * Taking the dom list of fields, creates an accurate mapping of fields for the preview. * * @param {jQuery} node The DOM representation of the selected fields. * @return {Array} The list of selected fields. */ getSelectedFieldList: function(node) { var subFields; var fieldList = []; node.children().each(function(index, field) { if ($(field).hasClass('multi-field-block')) { subFields = []; $(field).find('.pill').each(function(index, subField) { subFields.push({ name: $(subField).attr('fieldname'), label: $(subField).attr('fieldlabel') }); }); if (subFields.length) { fieldList.push(subFields); } } else { fieldList.push([{ name: $(field).attr('fieldname'), label: $(field).attr('fieldlabel') }]); } }); return fieldList; }, }) }, "freeze-first-column": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.FreezeFirstColumnField * @alias SUGAR.App.view.fields.BaseMetricsFreezeFirstColumnField * @extends View.Fields.Base.BoolField */ ({ // Freeze-first-column FieldTemplate (base) extendsFrom: 'BoolField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setupField(); }, /** * Set the field value on load to be checked/unchecked based on the saved config */ setupField: function() { let freezeFirstColumn = this.model.get('freeze_first_column'); if (_.isUndefined(freezeFirstColumn)) { this.model.set('freeze_first_column', true); this.value = true; } } }) }, "context-module": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Metrics.ContextModuleField * @alias SUGAR.App.view.fields.BaseMetricsContextModuleField * @extends View.Fields.Base.BaseField */ ({ // Context-module FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); let fieldMeta = app.metadata.getModule('Metrics').fields.metric_context; let contextList = app.lang.getAppListStrings(fieldMeta.options); this.contextName = contextList[this.model.get('metric_context')]; this.moduleName = this.model.get('metric_module'); this.showNoData = false; } }) } }} , "views": { "base": { "record-side-pane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Metrics.RecordSidePaneView * @alias SUGAR.App.view.views.BaseMetricsRecordSidePaneView * @extends View.Views.Base.ConfigPanelView */ ({ // Record-side-pane View (base) extendsFrom: 'BaseConfigPanelView', /** * @inheritdoc */ render: function() { this._super('render'); let paneGroup = $('.record-side-pane-group'); let ariaControls = this.context.get('ariaControls'); paneGroup.toggle(this.context.get('action') === 'edit' && ariaControls === 'list_layout'); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.MetricsRecordlistView * @alias SUGAR.App.view.views.BaseMetricsRecordlistView * @extends View.Views.Base.RecordlistView */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ getDeleteMessages: function(model) { var messages = this._super('getDeleteMessages', [model]); messages.confirmation = messages.confirmation + '<br> ' + app.lang.get('LBL_METRIC_DELETE_WARNING'); return messages; } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.MetricsRecordView * @alias SUGAR.App.view.views.BaseMetricsRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ getDeleteMessages: function(model) { var messages = this._super('getDeleteMessages', [model]); messages.confirmation = messages.confirmation + '<br> ' + app.lang.get('LBL_METRIC_DELETE_WARNING'); return messages; } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Metrics.ConfigHeaderButtonsView * @alias SUGAR.App.view.views.BaseMetricsConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.model.set({ metric_context: this.context.get('metric_context') || 'service_console', metric_module: this.context.get('metric_module') || 'Cases' }); }, _beforeSaveConfig: function() { let columnsSortable = []; let columnLists = this.layout.$el.find('#columns-sortable li'); _.each(columnLists, function(li) { columnsSortable.push(li.getAttribute('fieldname')); }); let fieldsSortable = []; let fieldLists = this.layout.$el.find('#fields-sortable li'); _.each(fieldLists, function(li) { fieldsSortable.push(li.getAttribute('fieldname')); }); this.context.get('model').set({ is_setup: true, visible_list: columnsSortable, hidden_list: fieldsSortable }, {silent: true}); return this._super('_beforeSaveConfig'); }, /** * Calls the context model save and saves the config model in case * the default model save needs to be overwritten * * @protected */ _saveConfig: function() { this._super('_saveConfig'); }, }) }, "config-tab-settings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Metrics.ConfigTabSettingsView * @alias SUGAR.App.view.views.BaseMetricsConfigTabSettingsView * @extends View.Views.Base.ConsoleConfiguration.ConfigPaneView */ ({ // Config-tab-settings View (base) extendsFrom: 'BaseConsoleConfigurationConfigPanelView', events: { 'click .restore-defaults-btn': 'handleRestoreDefaults' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.isAdmin = this.checkAdminAccess(); }, /** * Checks Metrics ACLs to see if the User is a system admin * or if the user has a admin role for the Metrics module */ checkAdminAccess: function() { let acls = app.user.getAcls().Metrics; let isAdmin = !_.has(acls, 'admin'); let isSysAdmin = (app.user.get('type') === 'admin'); return (isSysAdmin || isAdmin); }, /** * Handles the click event on the restore defaults button */ handleRestoreDefaults: function() { app.alert.show('reset_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_RESTORE_DEFAULT_CONFIRM', 'Metrics'), onConfirm: _.bind(function() { this.restoreAdminDefaults(); }, this) }, this); }, /** * Restores the metrics to their initial state, i.e., all the metrics are visible and none are hidden. * This also get any new metrics that the admin might have created */ restoreAdminDefaults: function() { let metricContext = this.model.get('metric_context'); let metricModule = this.model.get('metric_module'); if (!metricContext || !metricModule) { return; } let url = app.api.buildURL('Metrics', 'restore-defaults', null, { metric_context: metricContext, metric_module: metricModule }); app.api.call('read', url, null, { success: _.bind(function(results) { // empty the hidden fields column this.$('#fields-sortable').empty(); if (!_.isEmpty(results)) { let visibleFieldComp = this.getField('visible-fields') || {}; // empty the previously stored metrics visibleFieldComp.visibleFields = []; _.each(results, function(field) { visibleFieldComp.visibleFields.push({ 'name': field.id, 'displayName': field.name }); }, this); visibleFieldComp.renderAfterFetch(); } }, this) }); } }) }, "record-content-tabs": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Metrics.RecordContentTabsView * @alias SUGAR.App.view.views.BaseMetricsRecordContentTabsView * @extends View.Views.Base.ConfigPanelView */ ({ // Record-content-tabs View (base) extendsFrom: 'BaseConfigPanelView', activeTabIndex: 0, events: { 'click .record-panel .btn-toggle': 'togglePanel', }, /** * @inheritdoc */ render: function() { var self = this; this.action = this.context.get('action'); this._super('render'); this.toggleFreezeColumn(); this.$('#tabs').tabs({ active: this.context.get('activeTabIndex') || 0, classes: { 'ui-tabs-active': 'active', }, // when selecting another tab, show/hide the corresponding side [ane div accordingly activate: function(event, ui) { let paneGroup = $('.record-side-pane-group'); let ariaControls = $(event.currentTarget).closest('li').attr('aria-controls'); paneGroup.toggle(self.action === 'edit' && ariaControls === 'list_layout'); $('.sidebar-toggle', this.$el).toggle(ariaControls !== 'settings'); } }); }, /** * Show/hide the Freeze first column config for the user based on the admin settings */ toggleFreezeColumn: function() { if (!app.config.allowFreezeFirstColumn) { let freezeElem = this.$('.freeze-config') || {}; let freezeCell = freezeElem.length > 0 && freezeElem.closest('.row-fluid') ? freezeElem.closest('.row-fluid') : {}; if (freezeCell.length > 0) { let freezeCellIndex = freezeCell.index(); let configParentElem = freezeCell.parent() || {}; // get the header label element for freeze option let fieldHeader = configParentElem.length > 0 && configParentElem.children() ? configParentElem.children().eq(freezeCellIndex - 1) : {}; fieldHeader.hide(); freezeCell.hide(); } } }, /** * Hide or show panel based on click to the panel toggle button * @param {Event} evt */ togglePanel: function(evt) { this.$(evt.currentTarget) .closest('.record-panel') .toggleClass('folded'); }, }) }, "record-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Metrics.RecordHeaderButtonsView * @alias SUGAR.App.view.views.BaseMetricsRecordHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Record-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * The labels to be created when saving console configuration */ labelList: [], /** * The column definitions to be saved when saving console configuration */ selectedFieldList: {}, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._viewAlerts = []; this.events = _.extend(this.events, {'click a[name="edit_button"]:not(.disabled)': 'editConfig'}); }, /** * Displays alert message for invalid models */ showInvalidModel: function() { if (!this instanceof app.view.View) { app.logger.error('This method should be invoked by Function.prototype.call(), passing in as ' + 'argument an instance of this view.'); return; } var name = 'invalid-data'; this._viewAlerts.push(name); app.alert.show(name, { level: 'error', messages: 'ERR_RESOLVE_ERRORS' }); }, /** * @inheritdoc */ cancelConfig: function() { if (this.triggerBefore('cancel')) { if (this.context.get('create')) { if (app.drawer.count()) { app.drawer.close(this.context, this.model); } } else { // cancel edit this.context.trigger('edit:cancelled'); } } }, /** * Handles 'edit' button click */ editConfig: function() { let activeTabIndex = 0; let ariaControls; if (this.layout && this.layout.$('#tabs')) { activeTabIndex = this.layout.$('#tabs').tabs('option', 'active'); ariaControls = this.layout.$('#tabs').find('li.tab.active').attr('aria-controls'); } this.context.set({ 'activeTabIndex': activeTabIndex, 'ariaControls': ariaControls }); this.context.trigger('edit:clicked'); }, /** * Prepares the context beans "order_by_primary" & "order_by_secondary" for save action */ _setOrderByFields: function() { }, /** * Prepares the context bean for save action */ _beforeSaveConfig: function() { // to build the definitions of selected fields and labels this.buildSelectedList(); this.model.set({ labels: this.labelList, viewdefs: this.selectedFieldList }, {silent: true}); return this._super('_beforeSaveConfig'); }, /** * This build a view meta object for a module * * @param module * @return An object of view metadata */ buildViewMetaObject: function(module) { return { base: { view: { 'multi-line-list': { panels: [ { label: 'LBL_LABEL_1', fields: [] } ], // use the original collectionOptions and filterDef collectionOptions: app.metadata.getView(module, 'multi-line-list').collectionOptions || {}, filterDef: app.metadata.getView(module, 'multi-line-list').filterDef || {} } } } }; }, /** * This builds both field list and label list. */ buildSelectedList: function() { var self = this; var selectedList = {}; var labelList = []; // the main ul elements of the selected list, one ul for each module $('.columns ul.field-list').each(function(idx, ul) { var module = $(ul).attr('module_name'); // init selectedList for this module selectedList = self.buildViewMetaObject(module); // init labelList for this module labelList = []; $(ul).children('li').each(function(idx2, li) { if (_.isEmpty($(li).attr('fieldname'))) { // multi field column selectedList.base.view['multi-line-list'].panels[0].fields .push(self.buildMultiFieldObject(li, module, labelList)); } else { // single field column selectedList.base.view['multi-line-list'].panels[0].fields .push(self.buildSingleFieldObject(li, module)); } }); }); this.selectedFieldList = selectedList; this.labelList = labelList; }, /** * * @param li The <li> element that represents the multi field column * @param module Module name * @param labelList The label list * @return Object */ buildMultiFieldObject: function(li, module, labelList) { var subfields = []; var header = $(li).find('li.list-header'); var self = this; // We may need to add the label to the system if it's a multi field column this.addLabelToList(header, module, labelList); // construct the field level definitions in subfields $(li).find('li.pill').each(function(idx2, li) { var field = {default: true, enabled: true}; var fieldname = $(li).attr('fieldname'); if (self.isSpecialField(fieldname, module)) { self.buildSpecialField(fieldname, field, module); } else { self.buildRegularField(li, field, module); } subfields.push(field); }); return { // column level definitions name: $(header).attr('fieldname'), label: $(header).attr('fieldlabel'), subfields: subfields }; }, /** * * @param header The header element * @param module Module name * @param labelList The list to be added to */ addLabelToList: function(header, module, labelList) { var label = $(header).attr('fieldlabel'); var labelValue = $(header).attr('data-original-title'); if (label == app.lang.get(label, module) && !_.isEmpty(labelValue)) { // label not already in system, add it to the list to save to system labelList.push({label: label, labelValue: labelValue}); } }, /** * * @param li The <li> element * @param module * @return Object */ buildSingleFieldObject: function(li, module) { var subfields = []; var field = {default: true, enabled: true}; var fieldname = $(li).attr('fieldname'); // construct the field level definitions in subfields if (this.isSpecialField(fieldname, module)) { this.buildSpecialField(fieldname, field, module); } else { this.buildRegularField(li, field, module); } subfields.push(field); return { // column level definitions name: $(li).attr('fieldname'), label: $(li).attr('fieldlabel'), subfields: subfields }; }, /** * To check if this is a special field. * @param fieldname * @param module * @return {boolean} true if it's a special field, false otherwise */ isSpecialField: function(fieldname, module) { var type = app.metadata.getModule(module, 'fields')[fieldname].type; return type == 'widget'; }, /** * To build the special field definitions. * @param fieldname The field name * @param field The field object to be populated * @param module The module name */ buildSpecialField: function(fieldname, field, module) { var console = app.metadata.getModule(module, 'fields')[fieldname].console; // copy everything from console for (property in console) { field[property] = console[property]; } field.widget_name = fieldname; }, /** * Gets a list of the underlying fields contained in a multi-line list * @param module * @return {Array} a list of field definitions from the multi-line list metadata * @private */ _getMetaFields: function(module) { let multiLineMeta = app.metadata.getView(module, 'multi-line-list'); let subfields = []; _.each(multiLineMeta.panels, function(panel) { _.each(panel.fields, function(fieldDefs) { subfields = subfields.concat(fieldDefs.subfields); }); }, this); return subfields; }, /** * To build the regular field definitions * @param li The <li> element of a regular field. * @param field The field object to be populated * @param module The module name */ buildRegularField: function(li, field, module) { field.name = $(li).attr('fieldname'); field.label = $(li).attr('fieldlabel'); var fieldDef = app.metadata.getModule(module, 'fields')[field.name]; var type = fieldDef.type; field.type = type; let metaFields = this._getMetaFields(module); let metaField = metaFields.find(metaField => metaField.name === field.name && !metaField.widget_name); if (metaField && metaField.type) { field.type = metaField.type; if (metaField.disable_field) { field.disable_field = metaField.disable_field; } } if (!_.isEmpty(fieldDef.related_fields)) { field.related_fields = fieldDef.related_fields; } if (type === 'relate') { // relate field, get the actual field type var actualType = this.getRelateFieldType(field.name, module); if (!_.isEmpty(actualType) && actualType === 'enum') { // if the actual type is enum, need to add enum and enum_module field.type = actualType; field.enum_module = fieldDef.module; } else { // not enum type, add module and related_fields field.module = fieldDef.module; field.related_fields = fieldDef.related_fields || [fieldDef.id_name]; } field.link = false; } else if (type === 'name') { field.link = false; } else if (type === 'text') { if (_.isEmpty(fieldDef.dbType)) { // if type is text and there is no dbType (such as description field) // make it not sortable field.sortable = false; } } }, /** * To get the actual field type of a relate field. * @param fieldname * @param module * @return {string|*} */ getRelateFieldType: function(fieldname, module) { var fieldDef = app.metadata.getModule(module, 'fields')[fieldname]; if (!_.isEmpty(fieldDef) && !_.isEmpty(fieldDef.rname) && !_.isEmpty(fieldDef.module)) { return app.metadata.getModule(fieldDef.module, 'fields')[fieldDef.rname].type; } return ''; }, /** * Parses the 'order by' components of the given model for the given field * and concatenates them into the proper ordering string. Example: if the * primary sort field is 'name', and primary sort direction is 'asc', * it will return 'name:asc' * * @param {Object} model the model being saved * @param {string} the base field name * @private */ _buildOrderByValue: function(model, fieldName) { var value = model.get(fieldName) || ''; if (!_.isEmpty(value)) { var direction = model.get(fieldName + '_direction') || 'asc'; value += ':' + direction; } return value; }, /** * Calls the context model save and saves the config model in case * the default model save needs to be overwritten * * @protected */ _saveConfig: function() { this.validateModel(_.bind(function(result) { if (!result.isValid) { this.showButton('save_button'); this.showInvalidModel(); } else { this._setOrderByFields(); this.model.save({}, { success: _.bind(function() { this.showSavedConfirmation(); if (app.drawer.count()) { // close the drawer and return to Opportunities app.drawer.close(this.context, this.context.get('model')); // Config changed... reload metadata app.sync(); } else { app.router.navigate(app.router.getPreviousFragment() || this.module, {trigger: true}); } }, this), error: _.bind(function() { this.showButton('save_button'); }, this) }); } }, this)); }, /** * @inheritdoc */ _getSaveConfigURL: function() { return app.api.buildURL(this.module); }, /** * @inheritdoc */ _render: function() { this.action = this.context.get('action'); this._super('_render'); this.setButtonStates(); }, /** * Shows a button. * @param {string} name */ showButton: function(name) { this.getField(name).setDisabled(false); this.$('a[name=' + name + ']').removeClass('hide'); }, /** * Shows buttons for current action */ setButtonStates: function() { let acls = app.user.getAcls().Metrics; let isAdmin = (app.user.get('type') == 'admin'); let isDev = (!_.has(acls, 'developer')); let $cancelButton = this.$('a[name=cancel_button]'); let $saveButton = this.$('a[name=save_button]'); let $editButton = this.$('a[name=edit_button]'); if (this.action === 'detail') { $cancelButton.addClass('hide'); $saveButton.addClass('hide'); if (!isAdmin && !isDev) { $editButton.addClass('hide'); } else { $editButton.removeClass('hide'); } } else { $editButton.addClass('hide'); $cancelButton.removeClass('hide'); $saveButton.removeClass('hide'); } }, /** * Validates model using the validation tasks */ validateModel: function(callback) { var fieldsToValidate = {}; var allFields = this.getFields(this.module, this.model); for (var fieldKey in allFields) { if (app.acl.hasAccessToModel('edit', this.model, fieldKey)) { _.extend(fieldsToValidate, _.pick(allFields, fieldKey)); } } this.model.doValidate(fieldsToValidate, function(isValid) { callback({isValid: isValid}); }); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.MetricsConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseMetricsConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'BaseConfigDrawerLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); }, /** * Sets up the models for each of the enabled modules from the configs */ loadData: function(options) { this.collection.add(app.data.createBean(this.module)); }, /** * @return {boolean} * @private */ _checkConfigMetadata: function() { return true; }, }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.MetricsRecordLayout * @alias SUGAR.App.view.layouts.BaseMetricsRecordLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Record Layout (base) extendsFrom: 'BaseConfigDrawerLayout', plugins: ['ErrorDecoration'], events: { 'click a[name="cancel_button"]': 'editCancelled', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.createMode = false; if (this.context.get('action') === 'edit') { this.action = 'edit'; // if the model is not fully loaded, then load model again // else set model if (_.isUndefined(this.model.get('metric_module'))) { this.loadData(); } else { this.setModel(this.model); } } else { if (this.model.get('id')) { this.action = 'detail'; this.loadData(); } else { this.createMode = true; this.action = 'edit'; this.setModel(this.model); } } this.context.set({create: this.createMode, action: this.action}); this.context.on('edit:clicked', this.editClicked, this); this.context.on('edit:cancelled', this.editCancelled, this); }, /** * @inheritdoc */ loadData: function(options) { if (!this.model.get('id')) { return; } app.alert.show('fetching_metric', { level: 'process', title: app.lang.get('LBL_LOADING'), autoClose: false }); this.model.fetch({ success: _.bind(function() { if (this.disposed) { return; } this.setModel(this.model); this.render(); }, this), complete: function() { app.alert.dismiss('fetching_metric'); } }); }, /** * @inheritdoc */ _render: function() { if (!this.createMode && !this.model.dataFetched) { return; } return this._super('_render'); }, /** * @inheritdoc */ setContextModel: function(options) { return; }, /** * Checks Metrics ACLs to see if the User is a system admin * or if the user has a developer role for the Metrics module * * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().Metrics; var isSysAdmin = (app.user.get('type') == 'admin'); var isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isDev || this.action !== 'edit'); }, _checkConfigMetadata: function() { return true; }, /** * Handles 'edit' button click */ editClicked: function() { this.action = 'edit'; this.context.set('action', this.action); this.render(); }, /** * Handles 'cancel' button click */ editCancelled: function() { this.action = 'detail'; this.context.set('action', this.action); this.model.revertAttributes({ hideDbvWarning: true }); this.render(); }, /** * @inheritdoc */ _checkConfigMetadata: function() { return true; }, /** * Takes a stored order_by value and splits it into field name and direction * * @param value * @return {Array} an array containing the order_by field and direction data * @private */ _parseOrderByComponents: function(value) { if (_.isString(value)) { return value.split(':'); } return []; }, /** * Sets default values. * @param {Object} bean */ setModelDefaults: function(bean) { var config = app.metadata.getView(bean.get('metric_module'), 'multi-line-list') || {}; var defaults = config.defaults || {}; var defaultAttributes = {}; _.each(defaults, function(value, key) { if (key === 'order_by_primary' || key === 'order_by_secondary') { var orderByComponents = this._parseOrderByComponents(value); defaultAttributes[key] = orderByComponents[0] || ''; defaultAttributes[key + '_direction'] = orderByComponents[1] || 'desc'; } else { defaultAttributes[key] = value; } }, this); bean.set('defaults', defaultAttributes); }, /** * Sets up the model */ setModel: function(bean) { if (!this.checkAccess()) { this.blockModule(); return; } this.setModelDefaults(bean); this.setTabContent(bean); this.setFilterableFields(bean); this.addValidationTasks(bean); bean.on('change:columns', function() { this.setTabContent(bean, true); this.setSortValues(bean); }, this); }, /** * Sets the filterable fields * @param bean */ setFilterableFields: function(bean) { var module = bean.get('metric_module'); var filterableFields = app.data.getBeanClass('Filters').prototype.getFilterableFields(module); bean.set('filterableFields', filterableFields); }, /** * Sets tab content for the module on the bean * * @param {Object} bean to model * @param {boolean} update Flag to show if it's the updating of bean */ setTabContent: function(bean, update) { update = update || false; var tabContent = {}; var module = bean.get('metric_module'); var multiLineFields = update ? this.getColumns(bean) : this._getMultiLineFields(module, bean); // Set the information about the tab's fields, including which fields // can be used for sorting var fields = {}; var sortFields = {}; var nonSortableTypes = ['id', 'widget']; _.each(multiLineFields, function(field) { if (_.isObject(field) && app.acl.hasAccess('read', module, null, field.name)) { // Set the field information fields[field.name] = field; // Set the sort field information if the field is sortable var label = app.lang.get(field.label || field.vname, module); var isSortable = !_.isEmpty(label) && field.sortable !== false && field.sortable !== 'false' && nonSortableTypes.indexOf(field.type) === -1; if (isSortable) { sortFields[field.name] = label; } } }); tabContent.fields = fields; tabContent.sortFields = sortFields; bean.set('tabContent', tabContent); bean.trigger('change:tabContent'); }, /** * Sets values of the sortable fields * * @param {Object} bean */ setSortValues: function(bean) { const sortValue1 = bean.get('order_by_primary'); const sortValue2 = bean.get('order_by_secondary'); const columns = this.getColumns(bean); if (sortValue2 && !columns[sortValue2]) { bean.set('order_by_secondary', ''); } if (sortValue1 && !columns[sortValue1]) { if (sortValue2) { bean.set('order_by_primary', sortValue2); bean.set('order_by_secondary', ''); } else { bean.set('order_by_primary', ''); } } }, /** * Return values of the sortable fields using selected columns and metadata * * @param {Object} bean * @return {Object} a list fields by selected columns */ getColumns: function(bean) { const module = bean.get('metric_module'); var columns = bean.get('columns'); var moduleFields = app.metadata.getModule(module, 'fields'); _.each(columns, function(field, key) { // add related_fields from widgets, they should be sortable if (!_.isEmpty(field.console) && !_.isEmpty(field.console.related_fields)) { var relatedFields = field.console.related_fields; _.each(relatedFields, function(field) { if (_.isEmpty(columns[field]) && !_.isEmpty(moduleFields[field])) { columns[field] = moduleFields[field]; } }); } }); return columns; }, /** * Gets a unique list of the underlying fields contained in a multi-line list * @param module * @param {Object} bean * @return {Array} a list of field definitions from the multi-line list metadata * @private */ _getMultiLineFields: function(module, bean) { // Get the unique lists of subfields and related_fields from the multi-line // list metadata of the module var beanViewDefs = bean.attributes.viewdefs; var multiLineMeta = beanViewDefs && beanViewDefs.base ? beanViewDefs.base.view['multi-line-list'] : app.metadata.getView(module, 'multi-line-list'); var moduleFields = app.metadata.getModule(module, 'fields'); var subfields = []; var relatedFields = []; _.each(multiLineMeta.panels, function(panel) { var panelFields = panel.fields; _.each(panelFields, function(fieldDefs) { subfields = subfields.concat(fieldDefs.subfields); _.each(fieldDefs.subfields, function(subfield) { if (subfield.related_fields) { var related = _.map(subfield.related_fields, function(relatedField) { return moduleFields[relatedField]; }); relatedFields = relatedFields.concat(related); } }); }, this); }, this); // To filter out special fields as they should not be available for sorting or filtering. subfields = _.filter(subfields, function(field) { return _.isEmpty(field.widget_name); }); // Return the combined list of subfields and related fields. Ensure that // the correct field type is associated with the field (important for // filtering) var fields = _.compact(_.uniq(subfields.concat(relatedFields), false, function(field) { return field.name; })); return _.map(fields, function(field) { if (moduleFields[field.name]) { field.type = moduleFields[field.name].type; } return field; }); }, /** * Adds validation tasks to the fields in the layout for the enabled modules */ addValidationTasks: function(bean) { if (bean !== undefined) { bean.addValidationTask('check_name', _.bind(this._validateName, bean)); bean.addValidationTask('check_order_by_primary', _.bind(this._validatePrimaryOrderBy, bean)); } }, /** * Validates name * * @protected */ _validateName: function(fields, errors, callback) { if (_.isEmpty(this.get('name'))) { errors.name = errors.name || {}; errors.name.required = true; } callback(null, fields, errors); }, /** * Validates table header values for the enabled module * * @protected */ _validatePrimaryOrderBy: function(fields, errors, callback) { if (_.isEmpty(this.get('order_by_primary'))) { errors.order_by_primary = errors.order_by_primary || {}; errors.order_by_primary.required = true; } callback(null, fields, errors); } }) }, "record-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.MetricsRecordContentLayout * @alias SUGAR.App.view.layouts.BaseMetricsRecordContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Record-content Layout (base) extendsFrom: 'BaseConfigDrawerContentLayout', /** * @inheritdoc */ _render: function() { this._super('_render'); this.$el.addClass('record-panel'); } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.MetricsConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseMetricsConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'BaseConfigDrawerContentLayout', /** * @inheritdoc */ _render: function() { this._super('_render'); this.$el.addClass('record-panel'); } }) } }} , "datas": {} }, "Messages":{"fieldTemplates": { "base": { "conversation": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Messages.ConversationField * @alias SUGAR.App.view.fields.BaseMessagesConversationField * @extends View.Fields.BaseField */ ({ // Conversation FieldTemplate (base) /** * Event listeners */ events: { 'click .more-btn': 'paginate', }, /** * List of parsed messages */ messagesList: [], /** * Current page of pagination */ page: 1, /** * Default settings used when none are supplied through metadata */ _defaultSettings: { max_display_messages: 'all', pagination: false, per_page: 10, }, /** * Paginate messages */ paginate: function() { this.page++; this.render(); }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.page = 1; this.messagesList = []; this._initSettings(); this.platform = app.config.platform; }, /** * Initialize settings, default settings are used when none are supplied * through metadata. * * @return {View.Fields.BaseTextareaField} Instance of this field. * @protected */ _initSettings: function() { this._settings = _.extend({}, this._defaultSettings, this.def && this.def.settings || {} ); }, /** * Parse messages from the Conversation field * * @param {string} transcript * @return {Array} parsed messages */ parseMessages: function(transcript) { if (this.messagesList.length) { return; } var allowedAuthors = ['CUSTOMER', 'AGENT']; var allAuthors = _.union(allowedAuthors, ['SYSTEM']); var allAuthorsStr = allAuthors.join('|'); if (transcript) { var reg = new RegExp(`\\[(${allAuthorsStr}) [a-zA-Z0-9_\\s]+\\] \\d{2}:\\d{2}`, 'i'); var result = transcript.split(reg); var maxShow = this._settings.max_display_messages; var author = ''; _.each(result, _.bind(function(item) { var value = item.trim(); if (allAuthors.indexOf(value) >= 0) { author = value; } if (allowedAuthors.indexOf(author) >= 0 && value !== author && (maxShow === 'all' || this.messagesList.length < parseInt(maxShow))) { this.messagesList.push({ author: author, message: value, }); } }, this)); } }, /** * @inheritdoc * * @param {string} value The value to format * @return {Array} formatted value */ format: function(value) { var messageBox = {}; var messages = []; var showCount = this.page * this._settings.per_page; this.parseMessages(value); this.moreBtn = this._settings.pagination && this.messagesList.length > showCount; _.each(this.messagesList, _.bind(function(item, key) { if (this._settings.pagination && key >= showCount) { return; } if (item.author === messageBox.author) { messageBox.messagesList.push(item.message); } else { messageBox = { author: item.author, messagesList: [ item.message, ], }; messages.push(messageBox); } }, this)); return messages; } }) } }} , "views": { "base": { "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Messages.RecordView * @alias SUGAR.App.view.views.MessagesRecordView * @extends View.Views.Base.RecordView */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee']); this._super('initialize', [options]); } }) }, "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Messages.ActivityCardContentView * @alias SUGAR.App.view.views.MessagesActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * Return formatted date of the end chat */ getMessageDate: function() { return this.activity.get('date_end') ? app.date(this.activity.get('date_end')).formatUser() : ''; }, /** * Get the status message * * @return {String} */ getStatusMessage: function() { var message = ''; var statusString = ''; switch(this.activity.get('status')) { case 'Completed': statusString = app.lang.get('LBL_ACTIVITY_FINISHED', this.module); break; default: statusString = app.lang.get('LBL_ACTIVITY_IN_PROGRESS', this.module); } message = '(' + app.lang.get('LBL_ACTIVITY_STATUS_TEXT', this.module) + ' ' + statusString + ') ' + this.getMessageDate(); return message; }, /** * Set the status message in the status panel * * This function manipulates the DOM, so must be invoked after any render call */ setStatusMessage: function() { var $panel = this.$el.find('.panel-status'); if ($panel && $panel.length) { $panel.append(this.getStatusMessage()); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.setStatusMessage(); } }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Messages.CreateView * @alias SUGAR.App.view.views.MessagesCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['AddAsInvitee']); this._super('initialize', [options]); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Messages.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseMessagesActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setUsersFields(); }, /** * @inheritdoc * * Do not set user fields as that will be set after activity fetch */ setUsersPanel: function() { this.setUsersTemplate(); }, /** * @inheritdoc */ setUsersFields: function() { this.setInvitees(); } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.MessagesModel * @alias SUGAR.App.model.datas.BaseMessagesModel * @extends Model.Bean */ ({ // Model Data (base) plugins: ['VirtualCollection'] }) } }} }, "Audit":{"fieldTemplates": { "base": { "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Audit.CurrencyField * @alias SUGAR.App.view.fields.BaseAuditCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); //audit log is always in base currency. Make sure the currency def reflects that. this.def.is_base_currency = true; } }) }, "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Htmleditable_tinymce FieldTemplate (base) extendsFrom: 'Htmleditable_tinymceField', /** * Sets the content displayed in the non-editor view * * @param {String} value Sanitized HTML to be placed in view */ setViewContent: function(value) { var editable = this._getHtmlEditableField(); if (this.action == 'list') { // Strip HTML tags for ListView. value = $('<div/>').html(value).text(); } if (!editable) { return; } if (!_.isUndefined(editable.get(0)) && !_.isEmpty(editable.get(0).contentDocument)) { if (editable.contents().find('body').length > 0) { var frame = editable.get(0); frame.contentWindow.document.open(); frame.contentWindow.document.write(value); frame.contentWindow.document.close(); } } else { // If the element has no body, the iframe hasn't loaded. Wait until // it loads so we don't write to non-sandboxed element editable.on('load', function() { this.setViewContent(value); }, this); } } }) }, "fieldtype": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.Audit.FieldtypeField * @alias SUGAR.App.view.fields.BaseAuditFieldtypeField * @extends View.Fields.Base.BaseField */ ({ // Fieldtype FieldTemplate (base) /** * @inheritdoc * Convert the raw field type name * into the label of the field of the parent model. */ format: function(value) { if (this.context && this.context.parent) { var parentModel = this.context.parent.get('model'), field = parentModel.fields[value]; if (field) { value = app.lang.get(field.label || field.vname, parentModel.module); } } return value; } }) } }} , "views": { "base": { "activity-card-header-create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardHeaderCreateView * @alias SUGAR.App.view.views.BaseAuditActivityCardHeaderCreateView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header-create View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); const module = this.activity.get('parent_model').module; this.moduleName = app.lang.getModuleName(module); const name = `${this.moduleName} ${app.lang.get('LBL_CREATED', this.module)}`; this.createModel = app.data.createBean(module, { id: this.activity.get('parent_id'), name: name, created_by_name: this.activity.get('created_by_name'), created_by: this.activity.get('created_by'), }); }, /** * @inheritdoc */ getActivityCardLayout: function() { return this.closestComponent('activity-card-create'); }, /** * @inheritdoc */ setUsersFields: function() { const panel = this.getUsersPanel(); this.userField = _.find(panel.defaultFields, function(field) { return field.name === 'created_by_name'; }); this.hasAvatarUser = !!this.userField; }, }) }, "activity-card-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardMenuView * @alias SUGAR.App.view.views.BaseAuditActivityCardMenuView * @extends View.Views.Base.ActivityCardMenuView */ ({ // Activity-card-menu View (base) /** * {@inheritdoc} */ _render: function() { this._super('_render'); if (!this.cabMenu.length && !this.cabButtons.length) { this.$el.remove(); } }, }) }, "activity-card-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardContentView * @alias SUGAR.App.view.views.BaseAuditActivityCardContentView * @extends View.Views.Base.ActivityCardContentView */ ({ // Activity-card-content View (base) extendsFrom: 'ActivityCardContentView', /** * The panel_change panel metadata */ changePanel: null, /** * A list of change field definitions for a single field * * 0 name def | 1 before def | 2 after def */ changeDef: [], /** * Enum type fields that can take the type of 'enum-colorcoded' field */ enumColorcodedFields: [ 'status' ], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.setChangeDef(true); }, /** * Initializes hbs date variables with date_created */ initDateDetails: function() { if (this.activity && this.activity.get('date_created')) { value = app.date(this.activity.get('date_created')); if (value.isValid()) { this.dateModified = value.formatUser(); } } }, /** * Get and cache the change panel * * @return {Object} */ getChangePanel: function() { if (!this.changePanel) { this.changePanel = this.getMetaPanel('panel_change'); } return this.changePanel; }, /** * Get the specified field def from the change panel's * defaultFields * * @param fieldName the field name to find * @return {Object} the field def from panel metadata */ getFieldDefFromChangePanelFields: function(fieldName) { var panel = this.getChangePanel(); return _.find(panel.defaultFields, function(field) { return field.name === fieldName; }); }, /** * Get the specified field from the model and add relevant data * * @param model * @param fieldName * @param type * @return {Object} */ getChangeFieldDefFromModel: function(model, fieldName, type) { var def = {}; // retrieving field def from the model will not have the defined css_class var fieldDefFromPanel = this.getFieldDefFromChangePanelFields(type); if (fieldDefFromPanel) { def.css_class = fieldDefFromPanel.css_class; } // if the value is empty, return minimal field def if (!model.get(fieldName)) { return def; } def = _.extend( def, _.find(model.fields, function(field) { return field.name === fieldName; }), { model: model } ); // convert fields to type 'enum-colorcoded' if conditions hold if (type === 'after' && def.type === 'enum' && _.indexOf(this.enumColorcodedFields, fieldName) !== -1) { def.type = 'enum-colorcoded'; def.template = 'list'; } return def; }, /** * Get the specified field from the panel and add relevant data * * @param model * @param fieldName * @return {Object} */ getChangeFieldDefFromPanel: function(model, fieldName) { var def = this.getFieldDefFromChangePanelFields(fieldName); if (!def) { return {}; } return _.extend(def, { model: model }); }, /** * Get the change value from the activity model * * Depending on the field, the change value can be in an array or a string * * @param type 'before' or 'after' * @return {string} the value */ getChangeValue: function(type) { var value = this.activity.get(type); return _.isArray(value) ? _.first(value) : value; }, /** * Get the change model for the specified module and type * * @param module the parent module (usually not Audit) * @param type 'before' or 'after' * @return {Bean} */ getTypeChangeModel: function(module, type) { var value = this.getChangeValue(type); var fieldName = this.activity.get('field_name'); return app.data.createBean(module, { [fieldName]: value }); }, /** * Set change def with change fields * * @param resetDef true will clear existing changeDef */ setChangeDef: function(resetDef) { if (!this.activity) { return; } if (resetDef) { this.changeDef = []; } var parentModule = this.activity.get('parent_model').get('_module') ? this.activity.get('parent_model').get('_module') : this.context.get('module'); var fieldName = this.activity.get('field_name'); // the hbs template expects the following index order: // 0 name field // 1 before field // 2 after field this.changeDef.push( this.getChangeFieldDefFromPanel(this.activity, 'field_name'), this.getChangeFieldDefFromModel( this.getTypeChangeModel(parentModule, 'before'), fieldName, 'before' ), this.getChangeFieldDefFromModel( this.getTypeChangeModel(parentModule, 'after'), fieldName, 'after' ) ); } }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseAuditActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) extendsFrom: 'ActivityCardDetailView', /** * @inheritdoc */ getModulesCardMeta: function(baseModule) { this.setBaseModule(); const customName = 'activity-card-definition-for-' + baseModule.toLowerCase(); return app.metadata.getView(this.baseModule, customName) || app.metadata.getView(this.baseModule, 'activity-card-definition'); }, /** * Set up base module variable */ setBaseModule: function() { if (this.baseModule) { return; } const parentModel = this.activity.get('parent_model'); this.baseModule = (this.activity.module === 'Audit' && parentModel && parentModel.module) ? parentModel.module : this.activity.module; }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseAuditActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setHeaderPanel: function() { this._super('setHeaderPanel'); this.updateModule = this.activity.get('parent_model').get('_module') ? this.activity.get('parent_model').get('_module') : this.context.get('module'); }, }) }, "activity-card-detail-create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Audit.ActivityCardDetailCreateView * @alias SUGAR.App.view.views.BaseAuditActivityCardDetailCreateView * @extends View.Views.Base.Audit.ActivityCardDetailView */ ({ // Activity-card-detail-create View (base) extendsFrom: 'ActivityCardDetailView', /** * @inheritdoc */ getActivityCardLayout: function() { return this.closestComponent('activity-card-create'); }, }) } }} , "layouts": { "base": { "activity-card-create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.Audit.ActivityCardCreateLayout * @alias SUGAR.App.view.layouts.BaseAuditActivityCardCreateLayout * @extends View.Layouts.Base.ActivityCardLayout */ ({ // Activity-card-create Layout (base) extendsFrom: 'ActivityCardLayout', }) } }} , "datas": {} }, "RevenueLineItems":{"fieldTemplates": { "base": { "badge": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.BadgeField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsBadgeField * @extends View.Fields.Base.RowactionField */ ({ // Badge FieldTemplate (base) /** * @inheritdoc */ extendsFrom: 'RowactionField', /** * @inheritdoc */ showNoData: false, /** * @inheritdoc */ bindDataChange: function() { this.model.on('change:' + this.name, this.render, this); } }) }, "currency": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.CurrencyField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsCurrencyField * @extends View.Fields.Base.CurrencyField */ ({ // Currency FieldTemplate (base) extendsFrom: 'BaseCurrencyField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); // Enabling currency dropdown on RLI list views this.hideCurrencyDropdown = false; } }) }, "convert-to-quote": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.ConvertToQuoteField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsConvertToQuoteField * @extends View.Fields.Base.RowactionField */ ({ // Convert-to-quote FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc * * @param {Object} options */ initialize: function(options) { this._super('initialize', [options]); this.type = 'rowaction'; this.context.on('button:convert_to_quote:click', this.convertToQuote, this); }, /** * convert RLI to quote * @param {Object} e */ convertToQuote: function(e) { var massCollection = this.context.get('mass_collection'); if (!massCollection) { massCollection = this.context.get('collection').clone(); this.context.set('mass_collection', massCollection); } this.view.layout.trigger('list:massquote:fire'); }, /** * @inheritdoc */ isAllowedDropdownButton: function() { // Filter logic for when it's on a dashlet return this.view.name !== 'dashlet-toolbar'; } }) }, "enum": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.EnumField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsEnumField * @extends View.Fields.Base.EnumField */ ({ // Enum FieldTemplate (base) extendsFrom: 'EnumField', /** * List of valid cascadable fields of this type * @property {Array} */ cascadableFields: ['sales_stage', 'commit_stage'], /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.view.name === 'subpanel-for-opportunities-create' && this.cascadableFields.includes(this.name)) { let oppModel = this.context.parent.get('model'); oppModel.on('cascade:checked:' + this.name, function(checked) { if (this.disposed || !app.utils.isRliFieldValidForCascade(this.model, this.name)) { return; } this.setDisabled(checked, {trigger: true}); }, this); this.context.on('field:disabled', function(fieldName) { if (this.name === fieldName) { let oppModel = this.context.parent.get('model'); if (app.utils.isTruthy(oppModel.get(this.name + '_cascade_checked'))) { this.setDisabled(true); } } }, this); } } }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * extend save options * @param {Object} options save options. * @return {Object} modified success param. */ getCustomSaveOptions: function(options) { // make copy of original function we are extending var origSuccess = options.success; // return extended success function with added alert return { success: _.bind(function() { if (_.isFunction(origSuccess)) { origSuccess.apply(this, arguments); } if (this.model && !_.isEmpty(this.model.get('quote_id'))) { app.alert.show('save_rli_quote_notice', { level: 'info', messages: app.lang.get( 'SAVE_RLI_QUOTE_NOTICE', 'RevenueLineItems' ), autoClose: true }); } // trigger a save event across the parent context so listening components // know the changes made in this row has been saved if(this.context.parent) { this.context.parent.trigger('editablelist:save', this.model); } }, this) }; } }) }, "rowactions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /* * @class View.Fields.Base.RevenueLineItems.RowactionsField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsRowactionsField * @extends View.Fields.Base.RowactionsField */ ({ // Rowactions FieldTemplate (base) extendsFrom: 'RowactionsField', /** * Enable or disable caret depending on if there are any enabled actions in the dropdown list * * @inheritdoc * @private */ _updateCaret: function() { // Left empty on purpose, the menu should always show } }) }, "rowaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Rowaction FieldTemplate (base) extendsFrom: "RowactionField", /** * @inheritdoc */ initialize: function(options) { this.plugins = _.clone(this.plugins) || []; if (!options.context.get('isCreateSubpanel')) { // if this is not a create subpanel, add the DisableDelete plugin // on a create subpanel, don't add the plugin so users can delete rows this.plugins.push('DisableDelete'); } this._super("initialize", [options]); } }) }, "relate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.RelateField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsRelateField * @extends View.Fields.Base.RelateField */ ({ // Relate FieldTemplate (base) extendsFrom: 'BaseRelateField', /** * @inheritdoc */ initialize: function(options) { // deleting for RLI create when there is no account_id. if (options && options.def.filter_relate && !options.model.has('account_id')) { delete options.def.filter_relate; } this._super('initialize', [options]); }, setValue: function(models) { if (!models) { return; } var userCurrency = app.user.getCurrency(); var createInPreferred = userCurrency.currency_create_in_preferred; var currencyFields; var currencyFromRate; if (this.name === 'product_template_name' && createInPreferred) { // get any currency fields on the model currencyFields = _.filter(this.model.fields, function(field) { return field.type === 'currency'; }); currencyFromRate = models.base_rate; models.currency_id = userCurrency.currency_id; models.base_rate = userCurrency.currency_rate; _.each(currencyFields, function(field) { // if the field exists on the model, convert the value to the new rate if (models[field.name] && field.name.indexOf('_usdollar') === -1) { models[field.name] = app.currency.convertWithRate( models[field.name], currencyFromRate, userCurrency.currency_rate ); } }, this); } this._super('setValue', [models]); } }) }, "date": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.DateField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsDateField * @extends View.Fields.Base.DateField */ ({ // Date FieldTemplate (base) extendsFrom: 'DateField', /** * List of valid cascadable fields of this type * @property {Array} */ cascadableFields: ['date_closed', 'service_start_date'], /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.model && this.name && this.name === 'service_start_date') { this.model.on('addon:pli:changed', this.handleRecalculateServiceDuration, this); this.model.on('change:' + this.name, this.handleRecalculateServiceDuration, this); } if (this.view.name === 'subpanel-for-opportunities-create' && this.cascadableFields.includes(this.name)) { let oppModel = this.context.parent.get('model'); oppModel.on('cascade:checked:' + this.name, function(checked) { if (this.disposed || !app.utils.isRliFieldValidForCascade(this.model, this.name)) { return; } this.setDisabled(checked, {trigger: true}); }, this); this.context.on('field:disabled', function(fieldName) { if (this.name === fieldName) { let oppModel = this.context.parent.get('model'); if (app.utils.isTruthy(oppModel.get(this.name + '_cascade_checked'))) { this.setDisabled(true); } } }, this); } }, /** * If this is a coterm RLI, recalculate the service duration when the start date * changes so that the end date remains constant. */ handleRecalculateServiceDuration: function() { if (!_.isEmpty(this.model.get('add_on_to_id')) && app.utils.isTruthy(this.model.get('service'))) { var startDate = app.date(this.model.get('service_start_date')); var endDate = app.date(this.model.get('service_end_date')); if (startDate.isSameOrBefore(endDate)) { // we want to be inclusive of the end date endDate.add(1, 'days'); } // calculates the whole years, months, or days var wholeDurationUnit = this.getWholeDurationUnit( startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD') ); if (!_.isEmpty(wholeDurationUnit)) { this.model.set('service_duration_unit', wholeDurationUnit); this.model.set('service_duration_value', endDate.diff(startDate, wholeDurationUnit + 's')); } else { this.model.set('service_duration_unit', 'day'); this.model.set('service_duration_value', endDate.diff(startDate, 'days')); } } }, /** * Gets the whole years, months, or days between two dates * * @param {string} startDate the start date * @param {string} endDate the end date * @return {string} whole year, month or day unit */ getWholeDurationUnit: function(startDate, endDate) { var start = app.date(startDate); var end = app.date(endDate); var years = end.diff(start, 'years'); start.add(years, 'years'); var months = end.diff(start, 'months'); start.add(months, 'months'); var days = end.diff(start, 'days'); return days > 0 ? 'day' : (months > 0 ? 'month' : (years > 0 ? 'year' : '')); } }) }, "fieldset": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.RevenueLineItems.FieldsetField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsFieldsetField * @extends View.Fields.Base.FieldsetField */ ({ // Fieldset FieldTemplate (base) extendsFrom: 'FieldsetField', /** * List of valid cascadable fields of this type * @property {Array} */ cascadableFields: ['service_duration'], /** * Cascadable fieldsets with subfields * @property {Object} */ cascadableSubfields: { 'service_duration': ['service_duration_unit', 'service_duration_value'] }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); if (this.view.name === 'subpanel-for-opportunities-create' && this.cascadableFields.includes(this.name)) { let oppModel = this.context.parent.get('model'); oppModel.on('cascade:checked:' + this.name, function(checked) { if (this.disposed) { return; } if (!this.cascadableSubfields[this.name].every(subfield => { return app.utils.isRliFieldValidForCascade(this.model, subfield); })) { return; } this.setDisabled(checked, {trigger: true}); }, this); this.context.on('field:disabled', function(fieldName) { Object.entries(this.cascadableSubfields).forEach(([field, subfields]) => { if (subfields.includes(fieldName) && field === this.name) { let oppModel = this.context.parent.get('model'); if (app.utils.isTruthy(oppModel.get(this.name + '_cascade_checked'))) { this.setDisabled(true); } } }); }, this); } } }) }, "actiondropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Create a dropdown button that contains multiple * {@link View.Fields.Base.RowactionField} fields. * * @class View.Fields.Base.RevenueLineItems.ActiondropdownField * @alias SUGAR.App.view.fields.BaseRevenueLineItemsActiondropdownField * @extends View.Fields.Base.ActiondropdownField */ ({ // Actiondropdown FieldTemplate (base) extendsFrom: 'ActiondropdownField', /** * Enable or disable caret depending on if there are any enabled actions in the dropdown list * * @inheritdoc * @private */ _updateCaret: function() { // Left empty on purpose, the menu should always show } }) } }} , "views": { "base": { "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Massupdate View (base) extendsFrom: 'MassupdateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['DisableMassDelete', 'MassQuote', 'CommittedDeleteWarning']); this._super("initialize", [options]); }, /** * * @inheritdoc */ setMetadata: function(options) { var config = app.metadata.getModule('Forecasts', 'config'); this._super("setMetadata", [options]); if (!config || (config && !config.is_setup)) { _.each(options.meta.panels, function(panel) { _.every(panel.fields, function (item, index) { if (_.isEqual(item.name, "commit_stage")) { panel.fields.splice(index, 1); return false; } return true; }, this); }, this); } }, /** * @inheritdoc */ save: function(forCalcFields) { var forecastCfg = app.metadata.getModule("Forecasts", "config"); if (!this.isEndDateEditableByStartDate()) { this.handleUnEditableEndDateErrorMessage(); return; } if (forecastCfg && forecastCfg.is_setup) { // Forecasts is enabled and setup var hasCommitStage = _.some(this.fieldValues, function(field) { return field.name === 'commit_stage'; }), hasClosedModels = false; if(!hasCommitStage && this.defaultOption.name === 'commit_stage') { hasCommitStage = true; } if(hasCommitStage) { hasClosedModels = this.checkMassUpdateClosedModels(); } if(!hasClosedModels) { // if this has closed models, first time through will uncheck but not save // if this doesn't it will save like normal this._super('save', [forCalcFields]); } } else { // Forecasts is not enabled and the commit_stage field isn't in the mass update list this._super('save', [forCalcFields]); } } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom : 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['CommittedDeleteWarning']); this._super("initialize", [options]); this.before('mergeduplicates', this._checkMergeModels, this); //Extend the prototype's events object to setup additional events for this controller this.events = _.extend({}, this.events, { 'click [name=inline-cancel]': 'cancelClicked' }); }, /** * Event handler to make sure that before the merge drawer shows, make sure that all the models contain the first * records opportunity_id * * @param {Array} mergeModels * @returns {boolean} * @private */ _checkMergeModels: function(mergeModels) { var primaryRecordOppId = _.first(mergeModels).get('opportunity_id'); var invalid_models = _.find(mergeModels, function(model) { return !_.isEqual(model.get('opportunity_id'), primaryRecordOppId); }); if (!_.isUndefined(invalid_models)) { app.alert.show("merge_duplicates_different_opps_warning", { level: "warning", messages: app.lang.get('WARNING_MERGE_RLIS_WITH_DIFFERENT_OPPORTUNITIES', this.module) }); return false; } return true; }, /** * @inheritdoc * * Augment to remove the fields that should not be displayed. */ _createCatalog: function(fields) { var forecastConfig = app.metadata.getModule('Forecasts', 'config'), isSetup = (forecastConfig && forecastConfig.is_setup); if (isSetup) { fields = _.filter(fields, function(fieldMeta) { if (fieldMeta.name.indexOf('_case') !== -1) { var field = 'show_worksheet_' + fieldMeta.name.replace('_case', ''); return (forecastConfig[field] == 1); } return true; }); } else { // Forecast is not setup fields = _.reject(fields, function(fieldMeta) { return (fieldMeta.name === 'commit_stage'); }); } var catalog = this._super('_createCatalog', [fields]); return catalog; }, /** * @inheritdoc * * Tracks the last row where the view was changed to non-edit */ toggleRow: function(modelId, isEdit) { this._super('toggleRow', [modelId, isEdit]); if (!isEdit) { this.lastToggledModel = this.collection.get(modelId); } }, /** * Adds a reverting of model attributes when cancelling an edit view of * a row. This fixes issues with service fields not properly clearing when * cancelling the edit */ cancelClicked: function() { if (this.lastToggledModel) { this.lastToggledModel.revertAttributes(); } this.resize(); } }) }, "subpanel-for-opportunities-create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Custom Subpanel Layout for Revenue Line Items. * * @class View.Views.Base.RevenueLineItems.SubpanelForOpportunitiesCreate * @alias SUGAR.App.view.views.BaseRevenueLineItemsSubpanelForOpportunitiesCreate * @extends View.Views.Base.SubpanelListCreateView */ ({ // Subpanel-for-opportunities-create View (base) extendsFrom: 'SubpanelListCreateView', /** * List of field names that can cascade down from the Opp level * @property {Array} */ cascadableFields: [ 'service_duration_value', 'service_duration_unit', 'service_start_date', 'date_closed', 'sales_stage', 'commit_stage', ], /** * List of field names that can cascade down from the Opp level, as they're used in the viewdefs. * This differs from the above list as it does not have fieldset's subfields * @property {Array} */ cascadableViewFields: [ 'service_duration', 'service_start_date', 'date_closed', 'sales_stage', 'commit_stage', ], /** * List of field names that, when changed, can allow or disallow cascading * @property {Array} */ cascadePrereqFields: ['service', 'add_on_to_id', 'lock_duration'], initialize: function(options) { // From SS-492: This allows the RLI subpanel on Opportunities/create to pick up the layout from the out-of-the- // box RLI subpanel in Studio. It swaps the metadata, and then initializes the RLI subpanel on // Opportunities/create with the new metadata. It will only use the RLI subpanel metadata if it exists; // otherwise it'll use the metadata found in this folder (in the associated php file). Since creating an // Opportunity does not make any requests to the server, this functionality needs to take place in the client. var subpanelLayouts = app.metadata.getModule('Opportunities').layouts.subpanels.meta.components; var rliSubpanelLayout = _.chain(subpanelLayouts) .filter(function(e) { return e.context.link === 'revenuelineitems'; }) .first() .value(); var rliSubpanelViewName = _.property('override_subpanel_list_view')(rliSubpanelLayout); var rliModuleViews = app.metadata.getModule('RevenueLineItems').views; if (!_.isEmpty(rliSubpanelViewName)) { var customRliSubpanelViewDefs = _.property(rliSubpanelViewName)(rliModuleViews); if (!_.isEmpty(customRliSubpanelViewDefs)) { var subpanelFields = _.first(customRliSubpanelViewDefs.meta.panels).fields; _.first(options.meta.panels).fields = subpanelFields; } } this._super('initialize', [options]); }, /** * Add any custom or default fields to the bean, including Opp-level cascade fields * @inheritdoc */ _addCustomFieldsToBean: function(bean, skipCurrency, prepopulatedData) { var dom; var attrs = {}; var userCurrencyId; var userCurrency = app.user.getCurrency(); var createInPreferred = userCurrency.currency_create_in_preferred; var currencyFields; var currencyFromRate; if (bean.has('sales_stage')) { dom = app.lang.getAppListStrings('sales_probability_dom'); attrs.probability = dom[bean.get('sales_stage')]; } if (skipCurrency && createInPreferred) { // force the line item to the user's preferred currency and rate attrs.currency_id = userCurrency.currency_id; attrs.base_rate = userCurrency.currency_rate; // get any currency fields on the model currencyFields = _.filter(this.model.fields, function(field) { return field.type === 'currency'; }); currencyFromRate = bean.get('base_rate'); _.each(currencyFields, function(field) { // if the field exists on the bean, convert the value to the new rate // do not convert any base currency "_usdollar" fields if (bean.has(field.name) && field.name.indexOf('_usdollar') === -1) { attrs[field.name] = app.currency.convertWithRate( bean.get(field.name), currencyFromRate, userCurrency.currency_rate ); } }, this); } else if (!skipCurrency) { userCurrencyId = userCurrency.currency_id || app.currency.getBaseCurrencyId(); attrs.currency_id = userCurrencyId; attrs.base_rate = app.metadata.getCurrency(userCurrencyId).conversion_rate; } attrs.catalog_service_duration_value = bean.get('service_duration_value'); attrs.catalog_service_duration_unit = bean.get('service_duration_unit'); var addOnToData = this.context.parent.get('addOnToData'); if (addOnToData) { _.each(addOnToData, function(value, key) { attrs[key] = value; }, this); this.context.parent.set('addOnToData', null); } // If any Opp-level cascade fields are set, include those as well let cascadeFields = this.getCascadeFieldsFromOpp(bean, attrs); let oppModel = this._getOppModel(); if (oppModel) { Object.entries(cascadeFields).forEach(([fieldName, fieldValue]) => { let cascadeChecked = oppModel.get(`${this._getParentFieldName(fieldName)}_cascade_checked`); if (app.utils.isTruthy(cascadeChecked) && app.utils.isRliFieldValidForCascade(bean, fieldName, attrs)) { attrs[fieldName] = fieldValue; } }); } if (!_.isEmpty(prepopulatedData) && !_.isUndefined(prepopulatedData.lock_duration)) { attrs.lock_duration = prepopulatedData.lock_duration; } if (!_.isEmpty(attrs)) { // we need to set the defaults bean.setDefault(attrs); // just to make sure that any attributes that were already set, are set again. bean.set(attrs); } // Fix the forecast field. If sales_stage and commit_stage are both cascaded at once, then commit_stage // gets recalculated, ignoring the cascade-set value. if (_.has(cascadeFields, 'commit_stage') && !_.isEmpty(cascadeFields.commit_stage)) { let forecastCascadeChecked = app.utils.isTruthy(oppModel.get('commit_stage_cascade_checked')); if (forecastCascadeChecked && app.utils.isRliFieldValidForCascade(bean, 'commit_stage')) { bean.setDefault('commit_stage', cascadeFields.commit_stage); bean.set('commit_stage', cascadeFields.commit_stage); } } return bean; }, /** * Gets the model of the parent Opp, if one exists * @return {null|*} * @private */ _getOppModel: function() { if (!this.context || !this.context.parent) { return null; } return this.context.parent.get('model'); }, /** * Gets the current values of the Opp level cascade fields * @param bean * @param attrs (optional) * @return {Object} */ getCascadeFieldsFromOpp: function(bean, attrs) { let oppModel = this._getOppModel(); if (!oppModel) { return {}; } let cascadeValues = {}; this.cascadableFields.forEach(fieldName => { let cascadeFieldName = fieldName + '_cascade'; if (app.utils.isRliFieldValidForCascade(bean, fieldName, attrs)) { cascadeValues[fieldName] = oppModel.get(cascadeFieldName) || ''; } else { cascadeValues[fieldName] = ''; } }); return cascadeValues; }, /** * Add listeners to the bean to ensure cascadable fields are set properly when prereq fields are changed. * @inheritdoc */ _addCustomEventHandlers: function(bean) { this.cascadePrereqFields.forEach(fieldName => { bean.on('change:' + fieldName, this.checkCascadePrereqChanges, this); }); this.cascadableFields.forEach(fieldName => { bean.on('change:' + fieldName, this.verifyChangeFromCascade, this); }); }, /** * Handles changes to fields that cascadable fields depend on. When a prereq field changes, update any * fields that depend on it * @param model */ checkCascadePrereqChanges: function(model) { let oppModel = this._getOppModel(); if (!oppModel) { return; } let cascadeFields = this.getCascadeFieldsFromOpp(model); Object.entries(cascadeFields).forEach(([fieldName, fieldValue]) => { let cascadeChecked = oppModel.get(this._getParentFieldName(fieldName) + '_cascade_checked'); if (app.utils.isTruthy(cascadeChecked) && app.utils.isRliFieldValidForCascade(model, fieldName)) { model.set(fieldName, fieldValue); } }); this._checkCascadeFieldEditability(); }, /** * When one of the cascadable fields is modified, make sure that it inherits the cascade value * if applicable. This is to ensure the cascading works properly with SetValue actions. * @param model */ verifyChangeFromCascade: function(model) { let oppModel = this._getOppModel(); if (!oppModel) { return; } this.cascadableFields.forEach(fieldName => { let cascadeChecked = oppModel.get(this._getParentFieldName(fieldName) + '_cascade_checked'); if (app.utils.isTruthy(cascadeChecked) && app.utils.isRliFieldValidForCascade(model, fieldName)) { model.set(fieldName, oppModel.get(fieldName + '_cascade')); } }); }, /** * Gets the name of the parent field. For most fields this will be the field name, but for a subfield * in a fieldset, it is the name of the fieldset. * @param fieldName * @return {string} * @private */ _getParentFieldName: function(fieldName) { if (['service_duration_unit', 'service_duration_value'].includes(fieldName)) { return 'service_duration'; } return fieldName; }, /** * @inheritdoc */ render: function() { this._super('render'); this._checkCascadeFieldEditability(); }, /** * Check the parent Opp model for if we need to enable or disable cascadable fields * @private */ _checkCascadeFieldEditability: function() { let oppModel = this._getOppModel(); if (!oppModel) { return; } this.cascadableViewFields.forEach(fieldName => { if (app.utils.isTruthy(oppModel.get(fieldName + '_cascade_checked'))) { oppModel.trigger('cascade:checked:' + fieldName, true); } }); }, /** * We have to overwrite this method completely, since there is currently no way to completely disable * a field from being displayed * * @returns {{default: Array, available: Array, visible: Array, options: Array}} */ parseFields : function() { var catalog = this._super('parseFields'); var forecastConfig = app.metadata.getModule('Forecasts', 'config'); // if forecast is not setup, we need to make sure that we hide the commit_stage field _.each(catalog, function (group, i) { var filterMethod = _.isArray(group) ? 'filter' : 'pick'; if (forecastConfig && forecastConfig.is_setup) { catalog[i] = _[filterMethod](group, function(fieldMeta) { if (fieldMeta.name.indexOf('_case') != -1) { var field = 'show_worksheet_' + fieldMeta.name.replace('_case', ''); return (forecastConfig[field] == 1); } return true; }); } else { catalog[i] = _[filterMethod](group, function(fieldMeta) { return (fieldMeta.name != 'commit_stage'); }); } }); return catalog; } }) }, "subpanel-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Custom Subpanel Layout for Revenue Line Items. * * @class View.Views.Base.RevenueLineItems.SubpanelListView * @alias SUGAR.App.view.views.BaseRevenueLineItemsSubpanelListView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-list View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); //Extend the prototype's events object to setup additional events for this controller this.events = _.extend({}, this.events, { 'click [name=inline-cancel]': 'cancelClicked' }); }, /** * We have to overwrite this method completely, since there is currently no way to completely disable * a field from being displayed * * @returns {{default: Array, available: Array, visible: Array, options: Array}} */ parseFields : function() { var catalog = this._super('parseFields'), config = app.metadata.getModule('Forecasts', 'config'), isForecastSetup = (config && config.is_setup); // if forecast is not setup, we need to make sure that we hide the commit_stage field _.each(catalog, function (group, i) { var filterMethod = _.isArray(group) ? 'filter' : 'pick'; if (isForecastSetup) { catalog[i] = _[filterMethod](group, function(fieldMeta) { if (fieldMeta.name.indexOf('_case') != -1) { var field = 'show_worksheet_' + fieldMeta.name.replace('_case', ''); return (config[field] == 1); } return true; }); } else { catalog[i] = _[filterMethod](group, function(fieldMeta) { return (fieldMeta.name != 'commit_stage'); }); } }); return catalog; }, /** * @inheritdoc * * Tracks the last row where the view was changed to non-edit */ toggleRow: function(modelId, isEdit) { this._super('toggleRow', [modelId, isEdit]); if (!isEdit) { this.lastToggledModel = this.collection.get(modelId); } }, /** * Adds a reverting of model attributes when cancelling an edit view of * a row. This fixes issues with service fields not properly clearing when * cancelling the edit */ cancelClicked: function() { if (this.lastToggledModel) { this.lastToggledModel.revertAttributes(); } this.resize(); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['HistoricalSummary', 'CommittedDeleteWarning', 'MassQuote']); this._super('initialize', [options]); app.utils.hideForecastCommitStageField(this.meta.panels); }, /** * @inheritdoc */ cancelClicked: function() { /** * todo: this is a sad way to work around some problems with sugarlogic and revertAttributes * but it makes things work now. Probability listens for Sales Stage to change and then by * SugarLogic, it updates probability when sales_stage changes. When the user clicks cancel, * it goes to revertAttributes() which sets the model back how it was, but when you try to * navigate away, it picks up those new changes as unsaved changes to your model, and tries to * falsely warn the user. This sets the model back to those changed attributes (causing them to * show up in this.model.changed) then calls the parent cancelClicked function which does the * exact same thing, but that time, since the model was already set, it doesn't see anything in * this.model.changed, so it doesn't warn the user. */ var changedAttributes = this.model.changedAttributes(this.model.getSynced()); this.model.set(changedAttributes, {revert: true, hideDbvWarning: true}); this._super('cancelClicked'); }, /** * extend save options * @param {Object} options save options. * @return {Object} modified success param. */ getCustomSaveOptions: function(options) { // make copy of original function we are extending var origSuccess = options.success; // return extended success function with added alert return { success: _.bind(function() { if (_.isFunction(origSuccess)) { origSuccess(); } if (!_.isEmpty(this.model.get('quote_id'))) { app.alert.show('save_rli_quote_notice', { level: 'info', messages: app.lang.get( 'SAVE_RLI_QUOTE_NOTICE', 'RevenueLineItems' ), autoClose: true }); } }, this) }; }, /** * Bind to model to make it so that it will re-render once it has loaded. */ bindDataChange: function() { this.model.on('duplicate:before', this._handleDuplicateBefore, this); this._super('bindDataChange'); }, /** * Handle what should happen before a duplicate is created * * @param {Backbone.Model} new_model * @private */ _handleDuplicateBefore: function(new_model) { new_model.unset('quote_id'); new_model.unset('quote_name'); } }) }, "filter-rows": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-rows View (base) extendsFrom: 'FilterRowsView', /** * @inheritdoc */ loadFilterFields: function(moduleName) { this._super('loadFilterFields', [moduleName]); var cfg = app.metadata.getModule("Forecasts", "config"); if (cfg && cfg.is_setup === 1) { _.each(this.filterFields, function(field, key, list) { if (key.indexOf('_case') != -1) { var fieldName = 'show_worksheet_' + key.replace('_case', ''); if (cfg[fieldName] !== 1) { delete list[key]; delete this.fieldList[key]; } } }, this); } else { delete this.fieldList['commit_stage']; delete this.filterFields['commit_stage']; } } }) }, "activity-card-detail": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Opportunities.ActivityCardDetailView * @alias SUGAR.App.view.views.BaseOpportunitiesActivityCardDetailView * @extends View.Views.Base.ActivityCardDetailView */ ({ // Activity-card-detail View (base) /** * @inheritdoc */ formatDate: function(date) { return date.formatUser(true); }, }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.RevenueLineItems.CreateView * @alias SUGAR.App.view.views.RevenueLineItemsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', initialize: function(options) { this._super('initialize', [options]); app.utils.hideForecastCommitStageField(this.meta.panels); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "panel-top": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Panel-top View (base) extendsFrom: 'PanelTopView', /** * @inheritdoc */ initialize: function(options) { var userACLs; this._super('initialize', [options]); if (['Accounts', 'Documents'].includes(this.parentModule)) { this.context.parent.on('editablelist:save', this._reloadOpportunities, this); this.on('linked-model:create', this._reloadOpportunities, this); } if (this.parentModule === 'Accounts') { this.meta.buttons = _.filter(this.meta.buttons, function(item) { return item.type !== 'actiondropdown'; }); } userACLs = app.user.getAcls(); if (!(_.has(userACLs.Opportunities, 'edit') || _.has(userACLs.RevenueLineItems, 'access') || _.has(userACLs.RevenueLineItems, 'edit'))) { // need to trigger on app.controller.context because of contexts changing between // the PCDashlet, and Opps create being in a Drawer, or as its own standalone page // app.controller.context is the only consistent context to use var viewDetails = this.closestComponent('record') ? this.closestComponent('record') : this.closestComponent('create'); // only allow PCDashlet and QuickPicks to add RLIs if this is the Opps or RLI // page and the link is revenuelineitems if (!_.isUndefined(viewDetails) && (this.module === 'Opportunities' || this.module === 'RevenueLineItems') && this.context.get('link') === 'revenuelineitems') { app.controller.context.on(viewDetails.cid + ':productCatalogDashlet:add', this.openRLICreate, this); } } }, /** * Refreshes the RevenueLineItems subpanel when a new Opportunity is added * @private */ _reloadOpportunities: function() { var $oppsSubpanel = $('div[data-subpanel-link="opportunities"]'); // only reload Opportunities if it is closed & no data exists if ($('li.subpanel', $oppsSubpanel).hasClass('closed')) { if ($('table.dataTable', $oppsSubpanel).length) { this.context.parent.trigger('subpanel:reload', {links: ['opportunities']}); } else { this.context.parent.trigger('subpanel:reload'); } } else { this.context.parent.trigger('subpanel:reload', {links: ['opportunities']}); } }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); this.context.parent.on('subpanel:reload', function(args) { if (!_.isUndefined(args) && _.isArray(args.links) && _.contains(args.links, this.context.get('link'))) { // have to set skipFetch to false so panel.js will toggle this panel open this.context.set('skipFetch', false); this.context.reloadData({recursive: false}); } }, this); }, /** * @inheritdoc */ createRelatedClicked: function(event) { // close RLI warning alert app.alert.dismiss('opp-rli-create'); this._super('createRelatedClicked', [event]); }, /** * Open a new Drawer with the RLI Create Form */ openRLICreate: function(data) { var routerFrags = app.router.getFragment().split('/'); var parentModel; var model; var userCurrency; var createInPreferred; var currencyFields; var currencyFromRate; if (routerFrags[1] === 'create' || app.drawer.count()) { // if panel-top has been initialized on a record, but we're currently in create, ignore the event // or if there is already an Opps drawer opened return; } userCurrency = app.user.getCurrency(); createInPreferred = userCurrency.currency_create_in_preferred; if (data.product_template_id) { var metadataFields = app.metadata.getModule('Products', 'fields'); // getting the fields from metadata of the module and mapping them to data if (metadataFields && metadataFields.product_template_name && metadataFields.product_template_name.populate_list) { _.each(metadataFields.product_template_name.populate_list, function(val, key) { data[val] = data[key]; }, this); } } parentModel = this.context.parent.get('model'); model = this.createLinkModel(parentModel, 'revenuelineitems'); data.likely_case = data.discount_price; data.best_case = data.discount_price; data.worst_case = data.discount_price; data.assigned_user_id = app.user.get('id'); data.assigned_user_name = app.user.get('name'); // Update price on Flexible Duration Service data.catalog_service_duration_value = data.service_duration_value; data.catalog_service_duration_unit = data.service_duration_unit; if (createInPreferred) { currencyFields = _.filter(model.fields, function(field) { return field.type === 'currency'; }); currencyFromRate = data.base_rate; data.currency_id = userCurrency.currency_id; data.base_rate = userCurrency.currency_rate; _.each(currencyFields, function(field) { // if the field exists on the model, convert the value to the new rate if (data[field.name] && field.name.indexOf('_usdollar') === -1) { data[field.name] = app.currency.convertWithRate( data[field.name], currencyFromRate, userCurrency.currency_rate ); } }, this); } model.set(data); model.ignoreUserPrefCurrency = true; app.drawer.open({ layout: 'create', context: { create: true, module: 'RevenueLineItems', model: model } }, _.bind(this.rliCreateClose, this)); }, /** * Callback for when the create drawer closes * * @param {Data.Bean} model */ rliCreateClose: function(model) { var rliCtx; var ctx; if (!model) { return; } ctx = this.context; ctx.resetLoadFlag(); ctx.set('skipFetch', false); ctx.loadData(); // find the child collection for the RLI subpanel // if we find one and it has the loadData method, call that method to // force the subpanel to load the data. rliCtx = _.find(ctx.children, function(child) { return child.get('module') === 'RevenueLineItems'; }, this); if (!_.isUndefined(rliCtx) && _.isFunction(rliCtx.loadData)) { rliCtx.loadData(); } }, /** * @inheritdoc */ _dispose: function() { if (app.controller && app.controller.context) { var viewDetails = this.closestComponent('record') ? this.closestComponent('record') : this.closestComponent('create'); if (!_.isUndefined(viewDetails) && (this.module === 'Opportunities' || this.module === 'RevenueLineItems') && this.context.get('link') === 'revenuelineitems') { app.controller.context.off(viewDetails.cid + ':productCatalogDashlet:add', null, this); } } this._super('_dispose'); } }) }, "merge-duplicates": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Merge-duplicates View (base) extendsFrom: 'MergeDuplicatesView', /** * @inheritdoc */ _initializeMergeFields: function(module) { var config = app.metadata.getModule('Forecasts', 'config'); if (!config || !config.is_setup) { if(!_.contains(this.fieldNameBlacklist, 'commit_stage')) { this.fieldNameBlacklist.push('commit_stage'); } } else if (_.contains(this.fieldNameBlacklist, 'commit_stage')) { this.fieldNameBlacklist.splice(_.indexOf(this.fieldNameBlacklist, 'commit_stage'), 1); } this._super('_initializeMergeFields', [module]); }, /** * @inheritdoc */ bindDataChange: function() { this._super('bindDataChange'); var config = app.metadata.getModule('Forecasts', 'config'); if(config && config.is_setup && config.forecast_by === 'RevenueLineItems') { // make sure forecasts exists and is setup this.collection.on('change:sales_stage change:commit_stage reset', function(model) { var myModel = model; //check to see if this is a collection (for the reset event), use this.primaryRecord instead if true; if (!_.isUndefined(model.models)) { myModel = this.primaryRecord; } var salesStage = myModel.get('sales_stage'), commit_stage = this.getField('commit_stage'); if(salesStage) { if(_.contains(config.sales_stage_won, salesStage)) { // check if the sales_stage has changed to a Closed Won stage if(config.commit_stages_included.length) { // set the commit_stage to the first included stage myModel.set('commit_stage', _.first(config.commit_stages_included)); } else { // otherwise set the commit stage to just "include" myModel.set('commit_stage', 'include'); } commit_stage.setDisabled(true); this.$('input[data-record-id="' + myModel.get('id') + '"][name="copy_commit_stage"]').prop("checked", true); } else if(_.contains(config.sales_stage_lost, salesStage)) { // check if the sales_stage has changed to a Closed Lost stage // set the commit_stage to exclude myModel.set('commit_stage', 'exclude'); commit_stage.setDisabled(true); this.$('input[data-record-id="' + myModel.get('id') + '"][name="copy_commit_stage"]').prop("checked", true); } else { commit_stage.setDisabled(false); } } }, this); } } }) } }} , "layouts": {} , "datas": {} }, "DocuSignEnvelopes":{"fieldTemplates": { "base": { "resend-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocuSignEnvelopes.ResendActionField * @alias SUGAR.App.view.fields.BaseDocuSignEnvelopesResendActionField * @extends View.Fields.Base.RowactionField */ ({ // Resend-action FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [data-action=resend]': 'resend' }); }, /** * Resend */ resend: function() { if (this.model.get('status') !== 'sent') { app.alert.show('info-resend-envelope', { level: 'warning', messages: app.lang.get('LBL_ENVELOPE_NOT_SENT', 'DocuSignEnvelopes') }); return; } app.alert.show('resend_completed', { level: 'process', title: app.lang.get('LBL_LOADING') }); let options = { id: this.model.get('id') }; app.api.call('create', app.api.buildURL('DocuSign', 'resendEnvelope'), options, { success: function(res) { if (res.status == 'error') { app.alert.show('error-resend-envelope', { level: 'error', messages: res.message, autoClose: false }); return; } app.alert.show('success-resent-envelope', { level: 'success', messages: app.lang.get('LBL_ENVELOPE_SENT', 'DocuSignEnvelopes'), autoClose: true }); this.model.fetch(); }.bind(this), error: function(error) { app.alert.show('error-resend-envelope', { level: 'error', messages: error }); }, complete: function() { app.alert.dismiss('resend_completed'); } }); } }) }, "recipient-role": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocuSignEnvelopes.RecipientRoleField * @alias SUGAR.App.view.fields.BaseDocuSignEnvelopesRecipientRoleField * @extends View.Fields.Base.Field * @deprecated Use {@link View.Fields.Base.DocusignRecipientRoleField} instead. */ ({ // Recipient-role FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ initialize: function(options) { options = this.setupItems(options); this._super('initialize', [options]); }, /** * Setup dropdown items * * @param {Object} options * @return {Object} */ setupItems: function(options) { options.def.options = {'': ''}; const templateDetails = options.context.get('templateDetails'); let roles = templateDetails.roles; const firstRole = _.first(roles); if (roles.length > 0 && typeof firstRole.routing_order != 'undefined' && firstRole.routing_order != '') { roles = _.sortBy(roles, 'routing_order'); } _.each(roles, function setRoles(role) { options.def.options[role.name] = role.name; }); return options; } }) }, "fetch-envelope-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocuSignEnvelopes.FetchEnvelopeActionField * @alias SUGAR.App.view.fields.BaseDocuSignEnvelopesFetchEnvelopeActionField * @extends View.Fields.Base.RowactionField */ ({ // Fetch-envelope-action FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritDoc */ initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [data-action=fetchEnvelope]': 'fetchEnvelope' }); }, /** * Fetch envelope */ fetchEnvelope: function() { app.alert.show('fetch_envelope', { level: 'process', title: app.lang.get('LBL_LOADING') }); let options = { id: this.model.get('id') }; app.api.call('create', app.api.buildURL('DocuSign', 'updateEnvelope'), options, { success: function(res) { if (res && res.status && res.status === 'error') { app.alert.show('error-fetch-envelope', { level: 'error', messages: res.message, autoClose: false }); return; } app.alert.show('success-fetch-envelope', { level: 'success', messages: app.lang.get('LBL_DRAFT_CHANGED_SUCCESS', 'DocuSignEnvelopes'), autoClose: true }); this.model.fetch(); }.bind(this), error: function(error) { app.alert.show('error-fetch-envelope', { level: 'error', messages: error }); }, complete: function() { app.alert.dismiss('fetch_envelope'); } }); } }) }, "fetch-completed-action": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.DocuSignEnvelopes.FetchCompletedActionField * @alias SUGAR.App.view.fields.BaseDocuSignEnvelopesFetchCompletedActionField * @extends View.Fields.Base.RowactionField */ ({ // Fetch-completed-action FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.events = _.extend({}, this.events, { 'click [data-action=fetchCompleted]': 'fetchCompleted' }); }, /** * Fetch completed document */ fetchCompleted: function() { if (this.model.get('status') !== 'completed') { app.alert.show('info-fetch-document', { level: 'info', messages: app.lang.get('LBL_ENVELOPE_NOT_COMPLETED', 'DocuSignEnvelopes') }); return; } app.alert.show('fetch_completed', { level: 'process', title: app.lang.get('LBL_LOADING') }); let options = { id: this.model.get('id') }; app.api.call('create', app.api.buildURL('DocuSign', 'getCompletedDocument'), options, { success: function(res) { if (res && res.status && res.status === 'error') { app.alert.show('error-getting-completed-document', { level: 'error', messages: res.message }); return; } app.alert.show('success-fetch-document', { level: 'success', messages: app.lang.get('LBL_DOCUMENT_ADDED', 'DocuSignEnvelopes'), autoClose: true }); this.model.fetch(); }.bind(this), error: function(error) { app.alert.show('error-getting-completed-document', { level: 'error', messages: error }); }, complete: function() { app.alert.dismiss('fetch_completed'); } }); } }) } }} , "views": { "base": { "recipient-selection-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.RecipientSelectionHeaderpaneView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesRecipientSelectionHeaderpaneView * @extends View.Views.Base.DocusignRecipientSelectionHeaderpaneView */ ({ // Recipient-selection-headerpane View (base) extendsFrom: 'DocusignRecipientSelectionHeaderpaneView', /** * @inheritdoc */ initialize: function(options) { if (_.isUndefined(options.context.get('templateDetails'))) { options = this._removeBackButton(options); } this._super('initialize', [options]); }, /** * Remove back button * * @param {Object} options * @return {Object} */ _removeBackButton: function(options) { options.meta.buttons = _.filter(options.meta.buttons, function(button) { return button.name !== 'back_button'; }); return options; }, }) }, "envelope-setup": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.EnvelopeSetupView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesEnvelopeSetupView * @extends View.Views.Base.View */ ({ // Envelope-setup View (base) events: { 'change input[name=envelopeName]': 'envelopeNameChanged', }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); const payload = this.context.get('payload'); if (!_.isUndefined(payload) && !_.isUndefined(payload.template)) { this._envelopeName = payload.template.name; } else { this._envelopeName = ''; } this.context.set('_envelopeName', this._envelopeName); }, /** * Envelope name changed */ envelopeNameChanged: function() { this._envelopeName = this.$el.find('input[name=envelopeName]').val(); this.context.set('_envelopeName', this._envelopeName); } }) }, "templates-filter-quicksearch": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.TemplatesFilterQuicksearchView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesTemplatesFilterQuicksearchView * @extends View.Views.Base.View */ ({ // Templates-filter-quicksearch View (base) events: { 'keyup': 'throttledSearch', 'paste': 'throttledSearch', 'click .add-on.sicon-close': 'clearInput' }, /** * For customers with large datasets, allow customization to disable * the automatic filtering in the omnibar. * * @inheritdoc */ delegateEvents: function(events) { if (app.config.disableOmnibarTypeahead) { // Remove the keyup and paste events from this.events. // This is before the call to this._super('delegateEvents'), // so they have not been registered. delete this.events.keyup; delete this.events.paste; // On enter key press, apply the quicksearch. this.events.keydown = _.bind(function(evt) { // Enter key code is 13 if (evt.keyCode === 13) { this._applyQuickSearch(); } }, this); } this._super('delegateEvents', [events]); }, /** * Fires the quick search. * @param {Event} [event] A keyup event. */ throttledSearch: _.debounce(function(event) { this._applyQuickSearch(); }, 400), /** * Append or remove an icon to the quicksearch input so the user can clear the search easily * @param {boolean} addIt TRUE if you want to add it, FALSE to remove */ _toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.sicon-close.add-on')[0]) { this.$el.append('<i class="sicon sicon-close add-on"></i>'); } else if (!addIt) { this.$('.sicon-close.add-on').remove(); } }, /** * Clears out the filter search text for the layout */ clearFilter: function() { this.currentSearch = ''; this.$el.find('input').val(''); }, /** * Clear input */ clearInput: function() { this.$el.find('input').val(''); this._applyQuickSearch(true); }, /** * Invokes the `filter:apply` event with the current value on the * quicksearch field. * * @param {boolean} [force] `true` to always trigger the `filter:apply` * event, `false` otherwise. Defaults to `false`. */ _applyQuickSearch: function(force) { force = !_.isUndefined(force) ? force : false; var newSearch = this.$el.find('input').val(); if (force || this.currentSearch !== newSearch) { this.currentSearch = newSearch; this.context.trigger('filter:apply', newSearch); } //If the quicksearch field is not empty, append a remove icon so the user can clear the search easily this._toggleClearQuickSearchIcon(!_.isEmpty(newSearch)); } }) }, "admin-settings-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.AdminSettingsHeaderButtonsView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesAdminSettingsHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Admin-settings-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * Get save config url * * @return {string} */ _getSaveConfigURL: function() { return app.api.buildURL('DocuSign', 'setGlobalConfig'); }, /** * Get save config attributes * * @return {Object} */ _getSaveConfigAttributes: function() { const recipientSelection = this.model.get('recipientSelection'); return { recipientSelection: recipientSelection, }; }, /** * Save config */ _saveConfig: function() { app.api.call( 'create', this._getSaveConfigURL(), this._getSaveConfigAttributes(), { success: _.bind(function(settings) { if (_.isUndefined(app.config.docusign)) { app.config.docusign = {}; } app.config.docusign.recipientSelection = settings.recipientSelection; this.showSavedConfirmation(); app.router.navigate(this.module, {trigger: true}); }, this), error: _.bind(function() { this.getField('save_button').setDisabled(false); }, this) } ); }, }) }, "docusign-drafts-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.DocusignDraftsListView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesDocusignDraftsListView * @extends View.Views.Base.RecordlistView */ ({ // Docusign-drafts-list View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.filter(this.plugins, function(pluginName) { return pluginName !== 'ResizableColumns'; }); this._super('initialize', [options]); this.listenTo(this.context, 'click:draft:open', this.openDraft, this); }, /** * @inheritdoc */ _initializeMetadata: function() { return app.metadata.getView('DocuSignEnvelopes', 'docusign-drafts-list') || {}; }, /** * @inheritdoc */ _loadTemplate: function(options) { this.tplName = 'recordlist'; this.template = app.template.getView(this.tplName); }, /** * @inheritdoc */ _render: function() { this.leftColumns = []; this._super('_render'); }, /** * Handle open draft * * @param {Object} model */ openDraft: function(model) { this.context.trigger('list:draft:open', model); }, /** * @inheritdoc */ freezeFirstColumn: function(event) { event.stopPropagation(); let freeze = $(event.currentTarget).is(':checked'); this.isFirstColumnFreezed = freeze; app.user.lastState.set(this._thisListViewUserConfigsKey, {freezeFirstColumn: freeze}); let $firstColumns = this.$('table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'); if (freeze) { $firstColumns.addClass('sticky-column stick-first'); } else { $firstColumns.removeClass('sticky-column stick-first no-border'); } this.showFirstColumnBorder(); }, /** * @inheritdoc */ showFirstColumnBorder: function() { if (!this.isFirstColumnFreezed) { this.hasFirstColumnBorder = false; return; } let scrollPanel = this.$('.flex-list-view-content')[0]; let firstColumnSelector = 'table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'; if (scrollPanel.scrollLeft === 0) { this.$(firstColumnSelector).addClass('no-border'); this.hasFirstColumnBorder = false; } else if (!this.hasFirstColumnBorder) { this.$(firstColumnSelector).removeClass('no-border'); this.hasFirstColumnBorder = true; } } }) }, "list-pagination": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.ListPaginationView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesListPaginationView * @extends View.Views.Base.ListPaginationView */ ({ // List-pagination View (base) extendsFrom: 'ListPaginationView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._registerEvents(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'recipients:loaded', this.setCache, this); }, /** * @inheritdoc */ getPageCount: function() { if (!this.context.get('docusign_recipients')) { this._super('getPageCount'); return; } this.fetchCount(); }, /** * @inheritdoc */ fetchCount: function() { if (!this.context.get('docusign_recipients')) { this._super('fetchCount'); return; } const total = this.context.get('totalNumberOfRecipients'); this.collection.trigger('list:page-total:fetched', total); }, /** * @inheritdoc */ getPage: function(page) { if (!this.context.get('docusign_recipients')) { this._super('getPage', [page]); return; } this.page = page; if (this.restoreFromCache()) { // update count label this.context.trigger('list:paginate'); this.render(); } else { this.context.trigger('load:recipients'); } }, }) }, "template-selection-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.TemplateSelectionHeaderpaneView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesTemplateSelectionHeaderpaneView * @extends View.Views.Base.View */ ({ // Template-selection-headerpane View (base) /** * @inheritdoc */ _renderHtml: function() { this._super('_renderHtml'); this.layout.off('selection:closedrawer:fire'); this.layout.once( 'selection:closedrawer:fire', _.once( _.bind(function closeDrawer() { this.$el.off(); app.drawer.close({ closeEvent: true }); }, this) ) ); }, }) }, "templates-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.TemplatesListView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesTemplatesListView * @extends View.Views.Base.SelectionListView */ ({ // Templates-list View (base) extendsFrom: 'SelectionListView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._registerEvents(); this._initProperties(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'filter:apply', this.applyQuickSearch); this.listenTo(this.context, 'templates:loaded', this.render); }, /** * Init properties */ _initProperties: function() { this.rightColumns = []; }, }) }, "recipients-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.RecipientsListView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesRecipientsListView * @extends View.Views.Base.MultiSelectionListView */ ({ // Recipients-list View (base) extendsFrom: 'MultiSelectionListView', /** * @inheritdoc */ initialize: function(options) { const panelIdx = _.findIndex(options.meta.panels, function(panel) { return panel.name === 'recipients-list-panel'; }); if (options.context.get('templateDetails')) { options.meta.panels[panelIdx].fields = _.filter(options.meta.panels[panelIdx].fields, function(field) { return field.name !== 'type'; }); } else { options.meta.panels[panelIdx].fields = _.filter(options.meta.panels[panelIdx].fields, function(field) { return field.name !== 'role'; }); } this._super('initialize', [options]); this.rightColumns = []; this._initProperties(); }, /** * Init properties */ _initProperties: function() { this._fieldsInEditMode = []; }, /** * Checks the `[data-check=one]` element when the row is clicked. * * @param {Event} event The `click` event. */ triggerCheck: function(event) { const editableFieldTypes = ['base', 'enum', 'docusign-recipient-role']; const parentTd = event.target.closest('td'); if (!parentTd) { return; } const parentTdType = parentTd.dataset.type; if (!editableFieldTypes.includes(parentTdType)) { //revert everything to detail _.each(this._fieldsInEditMode, function(field) { field.setMode('list'); }); return; } const fieldUUID = this.$(parentTd).find('span').attr('sfuuid'); if (this.fields[fieldUUID].action === 'list') { this.fields[fieldUUID].setMode('edit'); this._fieldsInEditMode.push(this.fields[fieldUUID]); } }, /** * @inheritdoc */ _render: function() { this._super('_render'); this.createShortcutSession(); this.registerShortcuts(); }, /** * Create new shortcut session. */ createShortcutSession: function() { app.shortcuts.saveSession(); app.shortcuts.createSession([ 'Recipients:Inline:Cancel' ], this); }, /** * Register shortcuts */ registerShortcuts: function() { app.shortcuts.register({ id: 'Recipients:Inline:Cancel', keys: ['esc'], component: this, description: 'LBL_SHORTCUT_EDIT_RECIPIENT_CANCEL', callOnFocus: true, handler: _.bind(this._cancelKeyPressedHandler, this) }); }, /** * Cancel key pressed * * @param {Event} event */ _cancelKeyPressedHandler: function(event) { const parentTd = event.target.closest('td'); if (!parentTd) { return; } const fieldUUID = this.$(parentTd).find('span').attr('sfuuid'); if (this.fields[fieldUUID].action === 'edit') { this.fields[fieldUUID].setMode('list'); this._fieldsInEditMode = _.filter(this._fieldsInEditMode, function(field) { return field.sfId !== fieldUUID; }); } }, /** * @inheritdoc */ _validateSelection: function() { let selectedModels = this.context.get('mass_collection'); let recipientWithoutRole = _.find(selectedModels.models, function(recipient) { return !_.isString(recipient.get('role')) && !_.isString(recipient.get('type')); }); if (!_.isUndefined(recipientWithoutRole)) { app.DocuSign.utils._showRolesNotSetAlert(); return; } if (selectedModels.length > this.maxSelectedRecords) { this._showMaxSelectedRecordsAlert(); return; } app.drawer.close(this._getCollectionAttributes(selectedModels)); }, /** * @inheritdoc */ _dispose: function() { app.shortcuts.restoreSession(); this._super('_dispose'); } }) }, "docusign-envelopes-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.DocusignEnvelopesListView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesDocusignEnvelopesListView * @extends View.Views.Base.RecordlistView */ ({ // Docusign-envelopes-list View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.filter(this.plugins, function(pluginName) { return pluginName !== 'ResizableColumns'; }); this._super('initialize', [options]); this.listenTo(this.context, 'list:envelope:download', this.handleDownloadClick, this); }, /** * @inheritdoc */ _initializeMetadata: function() { return app.metadata.getView('DocuSignEnvelopes', 'docusign-envelopes-list') || {}; }, /** * @inheritdoc */ _loadTemplate: function(options) { this.tplName = 'recordlist'; this.template = app.template.getView(this.tplName); }, /** * @override */ _render: function() { this.leftColumns = []; this._super('_render'); }, /** * Handle download click * * @param {Object} model */ handleDownloadClick: function(model) { if ( !_.isEmpty(model.get('created_by_link')) && model.get('created_by_link').id === app.user.id ) { app.alert.show('download_documents', { level: 'process', title: app.lang.get('LBL_LOADING') }); app.api.call('create', app.api.buildURL('DocuSign/downloadDocument'), { sugarEnvelopeId: model.get('id') }, { success: function(data) { if (data.status && data.status === 'error') { app.alert.show('error-downloading-document', { level: 'error', messages: data.message }); return; } var url = app.api.buildURL('DocuSign/downloadDocument?sugarEnvelopeId=' + model.get('id') + '&fileUid=' + data.fileUid); app.api.fileDownload(url, {}, {iframe: this.$el}); }, error: function(error) { app.alert.show('error-downloading-document', { level: 'error', messages: error.message || error }); }, complete: function() { app.alert.dismiss('download_documents'); } }); } else { app.alert.show('warn-docusign-create-user', { level: 'warning', messages: app.lang.get('LBL_DOWNLOAD_NOT_ALLOWED', 'DocuSignEnvelopes'), autoClose: true, autoCloseDelay: '10000' }); } }, /** * @inheritdoc */ freezeFirstColumn: function(event) { event.stopPropagation(); let freeze = $(event.currentTarget).is(':checked'); this.isFirstColumnFreezed = freeze; app.user.lastState.set(this._thisListViewUserConfigsKey, {freezeFirstColumn: freeze}); let $firstColumns = this.$('table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'); if (freeze) { $firstColumns.addClass('sticky-column stick-first'); } else { $firstColumns.removeClass('sticky-column stick-first no-border'); } this.showFirstColumnBorder(); }, /** * @inheritdoc */ showFirstColumnBorder: function() { if (!this.isFirstColumnFreezed) { this.hasFirstColumnBorder = false; return; } let scrollPanel = this.$('.flex-list-view-content')[0]; let firstColumnSelector = 'table tbody tr td:nth-child(1), table thead tr th:nth-child(1)'; if (scrollPanel.scrollLeft === 0) { this.$(firstColumnSelector).addClass('no-border'); this.hasFirstColumnBorder = false; } else if (!this.hasFirstColumnBorder) { this.$(firstColumnSelector).removeClass('no-border'); this.hasFirstColumnBorder = true; } } }) }, "envelope-setup-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.EnvelopeSetupHeaderpaneView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesEnvelopeSetupHeaderpaneView * @extends View.Views.Base.View */ ({ // Envelope-setup-headerpane View (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._registerEvents(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.layout, 'setup:closedrawer:fire', this.closeDrawer, this); this.listenTo(this.layout, 'setup:back:fire', this.navigateBack, this); this.listenTo(this.layout, 'setup:send:fire', this.send, this); }, /** * Close drawer */ closeDrawer: function() { this.$el.off(); app.drawer.close({ closeEvent: true }); }, /** * Navigate to previous drawer */ navigateBack: function() { this.$el.off(); this._sendingPayload = this.context.get('payload'); let closeOptions = { closeEvent: true, }; if (app.DocuSign.utils.shouldShowRecipients() || !_.isUndefined(this._sendingPayload.templateSelected)) { closeOptions.dsProcessWillContinue = true; } app.drawer.close(closeOptions); if (app.DocuSign.utils.shouldShowRecipients()) { if (this._sendingPayload.composite) { _.debounce(_.bind(this._openCompositeRecipientsDrawer, this), 1000)(); } else { _.debounce(_.bind(this._openRecipientsDrawer, this), 1000)(); } } else if (!_.isUndefined(this._sendingPayload.templateSelected)) { if (this._sendingPayload.composite) { _.debounce(_.bind(this._openCompositeTemplatesListDrawer, this), 1000)(); } else { _.debounce(_.bind(this._openTemplatesListDrawer, this), 1000)(); } } }, /** * Continue sending the envelope */ send: function() { const validationResult = this._validateEnvelopeSetup(); if (_.isString(validationResult)) { app.alert.show('error-envelope-setup', { level: 'error', messages: validationResult, autoClose: true, }); return; } this.$el.off(); app.drawer.close({ envelopeName: this.context.get('_envelopeName'), }); }, /** * Open recipients drawer */ _openRecipientsDrawer: function() { app.events.trigger('docusign:send:initiate', this._sendingPayload); }, /** * Open composite recipients drawer */ _openCompositeRecipientsDrawer: function() { app.events.trigger('docusign:compositeSend:initiate', this._sendingPayload); }, /** * Open templates list drawer */ _openTemplatesListDrawer: function() { app.events.trigger('docusign:send:initiate', this._sendingPayload, 'selectTemplate'); }, /** * Open composite templates list drawer */ _openCompositeTemplatesListDrawer: function() { app.events.trigger('docusign:compositeSend:initiate', this._sendingPayload, 'selectTemplate'); }, /** * Validate envelope setup * * @return {mixed} */ _validateEnvelopeSetup: function() { const envelopeName = this.context.get('_envelopeName'); if (_.isEmpty(envelopeName)) { return app.lang.get('LBL_ENVELOPE_NAME_EMPTY', 'DocuSignEnvelopes'); } return true; }, }) }, "admin-settings": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.DocuSignEnvelopes.AdminSettingsView * @alias SUGAR.App.view.views.BaseDocuSignEnvelopesAdminSettingsView * @extends View.Views.Base.View */ ({ // Admin-settings View (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); var url = app.api.buildURL('DocuSign', 'getGlobalConfig'); app.api.call('read', url, {}, { success: function successCb(settings) { this.model.set('recipientSelection', settings.recipientSelection); this.render(); }.bind(this), error: function() { app.log.error('Could not make the call to getGlobalConfig'); } }); }, }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": { "base": { "templates-filter": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DocuSignEnvelopes.FilterLayout * @alias SUGAR.App.view.layouts.BaseDocuSignEnvelopesFilterLayout * @extends View.Layouts.Filter */ ({ // Templates-filter Layout (base) extendsFrom: 'FilterLayout', initialFilter: 'all_records', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.name = 'filter'; }, }) }, "templates-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DocuSignEnvelopes.TemplatesListLayout * @alias SUGAR.App.view.layouts.BaseDocuSignEnvelopesTemplatesListLayout * @extends View.Layouts.Base.SelectionListLayout */ ({ // Templates-list Layout (base) extendsFrom: 'SelectionListLayout', /** * @inheritdoc */ loadData: function(options) { this._super('loadData', [options]); var apiData = { module: this.context.get('contextModule'), id: this.context.get('ctxModelId'), offset: this.collection.offset }; app.api.call('read', app.api.buildURL('DocuSign', 'listTemplates', {}, apiData), {}, { success: _.bind(this.successCallback, this), error: function() { app.alert.show('failed-to-fetch-templates', { level: 'error', messages: app.lang.get('LBL_FAILED_FETCH_TEMPLATES', 'DocuSignEnvelopes'), autoClose: true }); } }); }, /** * Success getting templates * * @param {Object} data */ successCallback: function(data) { if (this.disposed) { return; } this.collection.reset(); _.each(data.templates, function(templateData) { const model = app.data.createBean(undefined, { id: templateData.id, name: templateData.name, _module: 'DocuSignEnvelopes' }); this.collection.add(model); }, this); this.collection.dataFetched = true; this.collection.offset = data.nextOffset; this.render(); this.context.trigger('templates:loaded'); }, }) }, "recipients-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DocuSignEnvelopes.RecipientsListLayout * @alias SUGAR.App.view.layouts.BaseDocuSignEnvelopesRecipientsListLayout * @extends View.Layouts.Base.Layout */ ({ // Recipients-list Layout (base) extendsFrom: 'MultiSelectionListLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.context.set({ mass_collection: app.data.createBeanCollection(), //reset mass collection docusign_recipients: true }); this._registerEvents(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'load:recipients', this.loadData, this); }, /** * @inheritdoc */ loadData: function(options) { this._super('loadData', [options]); var apiData = { module: this.context.get('contextModule'), id: this.context.get('ctxModelId'), offset: this.collection.offset }; app.api.call('read', app.api.buildURL('DocuSign', 'getListOfPossibleRecipients', {}, apiData), {}, { success: _.bind(this.successCallback, this), error: function() { app.alert.show('failed-to-fetch-recipients', { level: 'error', messages: app.lang.get('LBL_FAILED_FETCH_RECIPIENTS', 'DocuSignEnvelopes'), autoClose: true }); } }); }, /** * Success getting recipients * * @param {Object} data */ successCallback: function(data) { if (this.disposed) { return; } this.collection.reset(); _.each(data.recipients, function(recipientData) { const model = app.data.createBean(undefined, { id: recipientData.id, name: recipientData.name, email: recipientData.email, module: recipientData.module, _module: recipientData._module, type: recipientData.type, }); this.collection.add(model); }, this); this.collection.dataFetched = true; this.collection.offset = data.nextOffset; this.context.set('totalNumberOfRecipients', data.totalNumberOfRecipients); this.render(); this.context.trigger('recipients:loaded'); }, }) }, "templates-list-composite": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.DocuSignEnvelopes.TemplatesListCompositeLayout * @alias SUGAR.App.view.layouts.BaseDocuSignEnvelopesTemplatesListCompositeLayout * @extends View.Layouts.Base.SelectionListLayout */ ({ // Templates-list-composite Layout (base) extendsFrom: 'SelectionListLayout', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._registerEvents(); }, /** * Register events */ _registerEvents: function() { this.listenTo(this.context, 'filter:apply', this._resetCollection); }, /** * @inheritdoc */ loadData: function(options) { this._super('loadData', [options]); var apiData = { module: this.context.get('contextModule'), id: this.context.get('ctxModelId'), offset: this.collection.offset }; app.api.call('read', app.api.buildURL('DocuSign', 'listTemplates', {}, apiData), {}, { success: _.bind(this.successCallback, this), error: function() { app.alert.show('failed-to-fetch-templates', { level: 'error', messages: app.lang.get('LBL_FAILED_FETCH_TEMPLATES', 'DocuSignEnvelopes'), autoClose: true }); } }); }, /** * Success getting templates * * @param {Object} data */ successCallback: function(data) { this.data = data; this._resetCollection(); this.render(); }, /** * Reset collection * * @param {string} newSearch */ _resetCollection: function(newSearch) { this.collection.reset(); this._buildCollection(newSearch); this.context.trigger('templates:loaded'); }, /** * Build collection * Filter by newSearch considering existing filter functionality. % can be used as placehoder * * @param {string} newSearch */ _buildCollection: function(newSearch) { if (_.isUndefined(newSearch)) { newSearch = ''; } const sugarPlaceholder = '%'; const regexPlaceholder = '.'; newSearch = newSearch.toLowerCase(); newSearch = newSearch.replaceAll(sugarPlaceholder, regexPlaceholder); newSearch = newSearch.replaceAll(' ', ''); if (!_.isEmpty(newSearch) && newSearch.substring(0, 1) !== regexPlaceholder) { newSearch = '^' + newSearch; } if (!_.isEmpty(newSearch) && newSearch.substring(newSearch.length - 1) !== regexPlaceholder) { newSearch = newSearch + '.'; } const regExForSearch = new RegExp(newSearch); _.each(this.data.templates, function(templateData) { let templateName = templateData.name.toLowerCase(); templateName = templateName.replaceAll(' ', ''); if (_.isNull(regExForSearch.exec(templateName))) { return; } const model = app.data.createBean(undefined, { id: templateData.id, name: templateData.name, _module: 'DocuSignEnvelopes' }); this.collection.add(model); }, this); this.collection.dataFetched = true; this.collection.offset = -1; }, }) } }} , "datas": {} }, "Geocode":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "PubSub_ModuleEvent_PushSubs":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Library":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "EmailAddresses":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": { "base": { "model": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class Model.Datas.Base.EmailAddressesModel * @alias SUGAR.App.model.datas.BaseEmailAddressesModel * @extends Data.Bean */ ({ // Model Data (base) /** * @inheritdoc * * Defaults `opt_out` to the `new_email_addresses_opted_out` config. */ initialize: function(attributes) { this._defaults = _.extend({}, this._defaults, {opt_out: app.config.newEmailAddressesOptedOut}); app.Bean.prototype.initialize.call(this, attributes); } }) } }} }, "Words":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "Sugar_Favorites":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "KBDocuments":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "KBContents":{"fieldTemplates": { "base": { "enum-config": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Enum-config FieldTemplate (base) extendsFrom: 'EnumField', /** * @inheritdoc */ initialize: function(opts) { this._super('initialize', [opts]); if (this.model.isNew() && this.view.action === 'detail') { this.def.readonly = false; } else { this.def.readonly = true; } }, /** * @inheritdoc */ loadEnumOptions: function(fetch, callback) { var module = this.def.module || this.module, optKey = this.def.key || this.name, config = app.metadata.getModule(module, 'config') || {}; this._setItems(config[optKey]); fetch = fetch || false; if (fetch || !this.items) { var url = app.api.buildURL(module, 'config', null, {}); app.api.call('read', url, null, { success: _.bind(function(data) { this._setItems(data[optKey]); callback.call(this); }, this) }); } }, /** * @inheritdoc */ _loadTemplate: function() { this.type = 'enum'; this._super('_loadTemplate'); this.type = this.def.type; }, /** * Sets current items. * @param {Array} values Values to set into items. */ _setItems: function(values) { var result = {}, def = null; _.each(values, function(val) { var tmp = _.omit(val, 'primary'); _.extend(result, tmp); if (val.primary) { def = _.first(_.keys(tmp)); } }); this.items = result; if (def && _.isUndefined(this.model.get(this.name))) { this.defaultOnUndefined = false; // call with {silent: true} on, so it won't re-render the field, since we haven't rendered the field yet this.model.set(this.name, def, {silent: true}); //Forecasting uses backbone model (not bean) for custom enums so we have to check here if (_.isFunction(this.model.setDefault)) { this.model.setDefault(this.name, def); } } }, /** * @inheritdoc * * Filters language items for different modes. * Disable edit mode for editing revision and for creating new revision. * Displays only available langs for creating localization. */ setMode: function(mode) { if (mode == 'edit') { if (this.model.has('id')) { this.setDisabled(true); } else if (this.model.has('related_languages')) { if (this.model.has('kbarticle_id')) { this.setDisabled(true); } else { _.each(this.model.get('related_languages'), function(lang) { delete this.items[lang]; }, this); this.model.set(this.name, _.first(_.keys(this.items)), {silent: true}); } } } this._super('setMode', [mode]); } }) }, "status": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Status FieldTemplate (base) /** * status Widget. * * Extends from EnumField widget adding style property according to specific * status. */ extendsFrom: 'BadgeSelectField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); /** * An object where its keys map to specific status and color to matching * CSS classes. */ this.statusClasses = { 'draft': 'label-pending', 'in-review': 'label-warning', 'approved': 'label-info', 'published': 'label-success', 'expired': 'label' }; this.type = 'badge-select'; }, /** * @inheritdoc */ format: function(value) { if (this.action === 'edit') { var def = this.def.default ? this.def.default : value; value = (this.items[value] ? value : false) || (this.items[def] ? def : false) || value; } return this._super('format', [value]); } }) }, "nestedset": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.KBContents.NestedsetField * @alias SUGAR.App.view.fields.BaseNestedsetField * @extends View.Fields.Base.BaseField */ ({ // Nestedset FieldTemplate (base) /** * @inheritdoc */ fieldTag: 'div', /** * Root ID of a shown Nestedset. * @property {String} */ categoryRoot: null, /** * Module which implements Nestedset. * @property {String} */ moduleRoot: null, /** * @inheritdoc */ extendsFrom: 'BaseField', /** * @inheritdoc */ plugins: ['JSTree', 'NestedSetCollection'], /** * Selector for tree's placeholder. * @property {String} */ ddEl: '[data-menu=dropdown]', /** * Flag indicates if input for new node shown. * @property {Boolean} */ inCreation: false, /** * Callback to handle global dropdown click event. * @property {Callback} */ dropdownCallback: null, /** * Variable to track if dropdown is opened */ isOpened: false, /** * @inheritdoc */ events: { 'click [data-role=treeinput]': 'openDropDown', 'click': 'handleClick', 'keydown [data-role=secondinput]': 'handleKeyDown', 'click [data-action=full-screen]': 'fullScreen', 'click [data-action=create-new]': 'switchCreate', 'keydown [data-role=add-item]': 'handleKeyDown', 'click [data-action=show-list]': 'showList', 'click [data-action=clear-field]': 'clearField' }, /** * @inheritdoc */ initialize: function(opts) { this._super('initialize', [opts]); var module = this.def.config_provider || this.context.get('module'), config = app.metadata.getModule(module, 'config'); this.categoryRoot = this.def.category_root || config.category_root || ''; this.moduleRoot = this.def.category_provider || this.def.data_provider || module; this.dropdownCallback = _.bind(this.handleGlobalClick, this); this.emptyLabel = app.lang.get( 'LBL_SEARCH_SELECT_MODULE', this.module, {module: app.lang.get(this.def.label, this.module)} ); this.before('render', function() { if (this.$(this.ddEl).length !== 0 && this._dropdownExists()) { this.closeDropDown(); } return true; }); }, /** * @inheritdoc */ _render: function() { var treeOptions = {}, $ddEl, self = this; this._super('_render'); $ddEl = this.$(this.ddEl); if ($ddEl.length !== 0 && this._dropdownExists()) { $ddEl.dropdown(); this.isOpened = false; $ddEl.off('click.bs.dropdown'); treeOptions = { settings: { category_root: this.categoryRoot, module_root: this.moduleRoot }, options: {} }; this._renderTree( this.$('[data-place=tree]'), treeOptions, { 'onSelect': _.bind(this.selectedNode, this), 'onLoad': function () { if (!self.disposed) { self.toggleSearchIcon(false); } } } ); this.toggleSearchIcon(true); this.toggleClearIcon(); } }, /** * Gets HTML placeholder for a field. * @return {String} HTML placeholder for the field as Handlebars safe string. */ getPlaceholder: function() { // if this in the filter row, the placeholder must have some css rules if (this.view && this.view.action === 'filter-rows') { return new Handlebars.SafeString('<span sfuuid="' + this.sfId + '" class="nestedset-filter-container"></span>'); } return this._super('getPlaceholder'); }, /** * Show dropdown. * @param {Event} evt Triggered mouse event. */ openDropDown: function(evt) { if (!this._dropdownExists()) { return; } if (this.isOpened === true) { return; } this.view.trigger('list:scrollLock', true); $('body').on('click.bs.dropdown.data-api', this.dropdownCallback); evt.stopPropagation(); evt.preventDefault(); _.defer(function(self) { var treePosition, $input; if (self.disposed) { return; } treePosition = self.$el.find('[data-role=treeinput]').position(); $input = self.$('[data-role=secondinput]'); self.$(self.ddEl).css({'left': treePosition.left - 1 + 'px', 'top': treePosition.top + 27 + 'px'}); self.$(self.ddEl).dropdown('toggle'); $input.val(''); self.isOpened = true; $input.focus(); }, this); }, /** * Close dropdown. * @return {Boolean} Return `true` if dropdown has been closed, `false` otherwise. */ closeDropDown: function() { if (!this.isOpened) { return false; } this.view.trigger('list:scrollLock', false); this.$(this.ddEl).dropdown('toggle'); if (this.inCreation) { this.switchCreate(); } this.isOpened = false; $('body').off('click.bs.dropdown.data-api', this.dropdownCallback); this.clearSelection(); return true; }, /** * Toggle icon in search field while loading tree. * @param {Boolean} hide Flag indicates would we show the icon. */ toggleSearchIcon: function(hide) { this.$('[data-role=secondinputaddon]') .toggleClass('sicon-search', !hide) .toggleClass('sicon-reset', hide) .toggleClass('sicon-is-spinning', hide); }, /** * Toggle clear icon in field. */ toggleClearIcon: function() { if (_.isEmpty(this.model.get(this.def.name))) { this.$el.find('[data-action=clear-field]').hide(); } else { this.$el.find('[data-action=clear-field]').show(); } }, /** * Handle global dropdown clicks. * @param evt {Event} Triggered mouse event. */ handleGlobalClick: function(evt) { if (this._dropdownExists()) { this.closeDropDown(); evt.preventDefault(); evt.stopPropagation(); } }, /** * Handle all clicks for the field. * Need to catch for preventing external events. * @param evt {Event} Triggered mouse event. */ handleClick: function(evt) { evt.preventDefault(); evt.stopPropagation(); }, /** * Search in the tree. */ searchTreeValue: function() { var val = this.$('[data-role=secondinput]').val(); this.searchNode(val); }, /** * @override `Editable` plugin event to prevent default behavior. */ bindKeyDown: function() {}, /** * @override `Editable` plugin event to prevent default behavior. */ bindDocumentMouseDown: function() {}, /** * @override `Editable` plugin event to prevent default behavior. */ focus: function() { if (this._dropdownExists()) { this.$('[data-role=treeinput]').click(); } }, /** * Handle key events in input fields. * @param evt {Event} Triggered key event. */ handleKeyDown: function(evt) { var role = $(evt.currentTarget).data('role'); if (evt.keyCode !== 13 && evt.keyCode !== 27) { return; } evt.preventDefault(); evt.stopPropagation(); switch (evt.keyCode) { case 13: switch (role) { case 'secondinput': this.searchTreeValue(evt); break; case 'add-item': this.addNew(evt); break; } break; case 27: switch (role) { case 'secondinput': this.closeDropDown(); break; case 'add-item': this.switchCreate(); break; } break; } }, /** * Set value of a model. * @param {String} id Related ID value. * @param {String} val Related value. */ setValue: function(id, val) { this.model.set(this.def.id_name, id); this.model.set(this.def.name, val); }, /** * @inheritdoc * * No data changes to bind. */ bindDomChange: function () { }, /** * @inheritdoc * * Set right value in DOM for the field. */ bindDataChange: function() { this.model.on("change:" + this.name, this.dataChangeUpdate, this); }, /** * Update field data. */ dataChangeUpdate: function() { if (this._dropdownExists()) { var id = this.model.get(this.def.id_name), name = this.model.get(this.def.name), child = this.collection.getChild(id); if (!name && child) { name = child.get(this.def.rname); } if (!name) { bean = app.data.createBean(this.moduleRoot, {id: id}); bean.fetch({ success: _.bind(function(data) { if (this.model) { this.model.set(this.def.name, data.get(this.def.rname)); } }, this) }); } this.$('[data-role="treevalue"]','[name=' + this.def.name + ']').text(name); this.$('[name=' + this.def.id_name + ']').val(id); } if (!this.disposed) { this.render(); } }, /** * @inheritdoc */ _dispose: function() { if (this._dropdownExists()) { $('body').off('click.bs.dropdown.data-api', this.dropdownCallback); } this._super('_dispose'); }, /** * Open drawer with tree list. */ fullScreen: function() { var treeOptions = { category_root: this.categoryRoot, module_root: this.moduleRoot, plugins: ['dnd', 'contextmenu'], isDrawer: true }, treeCallbacks = { 'onRemove': function(node) { if (this.context.parent) { this.context.parent.trigger('kbcontents:category:deleted', node); } }, 'onSelect': function(node) { if (!_.isEmpty(node) && !_.isEmpty(node.id) && !_.isEmpty(node.name)) { return true; } } }, // @TODO: Find out why params from context for drawer don't pass to our view tree::_initSettings context = _.extend({}, this.context, {treeoptions: treeOptions, treecallbacks: treeCallbacks}); app.drawer.open({ layout: 'nested-set-list', context: { module: 'Categories', parent: context, treeoptions: treeOptions, treecallbacks: treeCallbacks } }, _.bind(this.selectedNode, this)); }, /** * Open drawer with module records. */ showList: function() { var popDef = {}, filterOptions; popDef[this.def.id_name] = this.model.get(this.def.id_name); filterOptions = new app.utils.FilterOptions() .config(this.def) .setFilterPopulate(popDef) .format(); app.drawer.open({ layout: 'prefiltered', module: this.module, context: { module: this.module, filterOptions: filterOptions, } }); }, /** * Add new element to the tree. * @param {Event} evt Triggered key event. */ addNew: function(evt) { var name = $(evt.target).val().trim(); if (!name) { app.alert.show('wrong_node_name', { level: 'error', messages: app.error.getErrorString('empty_node_name', this), autoClose: true }); } else { this.addNode(name, 'last', true, false, true); this.switchCreate(); } }, /** * Create and hide input for new element. */ switchCreate: function() { var $options = this.$('[data-place=bottom-options]'), $create = this.$('[data-place=bottom-create]'), $input = this.$('[data-role=add-item]'), placeholder = app.lang.get('LBL_CREATE_CATEGORY_PLACEHOLDER', this.module); if (this.inCreation === false) { $options.hide(); $create.show(); $input .tooltip({ title: placeholder, container: 'body', trigger: 'manual', delay: {show: 200, hide: 100} }) .tooltip('show'); $input.focus().select(); } else { $input.tooltip('dispose'); $input.val(''); $create.hide(); $options.show(); } this.inCreation = !this.inCreation; }, /** * Clear input element. */ clearField: function(event) { event.preventDefault(); event.stopPropagation(); this.setValue('', ''); this.$('[data-role="treevalue"]','[name=' + this.def.name + ']').text(this.emptyLabel); this.$('[name=' + this.def.id_name + ']').val(); this.toggleClearIcon(); }, /** * Callback to handle selection of the tree. * @param data {Object} Data from selected node. */ selectedNode: function(data) { if (_.isEmpty(data) || _.isEmpty(data.id) || _.isEmpty(data.name)) { return; } var id = data.id, val = data.name; this.setValue(id, val); this.closeDropDown(); this.toggleClearIcon(); }, /** * Checks whether we need to work with dropdown on the view. * @private */ _dropdownExists: function() { return this.action === 'edit' || (this.meta && this.meta.view === 'edit'); }, /** * We don't need tooltip, because it breaks dropdown. * @inheritdoc */ decorateError: function(errors) { var $tooltip = $(this.exclamationMarkTemplate()), $ftag = this.$('span.select-arrow'); this.$el.closest('.record-cell').addClass('error'); this.$el.addClass('error'); $ftag.after($tooltip); this.$('[data-role=parent]').addClass('error'); }, /** * Need to remove own error decoration. * @inheritdoc */ clearErrorDecoration: function() { this.$el.closest('.record-cell').removeClass('error'); this.$el.removeClass('error'); this.$('[data-role=parent]').removeClass('error'); this.$('.add-on.error-tooltip').remove(); if (this.view && this.view.trigger) { this.view.trigger('field:error', this, false); } }, /** * @inheritdoc */ exclamationMarkTemplate: function() { var extraClass = this.view.tplName === 'record' ? 'top0' : 'top4'; return '<span class="error-tooltip ' + extraClass + ' add-on" data-contexclamationMarkTemplateainer="body">' + '<i class="sicon sicon-warning-circle"> </i>' + '</span>'; } }) }, "usefulness": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Usefulness FieldTemplate (base) events: { 'click [data-action=useful]': 'usefulClicked', 'click [data-action=notuseful]': 'notusefulClicked' }, /** * @inheritdoc * * This field doesn't support `showNoData`. */ showNoData: false, plugins: [], KEY_USEFUL: '1', KEY_NOT_USEFUL: '-1', voted: false, votedUseful: false, votedNotUseful: false, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if (!this.model.has('useful')) { this.model.set('useful', 0); } if (!this.model.has('notuseful')) { this.model.set('notuseful', 0); } this.checkVotes(); }, /** * Check votes state, * Set values for votedUseful, if user voted `useful` and * votedNotUseful if user voted `not useful`. */ checkVotes: function() { var vote = this.model.get('usefulness_user_vote'); this.votedUseful = (vote == this.KEY_USEFUL); this.votedNotUseful = (vote == this.KEY_NOT_USEFUL); }, /** * The vote for useful or not useful. * * @param {boolean} isUseful Flag of useful or not useful. */ vote: function(isUseful) { if ( (isUseful && this.model.get('usefulness_user_vote') == this.KEY_USEFUL) || (!isUseful && this.model.get('usefulness_user_vote') == this.KEY_NOT_USEFUL) ) { return; } var action = isUseful ? 'useful' : 'notuseful'; var url = app.api.buildURL(this.model.module, action, { id: this.model.id }); var callbacks = { success: _.bind(function(data) { this.model.set({ 'usefulness_user_vote': data.usefulness_user_vote, 'useful': data.useful, 'notuseful': data.notuseful, 'date_modified': data.date_modified }); if (!this.disposed) { this.render(); } }, this), error: function() {} }; app.api.call('update', url, null, callbacks); }, /** * Handler to vote useful when icon clicked. */ usefulClicked: function() { this.vote(true); }, /** * Handler to vote not useful when icon clicked. */ notusefulClicked: function() { this.vote(false); }, /** * @inheritdoc */ _render: function() { this.checkVotes(); this._super('_render'); return this; } }) }, "editablelistbutton": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.KBContents.EditablelistbuttonField * @alias SUGAR.App.view.fields.KBContentsEditablelistbuttonField * @extends View.Fields.Base.EditablelistbuttonField */ ({ // Editablelistbutton FieldTemplate (base) extendsFrom: 'EditablelistbuttonField', /** * @inheritdoc * * Add KBNotify plugin for field. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'KBNotify' ]); this._super('initialize', [options]); }, /** * Overriding custom save options to trigger kb:collection:updated event when KB model saved. * * @override * @param {Object} options */ getCustomSaveOptions: function(options) { var success = _.compose(options.success, _.bind(function(model) { this.notifyAll('kb:collection:updated', model); return model; }, this)); return {'success': success}; } }) }, "sticky-rowaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Fields.Base.KBContents.StickyRowactionField * @alias SUGAR.App.view.fields.BaseKBContentsStickyRowactionField * @extends View.Fields.Base.StickyRowactionField */ ({ // Sticky-rowaction FieldTemplate (base) extendsFrom: 'StickyRowactionField', /** * Disable field if it has no access to edit. * @inheritdoc */ isDisabled: function() { var parentLayout = this.context.parent.get('layout'); var parentModel = this.context.parent.get('model'); if ( this.def.name === 'create_button' && parentLayout === 'record' && !app.acl.hasAccessToModel('edit', parentModel) ) { return true; } return this._super('isDisabled'); } }) }, "rowaction": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Rowaction is a button that when selected will trigger a Backbone Event. * * @class View.Fields.KBContents.RowactionField * @alias SUGAR.App.view.fields.KBContentsRowactionField * @extends View.Fields.Base.RowactionField */ ({ // Rowaction FieldTemplate (base) extendsFrom: 'RowactionField', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); if ((this.options.def.name === 'create_localization_button' || this.options.def.name === 'create_revision_button') && !app.acl.hasAccessToModel('view', this.model)) { this.hide(); } } }) }, "languages": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Languages FieldTemplate (base) extendsFrom: 'FieldsetField', events: { 'click .btn[data-action=add-field]': 'addItem', 'click .btn[data-action=remove-field]': 'removeItem', 'click .btn[data-action=set-primary-field]': 'setPrimaryItem' }, intKey: null, deletedLanguages: [], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._currentIndex = 0; this.model.unset('deleted_languages', {silent: true}); }, /** * @inheritdoc */ format: function(value) { var result = [], numItems = 0; value = app.utils.deepCopy(value); if (_.isString(value)) { value = [{'': value, primary: false}]; } // Place the add button as needed if (_.isArray(value) && value.length > 0) { _.each(value, function(item, ind) { delete item.remove_button; delete item.add_button; result[ind] = { name: this.name, primary: item.primary || false }; delete item.primary; result[ind].items = item; }, this); if (!result[this._currentIndex]) { result[this._currentIndex] = {}; } result[value.length - 1].add_button = true; // number of valid teams numItems = _.filter(result, function(item) { return _.isUndefined(item.items['']); }).length; // Show remove button for all unset combos and only set combos if there are more than one _.each(result, function(item) { if (!_.isUndefined(item.items['']) || numItems > 1) { item.remove_button = true; } }); } return result; }, /** * @inheritdoc */ unformat: function(value) { var result = []; _.each(value, function(item) { result.push(_.extend({}, item.items, {primary: item.primary})); }, this); return result; }, /** * Set primary item. * @param {number} index * @return {boolean} */ setPrimary: function(index) { var value = this.unformat(this.value); _.each(value, function(item) { item.primary = false; }, this); value[index].primary = true; this.model.set(this.name, value); return (this.value[index]) ? this.value[index].primary : false; }, /** * @inheritdoc */ bindDomChange: function() { var self = this, el = null; if (this.model) { el = this.$el.find('div[data-name=languages_' + this.name + '] input[type=text]'); el.on('change', function() { var value = self.unformatValue(); self.model.set(self.name, value, {silent: true}); self.value = self.format(value); }); } }, /** * @inheritdoc */ bindDataChange: function() { if (this.model) { this.model.on('change', function() { if (this.disposed) { return; } this.render(); }, this); } }, /** * Get value from view data. * @return [{}] */ unformatValue: function() { var container = $(this.$('div[data-name=languages_' + this.name + ']')), input = container.find('input[type=text]'), value = [], val, k, v, pr, i; for (i = 0; i < input.length / 2; i = i + 1) { val = {}; k = container.find('input[data-index=' + i + '][name=key_' + this.name + ']').val(); v = container.find('input[data-index=' + i + '][name=value_' + this.name + ']').val(); pr = container.find('button[data-index=' + i + '][name=primary]').hasClass('active'); val[k] = v; val.primary = pr; value.push(val); } return value; }, /** * Add item to list. * @param {Event} evt DOM event. */ addItem: function(evt) { var index = $(evt.currentTarget).data('index'), value = this.unformat(this.value); if (!index || _.isUndefined(this.value[this.value.length - 1].items[''])) { value.push({'': ''}); this._currentIndex += 1; this.model.set(this.name, value); } }, /** * Remove item from list. * @param {Event} evt DOM event. */ removeItem: function(evt) { this._currentTarget = evt.currentTarget; this.warnDelete(); }, /** * Popup dialog message to confirm delete action. */ warnDelete: function() { app.alert.show('delete_confirmation', { level: 'confirmation', messages: app.lang.get('LBL_DELETE_CONFIRMATION_LANGUAGE', this.module), onConfirm: _.bind(this.confirmDelete, this), onCancel: _.bind(this.cancelDelete, this) }); }, /** * Predefined function for confirm delete. */ confirmDelete: function() { var index = $(this._currentTarget).data('index'), value = null, removed = null; if (_.isNumber(index)) { if (index === 0 && this.value.length === 1) { return; } if (this._currentIndex === this.value.length - 1) { this._currentIndex -= 1; } value = this.unformat(this.value); removed = value.splice(index, 1); if (removed && removed.length > 0 && removed[0].primary) { value[0].primary = this.setPrimary(0); } for (var key in removed[0]) { if (key !== 'primary' && 2 == key.length) { if (-1 === this.deletedLanguages.indexOf(key)) { this.deletedLanguages.push(key); } } } if (value) { this.model.set(this.name, value); } if (_.size(this.deletedLanguages) > 0) { this.model.set({'deleted_languages': this.deletedLanguages}, {silent: true}); } } }, /** * Predefined function for cancel delete. * @param {Event} evt DOM event. */ cancelDelete: function(evt) { }, /** * Set primary item. * @param {Event} evt DOM event. */ setPrimaryItem: function(evt) { var index = $(evt.currentTarget).data('index'); if (!this.value[index] || !_.isUndefined(this.value[index].items['']) || $(evt.currentTarget).hasClass('active')) { return; } this.$('.btn[name=primary]').removeClass('active'); if (this.setPrimary(index)) { this.$('.btn[name=primary][data-index=' + index + ']').addClass('active'); } }, /** * @inheritdoc */ _dispose: function() { this.$el.off(); this.model.off('change'); this._super('_dispose'); }, /** * Need own decoration for field error. * @override */ handleValidationError: function (errors) { this.clearErrorDecoration(); var err = errors.errors || errors; _.each(err, function(value) { var inpName = value.type + '_' + this.name, $inp = this.$('input[data-index=' + value.ind + '][name=' + inpName + ']'); $inp.wrap('<div class="input-append input error ' + this.name + '">'); errorMessages = [value.message]; $tooltip = $(this.exclamationMarkTemplate(errorMessages)); $inp.after($tooltip); }, this); }, /** * Need own method to clear error decoration. * @override */ clearErrorDecoration: function () { this.$('.add-on.error-tooltip').remove(); _.each(this.$('input[type=text]'), function(inp) { var $inp = this.$(inp); if ($inp.parent().hasClass('input-append') && $inp.parent().hasClass('error')) { $inp.unwrap(); } }); if (this.view && this.view.trigger) { this.view.trigger('field:error', this, false); } } }) }, "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Htmleditable_tinymce FieldTemplate (base) extendsFrom: 'Htmleditable_tinymceField', /** * Flag indicates, should we disable field. * @property {boolean} */ shouldDisable: null, /** * The defined iframe height, in pixels. If no height is defined, * use the default height for iframes (150px) */ defaultIframeHeight: 150, /** * KB specific parameters. * @private */ _tinyMCEConfig: { 'height': '300', }, /** * @inheritdoc * Additional override fieldSelector property from field's meta. */ initialize: function(opts) { if (opts.view.action === 'filter-rows') { opts.viewName = 'filter-rows-edit'; } this._super('initialize', [opts]); this.shouldDisable = false; this.resizeWindowHandler = _.debounce(_.bind(this.adjustBodyHeight, this), 100); window.addEventListener('resize', this.resizeWindowHandler); if (!_.isUndefined(this.def.fieldSelector)) { this.fieldSelector = '[data-htmleditable=' + this.def.fieldSelector + ']'; } this.before('render', function() { if (this.shouldDisable != this.isDisabled()) { this.setDisabled(this.shouldDisable); return false; } }, this); }, /** * @inheritdoc */ _render: function() { this._super('_render'); // non-editor view if (this.tplName === 'detail') { this.defaultIframeHeight = this._getHtmlEditableField().height(); } }, /** * Gets the iframe body element and updates its height based on window size */ adjustBodyHeight: function() { var $iframeElement = this._getHtmlEditableField(); // adjust the KB body height once the iframe is ready $iframeElement.ready(_.bind(this.updateBodyHeight, this, $iframeElement)); }, /** * @inheritdoc * * Apply inline css style to iframe elements in detail view. */ setViewContent: function(value) { if (!_.isEmpty(value)) { var elemArr = $.parseHTML(value) || []; if (elemArr.length > 0) { var firstElem = $(elemArr[0]); // This makes sure that the first element is aligned with the label firstElem.css('font-size', '14px'); firstElem.css('margin-top', '7.5px'); elemArr[0] = firstElem[0]; // clear the value string before assigning it modified value value = ''; value += '<div class="kbdocument-body">'; // iterate over each element _.each(elemArr, function(elem) { // append the outerHTML of each element to recreate value string // Text elements don't have 'outerHTML' property. Use 'textContent' instead value += elem.outerHTML || elem.textContent || ''; }); value += '</div>'; } } this._super('setViewContent', [value]); this.adjustBodyHeight(); }, /** * @inheritdoc * * Apply document css style to editor. */ getTinyMCEConfig: function() { var config = this._super('getTinyMCEConfig'), content_css = []; // To open a link in the same window we need to use _top instead of _self as target _.each(config.link_target_list, function(target) { if (target.text === app.lang.getAppString('LBL_TINYMCE_TARGET_SAME')) { target.value = '_top'; } }, this); config = _.extend(config, this._tinyMCEConfig); return config; }, /** * @inheritdoc * Need to strip tags for list and activity stream. */ format: function(value) { var result; switch (this.view.tplName) { case 'audit': case 'list': case 'activitystream': result = this.stripTags(value); break; default: result = this._super('format', [value]); break; } return result; }, /** * Strip HTML tags from text. * @param {string} value Value to strip tags from. * @return {string} Plain text. */ stripTags: function(value) { var $el = $('<div/>').html(value), texts = $el.contents() .map(function() { if (this.nodeType === 1 && this.nodeName != 'STYLE' && this.nodeName != 'SCRIPT') { return this.textContent.replace(/ +?\r?\n/g, ' ').trim(); } if (this.nodeType === 3) { return this.textContent.replace(/ +?\r?\n/g, ' ').trim(); } }); return _.filter(texts, function(value) { return (value.length > 0); }).join(' '); }, /** * @inheritdoc * Should check, if field should be disabled while mode change. */ setMode: function(mode) { this.shouldDisable = (mode === 'edit' && (this.view.tplName === 'list' || (this.view.tplName == 'flex-list' && (this.tplName == 'subpanel-list' || this.tplName == 'list')) ) ); this._super('setMode', [mode]); }, /** * We are trying to get HTML content instead of raw one because * when editor initialized it already contains some HTML (blank <p> or <br> tags). * In this case it will be considered as non-empty value for this field even if we don't enter anything. * It comes from ticket RS-1072. * * @override * @inheritdoc */ getEditorContent: function() { var text = this._htmleditor.getContent({format: 'html'}); //We don't need to get empty html, to prevent model changes. if (text !== '') { text = this._super('getEditorContent'); } return text; }, /** * @inheritdoc */ setViewName: function () { this.destroyTinyMCEEditor(); this._super('setViewName', arguments); }, /** * Update the height of the KB body, up to maxBodyHeight * * @param $element the jQuery element */ updateBodyHeight: function($element) { var windowHeight = $(window).height(); this.maxBodyHeight = 6 * windowHeight / 10; var contentHeight = this._getContentHeight($element); // do nothing if the content height is less than the default iframe height if (contentHeight < this.defaultIframeHeight) { return; } // add padding to account for bottom margins/padding contentHeight += 20; if (contentHeight < this.maxBodyHeight) { $element.height(contentHeight); } else { $element.height(this.maxBodyHeight); } }, /** * @inheritdoc * * Adds a button for selecting and applying a template. */ addCustomButtons: function(editor) { // if the user has access to KB Templates then add the template button if (app.acl.hasAccess('view', 'KBContentTemplates')) { editor.ui.registry.addButton('kbtemplate', { tooltip: app.lang.get('LBL_TEMPLATE', this.module), icon: 'document-properties', name: 'template', onAction: () => { this.context.trigger('button:add-template:click'); }, }); } }, /** * @inheritdoc */ _dispose: function() { window.removeEventListener('resize', this.resizeWindowHandler); this._super('_dispose'); } }) } }} , "views": { "base": { "prefiltered-headerpane": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.PrefilteredHeaderpaneView * @alias SUGAR.App.view.views.BasePrefilteredHeaderpaneView * @extends View.Views.Base.SelectionHeaderpaneView */ ({ // Prefiltered-headerpane View (base) extendsFrom: 'SelectionHeaderpaneView', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.meta.fields = _.map(this.meta.fields, function(field) { if (field.name === 'title') { field['formatted_value'] = this.context.get('headerPaneTitle') || this._formatTitle(field['default_value']) || app.lang.get(field['value'], this.module); this.title = field['formatted_value']; } return field; }, this); } }) }, "help-create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Help-create View (base) // TODO: Remove this View completely, when it is possible to place a standard help-dashlet to the Create layout /** * @inheritdoc */ _renderHtml: function () { var helpUrl = { more_info_url: this.createMoreHelpLink(), more_info_url_close: '</a>' }, helpObject = app.help.get(this.context.get('module'), 'create', helpUrl); this._super('_renderHtml', [helpObject, this.options]); }, /** * Collects server version, language, module, and route and returns an HTML link to be used * in the template * * @returns {string} The HTML a-tag for the More Help link */ createMoreHelpLink: function () { var serverInfo = app.metadata.getServerInfo(), lang = app.lang.getLanguage(), module = app.controller.context.get('module'), route = 'create'; var url = 'https://www.sugarcrm.com/crm/product_doc.php?edition=' + serverInfo.flavor + '&version=' + serverInfo.version + '&lang=' + lang + '&module=' + module + '&route=' + route; let products = app.user.getProductCodes(); url += products ? '&products=' + encodeURIComponent(products.join(',')) : ''; return '<a href="' + url + '" target="_blank">'; } }) }, "massupdate": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Massupdate View (base) extendsFrom: 'MassupdateView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['CommittedDeleteWarning', 'KBContent', 'KBNotify']); this._super('initialize', [options]); }, /** * @inheritdoc */ saveClicked: function(evt) { var massUpdateModels = this.getMassUpdateModel(this.module).models, fieldsToValidate = this._getFieldsToValidate(), emptyValues = []; this._restoreInitialState(massUpdateModels); this._doValidateMassUpdate(massUpdateModels, fieldsToValidate, _.bind(function(fields, errors) { if (_.isEmpty(errors)) { this.trigger('massupdate:validation:complete', { errors: errors, emptyValues: emptyValues }); if(this.$('.btn[name=update_button]').hasClass('disabled') === false) { this.listenTo(this.collection, 'data:sync:complete', _.bind(function() { this.notifyAll('kb:collection:updated'); this.stopListening(this.collection); }, this)); this.save(); } } else { this.handleValidationError(errors); } }, this)); }, /** * Restore models state. * * @param {Array} models * @private */ _restoreInitialState: function(models) { _.each(models, function(model) { model.revertAttributes(); }); }, /** * Custom MassUpdate validation. * * @param {Object} models * @param {Object} fields * @param {Function} callback * @private */ _doValidateMassUpdate: function(models, fields, callback) { var checkField = 'status', errorFields = [], messages = [], errors = {}, updatedValues = {}; _.each(fields, function(field) { updatedValues[field.name] = this.model.get(field.name); if (undefined !== field.id_name && this.model.has(field.id_name)) { updatedValues[field.id_name] = this.model.get(field.id_name); } }, this); _.each(models, function(model) { var values = _.extend({}, model.toJSON(), updatedValues), newModel = app.data.createBean(model.module, values); if (undefined !== updatedValues[checkField] && updatedValues[checkField] === 'approved') { this._doValidateActiveDateField(newModel, fields, errors, function(model, fields, errors) { var fieldName = 'active_date'; if (!_.isEmpty(errors[fieldName])) { errors[checkField] = errors[fieldName]; errorFields.push(fieldName); messages.push(app.lang.get('LBL_SPECIFY_PUBLISH_DATE', 'KBContents')); } }); } this._doValidateExpDateField(newModel, fields, errors, function(model, fields, errors) { var fieldName = 'exp_date'; if (!_.isEmpty(errors[fieldName])) { errors[checkField] = errors[fieldName]; errorFields.push(fieldName); messages.push(app.lang.get('LBL_MODIFY_EXP_DATE_LOW', 'KBContents')); } }); }, this); if (!_.isEmpty(errorFields)) { if (!_.isUndefined(errors.active_date) && errors.active_date.activeDateLow || !_.isUndefined(errors.exp_date) && errors.exp_date.expDateLow) { callback(fields, errors); return; } errorFields.push(checkField); app.alert.show('save_without_publish_date_confirmation', { level: 'confirmation', messages: _.uniq(messages), confirm: { label: app.lang.get('LBL_YES') }, cancel: { label: app.lang.get('LBL_NO') }, onConfirm: function() { errors = _.filter(errors, function(error, key) { _.indexOf(errorFields, key) === -1; }); callback(fields, errors); } }); } else { callback(fields, errors); } }, /** * We don't need to initialize KB listeners. * @override. * @private */ _initKBListeners: function() {}, /** * @inheritdoc */ cancelClicked: function(evt) { this._restoreInitialState(this.getMassUpdateModel(this.module).models); this._super('cancelClicked', [evt]); } }) }, "subpanel-for-localizations": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Subpanel-for-localizations View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ dataView: 'subpanel-for-localizations', /** * @inheritdoc * * Check access to model. * Setup dataView to load correct viewdefs from subpanel-for-localizations */ initialize: function(options) { this._super('initialize', [options]); if (!app.acl.hasAccessToModel('edit', this.model)) { this.context.set('requiredFilter', 'records-noedit'); } }, /** * @inheritdoc * * Removes 'status' field from options if there is no access to model. */ parseFieldMetadata: function(options) { options = this._super('parseFieldMetadata', [options]); if (app.acl.hasAccess('edit', options.module)) { return options; } _.each(options.meta.panels, function(panel, panelIdx) { panel.fields = _.filter(panel.fields, function(field) { return field.name !== 'status'; }, this); }, this); return options; } }) }, "config-languages": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.KBContentsConfigLanguagesView * @alias SUGAR.App.view.layouts.BaseKBContentsConfigLanguages * @extends View.Views.Base.ConfigPanelView */ ({ // Config-languages View (base) extendsFrom: 'ConfigPanelView', /** * @inheritdoc */ initialize: function (options) { this._super('initialize', [options]); var model = this.context.get('model'); model.fields = this.getFieldNames(); model.addValidationTask('validate_config_languages', _.bind(this._validateLanguages, this)); model.on('validation:success', _.bind(this._validationSuccess, this)); app.error.errorName2Keys['lang_empty_key'] = 'ERR_CONFIG_LANGUAGES_EMPTY_KEY'; app.error.errorName2Keys['lang_empty_value'] = 'ERR_CONFIG_LANGUAGES_EMPTY_VALUE'; app.error.errorName2Keys['lang_duplicate'] = 'ERR_CONFIG_LANGUAGES_DUPLICATE'; }, /** * Validate languages duplicates. * @param {Object} fields * @param {Object} errors * @param {Function} callback */ _validateLanguages: function (fields, errors, callback) { var model = this.context.get('model'), languages = this.model.get('languages'), languagesToSave = [], index = 0, languageErrors = []; _.each(languages, function(lang) { var lng = _.omit(lang, 'primary'), key = _.first(_.keys(lng)), val = lang[key].trim(); if (val.length === 0) { languageErrors.push({ 'message': app.error.getErrorString('lang_empty_value', this), 'key': key, 'ind': index, 'type': 'value' }); } index = index + 1; languagesToSave.push(key.trim().toLowerCase()); }, this); if ((index = _.indexOf(languagesToSave, '')) !== -1) { languageErrors.push({ 'message': app.error.getErrorString('lang_empty_key', this), 'key': '', 'ind': index, 'type': 'key' }); } if (languagesToSave.length !== _.uniq(languagesToSave).length) { var tmp = languagesToSave.slice(0); tmp.sort(); for (var i = 0; i < tmp.length - 1; i++) { if (tmp[i + 1] == tmp[i]) { languageErrors.push({ 'message': app.error.getErrorString('lang_duplicate', this), 'key': tmp[i], 'ind': _.indexOf(languagesToSave, tmp[i]), 'type': 'key' }); } } } if (languageErrors.length > 0) { errors.languages = errors.languages || {}; errors.languages.errors = languageErrors; app.alert.show('languages', { level: 'error', autoClose: true, messages: app.lang.get('ERR_RESOLVE_ERRORS') }); } callback(null, fields, errors); }, /** * On success validation, trim language keys and labels */ _validationSuccess: function () { var model = this.context.get('model'), languages = this.model.get('languages'); // trim keys var buf = _.map(languages, function(lang) { var prim = lang['primary'], lng = _.omit(lang, 'primary'), key = _.first(_.keys(lng)), val = lang[key].trim(); key = key.trim(); var res = {primary: prim}; res[key] = val; return res; }, this); model.set('languages', buf); } }) }, "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', /** * @inheritdoc * * Add KBContent plugin for view. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'KBContent', 'KBNotify' ]); this._super('initialize', [options]); this.layout.on('list:record:deleted', function() { this.refreshCollection(); this.notifyAll('kb:collection:updated'); }, this); this.context.on('kbcontents:category:deleted', function(node) { this.refreshCollection(); this.notifyAll('kb:collection:updated'); }, this); if (!app.acl.hasAccessToModel('edit', this.model)) { this.context.set('requiredFilter', 'records-noedit'); } } }) }, "filter-module-dropdown": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-module-dropdown View (base) extendsFrom: 'FilterModuleDropdownView', /** * @inheritdoc */ getModuleListForSubpanels: function() { var filters = []; filters.push({id: 'all_modules', text: app.lang.get('LBL_MODULE_ALL')}); var subpanels = this.pullSubpanelRelationships(), subpanelsAcls = this._getSubpanelsAclsActions(); subpanels = this._pruneHiddenModules(subpanels); _.each(subpanels, function(value, key) { var module = app.data.getRelatedModule(this.module, value), aclToCheck = !_.isUndefined(subpanelsAcls[value]) ? subpanelsAcls[value] : 'list'; if (app.acl.hasAccess(aclToCheck, module)) { filters.push({id: value, text: app.lang.get(key, this.module)}); } }, this); return filters; }, /** * Returns acl actions for subpanels based on metadata. * @return {Object} Alcs for subpanels. * @private */ _getSubpanelsAclsActions: function() { var subpanelsMeta = app.metadata.getModule(this.module).layouts.subpanels, subpanelsAclActions = {}; if (subpanelsMeta && subpanelsMeta.meta && subpanelsMeta.meta.components) { _.each(subpanelsMeta.meta.components, function(comp) { if (comp.context && comp.context.link) { subpanelsAclActions[comp.context.link] = comp.acl_action ? comp.acl_action : 'list'; } }); } return subpanelsAclActions; } }) }, "dashlet-nestedset-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Dashlet-nestedset-list View (base) plugins: ['Dashlet', 'NestedSetCollection', 'JSTree', 'KBNotify'], /** * Module name that provides an netedset data. * * @property {String} */ moduleRoot: null, /** * Root ID of a shown NestedSet. * @property {String} */ categoryRoot: null, /** * Module to load additional data into nested set. * @property {Object} * @property {String} extraModule.module Module to load additional data from. * @property {String} extraModule.field Linked field of provided module. */ extraModule: null, /** * Cache to store loaded leafs to prevent extra loading. * @property {Object} */ loadedLeafs: null, /** * Lifetime for data cache in ms. * @property {Number} */ cacheLifetime: 300000, /** * Flag which indicate, if we need to use saved states. * @property {Boolean} */ useStates: true, /** * Value of extraModule.field. * @property {String} */ currentFieldValue: null, /** * Flag indicates should we hide tree. */ hidden: null, /** * Initialize dashlet properties. */ initDashlet: function() { var config = app.metadata.getModule( this.meta.config_provider, 'config' ), currentContext = this.context.parent || this.context, currentModule = currentContext.get('module'), currentAction = currentContext.get('action'); this.moduleRoot = this.settings.get('data_provider'); this.categoryRoot = !_.isUndefined(config.category_root) ? config.category_root : null; this.extraModule = this.meta.extra_provider || {}; if (currentModule === this.extraModule.module && (currentAction === 'detail' || currentAction === 'edit') ) { this.useStates = false; this.changedCallback = _.bind(this.modelFieldChanged, this); this.savedCallback = _.bind(this.modelSaved, this); this.context.get('model').on('change:' + this.extraModule.field, this.modelFieldChanged, this); this.context.get('model').on('data:sync:complete', this.modelSaved, this); this.currentFieldValue = this.context.get('model').get(this.extraModule.field); this.on('openCurrentParent', this.hideTree, this); } else { this.on('stateLoaded', this.hideTree, this); } currentContext.on('subpanel:reload', function(args) { if (!_.isUndefined(args) && _.isArray(args.links) && (_.contains(args.links, 'revisions') || _.contains(args.links, 'localizations')) ) { this.layout.reloadDashlet({complete: function() {}, saveLeafs: false}); } }, this); this.on('kb:collection:updated', _.bind(function() { _.defer(function(self) { if (self.layout.disposed === true) { return; } if (!_.isUndefined(self.layout.reloadDashlet)) { self.layout.reloadDashlet({complete: function() {}, saveLeafs: false}); } }, this); }, this)); }, /** * The view doesn't need standard handlers for data change because it use own events and handlers. * * @override. */ bindDataChange: function() {}, /** * @inheritdoc */ _render: function() { this._super('_render'); if (this.meta.config) { return; } this.hideTree(this.hidden); var treeOptions = { settings: { category_root: this.categoryRoot, module_root: this.moduleRoot, plugins: [], liHeight: 14 }, options: { }}, callbacks = { onLeaf: _.bind(this.leafClicked, this), onToggle: _.bind(this.folderToggled, this), onLoad: _.bind(this.treeLoaded, this), onSelect: _.bind(this.openRecord, this), onLoadState: _.bind(this.stateLoaded, this) }; if (this.useStates === true) { treeOptions.settings.plugins.push('state'); treeOptions.options.state = { save_selected: false, auto_save: false, save_opened: 'jstree_open', options: {}, storage: this._getStorage() }; } this._renderTree(this.$('[data-place=dashlet-tree]'), treeOptions, callbacks); }, /** * Return storage for tree state. * @return {Function} * @private */ _getStorage: function () { var self = this; return function(key, value, options) { var intKey = app.user.lastState.buildKey(self.categoryRoot, self.moduleRoot, self.module); if (!_.isUndefined(value)) { app.user.lastState.set(intKey, value); } return app.user.lastState.get(intKey); }; }, /** * Open a document. * @param {string} module Module name * @param {string} id Record id */ _openDocument: function(module, id) { var route = app.router.buildRoute(module, id); app.router.navigate(route, {trigger: true}); }, /** * Handle tree selection. * @param data {Object} Selected item. */ openRecord: function(data) { switch (data.type) { case 'document': if (_.isEmpty(this.extraModule.module)) { break; } let $selected = this.$el.find('[data-id=' + data.id + ']'); if (!$selected.data('disabled')) { if (this.closestComponent('side-drawer')) { let recordName = $selected.find('a').text(); let recordContext = { layout: 'record', dashboardName: recordName, context: { layout: 'record', name: 'record-drawer', contentType: 'record', module: this.extraModule.module, modelId: data.id, dataTitle: app.sideDrawer.getDataTitle( this.extraModule.module, 'LBL_RECORD', recordName) } }; app.sideDrawer.open(recordContext, null, true); break; } this._openDocument(this.extraModule.module, data.id); } break; case 'folder': if (this.$el.find('[data-id=' + data.id +']').hasClass('jstree-closed')) { this.openNode(data.id); data.open = true; } else { this.closeNode(data.id); data.open = false; } this.folderToggled(data); break; } }, /** * Handle tree loaded. Load additional leafs for the tree. * @return {boolean} If tree has been loaded. */ treeLoaded: function() { var self = this; if (_.isEmpty(this.collection)) { return false; } this.bulkLoadLeafs(this.collection.models, function() { if (self.useStates) { self.loadJSTreeState(); } else { self.openCurrentParent(); } }); return true; }, /** * Loads leafs for all models (nodes) using single request. * * @param {Array} models Array of models (categories) which additional leafs will be loaded for. * @param {Function} callback Callback function that will be run after leafs loaded. */ bulkLoadLeafs: function(models, callback) { var ids = _.map(models, function(model) { return model.id; }); if (ids.length === 0) { if (_.isFunction(callback)) { callback.call(); } return; } this.loadAdditionalLeafs(ids, callback, true); }, /** * Open category, which is parent to current record. */ openCurrentParent: function() { if (_.isEmpty(this.extraModule) || _.isEmpty(this.extraModule.module) || _.isEmpty(this.extraModule.field) ) { return; } var currentContext = this.context.parent || this.context, currentModel = currentContext.get('model'), id = currentModel.get(this.extraModule.field), self = this; this.loadAdditionalLeafs([id], function() { if (self.disposed) { return; } var nestedBean = self.collection.getChild(id); if (!_.isUndefined(nestedBean)) { nestedBean.getPath({ success: function(data) { var path = []; _.each(data, function(cat) { if (cat.id == this.categoryRoot) { return; } path.push({ id: cat.id, name: cat.name }); }, self); path.push({ id: nestedBean.id, name: nestedBean.get('name') }); async.forEach( path, function(item, c) { self.folderToggled({ id: item.id, name: item.name, type: 'folder', open: true }, c); }, function() { self.selectNode(currentModel.id); self.trigger('openCurrentParent', false); } ); } }); } else { self.trigger('openCurrentParent', false); } }); }, /** * Handle load state of tree. * Always returns true to process the code, which called the method. * @param {Object} data Data of loaded tree. * @return {Boolean} Always returns `true`. */ stateLoaded: function(data) { var self = this; var models = _.reduce(data.open, function(memo, value) { var model = self.collection.getChild(value.id); return _.extend(memo, model.children.models); }, []); this.bulkLoadLeafs(models, function() { _.each(data.open, function(value) { self.openNode(value.id); }); self.trigger('stateLoaded', false); }); return true; }, /** * Handle toggle of tree folder. * Always returns true to process the code, which called the method. * @param {Object} data Toggled folder. * @param {Function} callback Async callback to use with async.js * @return {Boolean} Return `true` to continue execution, `false` otherwise.. */ folderToggled: function (data, callback) { var triggeredCallback = false, self = this; if (data.open) { var model = this.collection.getChild(data.id), items = []; if (model.id) { items = model.children.models; if (items.length !== 0) { triggeredCallback = true; this.bulkLoadLeafs(items, function() { if (_.isFunction(callback)) { callback.call(); return false; } else if (self.useStates) { self.saveJSTreeState(); } }); } } } if (triggeredCallback === false && _.isFunction(callback)) { callback.call(); return false; } if (this.useStates && triggeredCallback === false) { this.saveJSTreeState(); } return true; }, /** * Handle leaf click for tree. * @param {Object} data Clicked leaf. */ leafClicked: function (data) { if (data.type !== 'folder') { if (_.isEmpty(this.extraModule.module)) { return; } if (!this.$el.find('[data-id=' + data.id +']').data('disabled')) { this._openDocument(this.extraModule.module, data.id); } return; } this.loadAdditionalLeafs([data.id]); }, /** * Load extra data for tree. * * @param {Array} ids Ids of tree nodes to load data in. * @param {Function} callback Callback funct * @param {boolean} bulkLoad Identify if we need to perform bulk load */ loadAdditionalLeafs: function(ids, callback, bulkLoad) { var self = this; var processedIds = _.filter(ids, function(id) { return self.addLeafFromCache(id); }); if (processedIds.length === ids.length) { if (_.isFunction(callback)) { callback.call(); } return; } var collection = this.createCollection(); collection.filterDef = [{}]; collection.filterDef[0][this.extraModule.field] = {$in: ids}; collection.filterDef[0]['status'] = {$equals: 'published'}; collection.filterDef[0]['active_rev'] = {$equals: 1}; collection.fetch({ success: function(data) { var groupedModels = _.groupBy(data.models, function(model) { return model.get('category_id'); }); _.each(ids, function(id) { self.addLeafs(groupedModels[id] || [], id); }); if (_.isFunction(callback)) { callback.call(); } }, apiOptions: bulkLoad ? {bulk: true} : {} }); app.api.triggerBulkCall(); }, /** * Tries to find loaded leaf in cache and adds it to the tree. * * @param {String} id Leaf id. * @return {boolean} Returns true if leaf was added from cache, otherwise - false. */ addLeafFromCache: function(id) { if (!_.isUndefined(this.loadedLeafs[id]) && this.loadedLeafs[id].timestamp < Date.now() - this.cacheLifetime) { delete this.loadedLeafs[id]; } if (_.isEmpty(this.extraModule) || id === undefined || _.isEmpty(id) || _.isEmpty(this.extraModule.module) || _.isEmpty(this.extraModule.field) || !_.isUndefined(this.loadedLeafs[id]) ) { if (!_.isUndefined(this.loadedLeafs[id])) { this.addLeafs(this.loadedLeafs[id].models, id); } return true; } return false; }, /** * Creates bean collection with predefined options. * * @return {Object} Bean Collection. */ createCollection: function() { var collection = app.data.createBeanCollection(this.extraModule.module); collection.options = { params: { order_by: 'date_entered:desc' }, fields: [ 'id', 'name' ] }; return collection; }, /** * @inheritdoc * * Need additional check for tree leafs. * * @override */ loadData: function(options) { this.hideTree(true); if (!options || _.isUndefined(options.saveLeafs) || options.saveLeafs === false) { this.loadedLeafs = {}; } if (options && options.complete) { this._render(); options.complete(); } }, /** * Override behavior of JSTree plugin. * @param {Data,BeanCollection} collection synced collection. */ onNestedSetSyncComplete: function(collection) { if (this.disposed || this.collection.root !== collection.root) { return; } this.layout.reloadDashlet({complete: function() {}, saveLeafs: true}); }, /** * Handle change of this.extraModule.field. * @param {Data.Bean} model Changed model. * @param {String} value Current field value of the field. */ modelFieldChanged: function(model, value) { delete this.loadedLeafs[this.currentFieldValue]; this.currentFieldValue = value; }, /** * Handle save of context model. */ modelSaved: function() { delete this.loadedLeafs[this.currentFieldValue]; this.onNestedSetSyncComplete(this.collection); }, /** * @inheritdoc */ _dispose: function() { var model; if (this.useStates === false && (model = this.context.get('model'))) { model.off('change:' + this.extraModule.field, this.changedCallback); model.off('data:sync:complete', this.savedCallback); } this._super('_dispose'); }, /** * Add documents as leafs for categories. * @param {Array} models Documents which should be added into the tree. * @param {String} id ID of category leaf to insert documents in. */ addLeafs: function(models, id) { this.removeChildrens(id, 'document'); _.each(models, function(value) { var insData = { id: value.id, name: value.get('name'), isViewable: app.acl.hasAccessToModel('view', value) }; this.insertNode(insData, id, 'document'); }, this); this.loadedLeafs[id] = { timestamp: Date.now(), models: models }; }, /** * Hide or show tree, * @param {boolean} hide Whether we need to hide tree. */ hideTree: function(hide) { hide = hide || false; if (!hide) { this.hidden = false; this.$('[data-place=dashlet-tree]').removeClass('hide').show(); this.$('[data-place=loading]').addClass('hide').hide(); } else { this.hidden = true; this.$('[data-place=dashlet-tree]').addClass('hide').hide(); this.$('[data-place=loading]').removeClass('hide').show(); } } }) }, "panel-top-for-cases": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.KBContentsPanelTopForCases * @alias SUGAR.App.view.views.BaseKBContentsPanelTopForCases * @extends View.Views.Base.PanelTopView */ ({ // Panel-top-for-cases View (base) extendsFrom: 'PanelTopView', plugins: ['KBContent'], /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); }, /** * Event handler for the create button. * * @param {Event} event The click event. */ createRelatedClicked: function(event) { this.createArticleSubpanel(); }, }) }, "panel-top-for-revisions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Panel-top-for-revisions View (base) extendsFrom: 'PanelTopView', plugins: ['KBContent'], /** * @inheritdoc */ createRelatedClicked: function(event) { var parentModel = this.context.parent.get('model'); if (parentModel) { this.createRelatedContent(parentModel, this.CONTENT_REVISION); } } }) }, "subpanel-for-revisions": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Subpanel-for-revisions View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ dataView: 'subpanel-for-revisions', }) }, "kbs-dashlet-usefulness": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Kbs-dashlet-usefulness View (base) plugins: ['Dashlet'], /** * Holds report data from the server's endpoint once we fetch it */ chartData: undefined, /** * We'll use this property to bind loadData function for event */ refresh: null, /** * Flag to store if we're already making a request. Use this to avoid obvious and unnecessary * chart re-renders from both `useful` and `notuseful` getting updated at once. * @property {boolean} */ isFetching: false, /** * @inheritdoc */ initialize: function(options) { this.chartData = new Backbone.Model(); this._super('initialize', [options]); this.refresh = _.bind(this.loadData, this); this.listenTo(app.controller.context.get('model'), 'change:useful', this.refresh); this.listenTo(app.controller.context.get('model'), 'change:notuseful', this.refresh); }, /** * @inheritdoc */ loadData: function(options) { var currModel = app.controller.context.get('model'), model = currModel.clone(), opts = options || {}, self = this; if (this.isFetching) { return; } this.isFetching = true; model.fetch({ success: function(model) { var dt = self.layout.getComponent('dashlet-toolbar'), useful = model.get('useful') || '0', notuseful = model.get('notuseful') || '0'; if (dt) { // manually set the icon class to spiny self.$('[data-action=loading]') .removeClass(dt.cssIconDefault) .addClass(dt.cssIconRefresh); } useful = parseInt(useful, 10); notuseful = parseInt(notuseful, 10); // correcting values for pie chart, // because pie chart not support all zero values. if (0 === useful && 0 === notuseful) { self.chartData.set({rawChartData: {values: []}}); return; } let chartData = { properties: [{ labels: 'value', type: 'donut chart' }], values: [ { label: [app.lang.get('LBL_USEFUL', 'KBContents')], values: [useful], }, { label: [app.lang.get('LBL_NOT_USEFUL', 'KBContents')], values: [notuseful], }, ], }; let chartParams = { hole: `${parseInt(useful * 100 / (notuseful + useful))}%`, donutLabelsOutside: true, chartType: 'donutChart', show_legend: false, show_title: false, colorOverrideList: [ '#00ba83', // @green '#fa374f', // @red ], }; _.defer(_.bind(function() { self.chartData.set({rawChartData: chartData, rawChartParams: chartParams}); }, this)); }, complete: () => { this.isFetching = false; if (opts && _.isFunction(opts.complete)) { opts.complete(); } } }); }, /** * @inheritdoc * * Dispose listeners for 'change:useful' and 'change:notuseful' events. */ dispose: function() { this.stopListening(app.controller.context.get('model'), 'change:useful', this.refresh); this.stopListening(app.controller.context.get('model'), 'change:notuseful', this.refresh); this._super('dispose'); } }) }, "module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Module menu provides a reusable and easy render of a module Menu. * * This also helps doing customization of the menu per module and provides more * metadata driven features. * * @class View.Views.Base.KBContents.ModuleMenuView * @alias SUGAR.App.view.views.BaseKBContentsModuleMenuView * @extends View.Views.Base.ModuleMenuView */ ({ // Module-menu View (base) extendsFrom: 'ModuleMenuView', /** * Root ID of a shown NestedSet. * @property {string} */ categoryRoot: null, /** * Module which implements NestedSet. */ moduleRoot: null, /** * Panel label. */ label: null, /** * @inheritdoc * * Init additional properties and events. */ initialize: function(options) { this._super('initialize', [options]); var module = this.meta.config.config_provider || this.context.get('module'), config = app.metadata.getModule(module, 'config'); this.categoryRoot = this.meta.config.category_root || config.category_root || ''; this.moduleRoot = this.meta.config.data_provider || module; this.label = this.meta.label || ''; this.events = _.extend({}, this.events, { 'click [data-event="tree:list:fire"]': 'handleCategoriesList' }); }, /** * Handle click on KB category menu item. */ handleCategoriesList: function() { var treeOptions = { category_root: this.categoryRoot, module_root: this.moduleRoot, plugins: ['dnd', 'contextmenu'], isDrawer: true }; var treeCallbacks = { 'onSelect': function() { return; }, 'onRemove': function(node) { if (this.context.parent) { this.context.parent.trigger('kbcontents:category:deleted', node); } } }, // @TODO: Find out why params from context for drawer don't pass to our view tree::_initSettings context = _.extend({}, this.context, {treeoptions: treeOptions, treecallbacks: treeCallbacks}); if (app.drawer.getActiveDrawerLayout().module === this.moduleRoot) { app.drawer.closeImmediately(); } app.drawer.open({ layout: 'nested-set-list', context: { module: this.moduleRoot, parent: context, title: app.lang.getModString(this.label, this.module), treeoptions: treeOptions, treecallbacks: treeCallbacks } }); }, /** * @inheritdoc */ populate: function(tplName, filter, limit) { if (limit <= 0) { return; } filter = _.union([], filter, this.meta.filterDef || []); this.getCollection(tplName).fetch({ 'showAlerts': false, 'fields': ['id', 'name'], 'filter': filter, 'limit': limit, 'success': _.bind(function() { this._renderPartial(tplName); }, this) }); } }) }, "subpanel-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * Custom Subpanel Layout for Revenue Line Items. * * @class View.Views.Base.KBContents.SubpanelListView * @alias SUGAR.App.view.views.BaseKBContentsSubpanelListView * @extends View.Views.Base.SubpanelListView */ ({ // Subpanel-list View (base) extendsFrom: 'SubpanelListView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['KBContent']); this._super('initialize', [options]); } }) }, "record": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Record View (base) extendsFrom: 'RecordView', /** * @inheritdoc * * Add KBContent plugin for view. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'KBContent' ]); this._super('initialize', [options]); this.context.on('kbcontents:category:deleted', this._categoryDeleted, this); }, /** * Process record on category delete. * @param {Object} node * @private */ _categoryDeleted: function(node) { if (this.model.get('category_id') === node.data('id')) { this.model.unset('category_id'); this.model.unset('category_name'); } if (this.disposed) { return; } this.render(); }, /** * @inheritdoc */ handleSave: function() { // this is to handle the issue caused by different value between boolean and tinyint this.model.set('is_external', app.utils.isTruthy(this.model.get('is_external')) ? 1 : 0); this._super('handleSave'); }, /** * @inheritdoc * * Need to switch field to `edit` if it has errors. */ handleFieldError: function(field, hasError) { this._super('handleFieldError', [field, hasError]); if (hasError && field.tplName === 'detail') { field.setMode('edit'); } }, /** * @inheritdoc */ hasUnsavedChanges: function() { // since TinyMCE deletes double spaces on text load, check for actual changes const delta = this.model.changedAttributes(this.model.getSynced()); const key = 'kbdocument_body'; if (delta && _.isEqual(Object.keys(delta), [key])) { const modelValue = this.model.get(key); if (modelValue === delta[key].replace(/\s+/g, ' ').replace(/>\s+</g, '><')) { return false; } } return this._super('hasUnsavedChanges'); } }) }, "config-header-buttons": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.SchedulersJobsConfigHeaderButtonsView * @alias SUGAR.App.view.layouts.BaseSchedulersJobsConfigHeaderButtonsView * @extends View.Views.Base.ConfigHeaderButtonsView */ ({ // Config-header-buttons View (base) extendsFrom: 'ConfigHeaderButtonsView', /** * Saves the config model * * Also calling doValidate to check that there is no Language duplication * * @private */ _saveConfig: function() { var self = this, model = this.context.get('model'); // Standard ConfigHeaderButtonsView doesn't use doValidate model.doValidate(null, function(isValid) { if (isValid) { self._super('_saveConfig'); } else { self.getField('save_button').setDisabled(false); } }); } }) }, "filter-rows": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Filter-rows View (base) extendsFrom: 'FilterRowsView', /** * @inheritdoc * * Add 'kbdocument_body' filter field only on KBContents listView. This field is not present in filter's * metadata to avoid its appearance on KBContents subpanels - this is done due to technical inability to make * REST calls to specific module's RelateApi and thus to perform KB specific filtering logic. */ loadFilterFields: function(module) { this._super('loadFilterFields', [module]); if (this.context.get('layout') === 'records') { var bodyField = this.model.fields['kbdocument_body']; this.fieldList[bodyField.name] = bodyField; this.filterFields[bodyField.name] = app.lang.get(bodyField.vname, this.module); } } }) }, "list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // List View (base) extendsFrom: 'ListView', /** * @inheritdoc * * Add KBContent plugin for view. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'KBContent' ]); this._super('initialize', [options]); } }) }, "panel-top-for-localizations": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Panel-top-for-localizations View (base) extendsFrom: 'PanelTopView', plugins: ['KBContent'], /** * @inheritdoc */ createRelatedClicked: function(event) { var parentModel = this.context.parent.get('model'); if (parentModel) { this.createRelatedContent(parentModel, this.CONTENT_LOCALIZATION); } } }) }, "dashlet-searchable-kb-list": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.KBContents.DashletSearchableKbListView * @alias SUGAR.App.view.views.BaseKBContentsDashletSearchableKbListView * @extends View.DashletNestedsetListView */ ({ // Dashlet-searchable-kb-list View (base) extendsFrom: 'KBContentsDashletNestedsetListView', events: { 'keyup [data-action="search"]': 'searchKBs', 'click .sicon-close': 'clearQuickSearch' }, /** * Search options. * @property {Object} */ searchOptions: { max_num: 4, module_list: 'KBContents' }, /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.searchDropdown = null; this.context.on('page:clicked', this._search, this); }, /** * Handler for clearing the quick search bar * * @param {Event} event A click event on the close button of search bar */ clearQuickSearch: function(event) { // clear the input this.$('input[data-action=search]').val(''); // clear the drop down if (this.searchDropdown) { this.searchDropdown.hide(); } // remove the icon this.toggleClearQuickSearchIcon(false); }, /** * Append or remove an icon to the quicksearch input so the user can clear the search easily * @param {boolean} addIt TRUE if you want to add it, FALSE to remove */ toggleClearQuickSearchIcon: function(addIt) { if (addIt && !this.$('.sicon-close.add-on')[0]) { this.$('.search-container').append('<i class="sicon sicon-close add-on"></i>'); } else if (!addIt) { this.$('.sicon-close.add-on').remove(); } }, /** * Initialize dashlet properties. */ initDashlet: function() { this._super('initDashlet'); this.searchOptions = { module_list: this.settings.get('module_list') || this.searchOptions.module_list, max_num: this.settings.get('max_num') || this.searchOptions.max_num }; this.maxChars = this.settings.get('max_chars') || this.maxChars; }, /** * Starts a new search and show the search results dropdown. */ searchKBs: _.debounce(function() { var $input = this.$('input[data-action=search]'); var term = $input.val().trim(); if (term === '') { if (this.searchDropdown) { this.searchDropdown.hide(); } this.toggleClearQuickSearchIcon(false); return; } this.searchOptions.q = term; if (_.isNull(this.searchDropdown)) { this.searchDropdown = app.view.createLayout({ context: this.context, name: 'contentsearch-dropdown' }); this.searchDropdown.initComponents(); this.layout._components.push(this.searchDropdown); this.searchDropdown.render(); $input.after(this.searchDropdown.$el); } this.searchDropdown.hide(); this.context.trigger('data:fetching'); this.searchDropdown.show(); this._search(); this.toggleClearQuickSearchIcon(true); }, 400), /** * Calls search api * * @param {Object} options The search options * @private */ _search: function(options) { var pageNumber = options && options.pageNum || 1; var offset = (pageNumber - 1) * this.searchOptions.max_num; var params = _.extend({}, this.searchOptions, {offset: offset}); var url = app.api.buildURL('genericsearch', null, null, params); app.api.call('read', url, null, { success: _.bind(function(result) { if (this.disposed) { return; } if (this.context) { var data = this._parseData(result); this.context.trigger('data:fetched', data); } }, this) }); }, /** * Parses search results. * * @param {Object} result The search result * @return {Object} parsed data * @private */ _parseData: function(result) { var self = this; var totalPages = result.total > 0 ? Math.ceil(result.total / this.searchOptions.max_num) : 0; var currentPage = result.next_offset > 0 ? result.next_offset / this.searchOptions.max_num : totalPages; var records = _.map(result.records, function(record) { return { name: record.name, description: self._truncate(record.description), url: app.utils.buildUrl(record.url.replace(/^\/+/g, '')) }; }); return { options: this.searchOptions, currentPage: currentPage, records: records, totalPages: totalPages }; }, /** * Truncates search result so it is shorter than the maxChars * Only truncate on full words to prevent ellipsis in the middle of words * @param {string} text The search result entry to truncate * @return {string} the shortened version of an entry * @private */ _truncate: function(text) { text = text || ''; if (text.length > this.maxChars) { var cut = text.substring(0, this.maxChars); // cut at a full word while (!(/\s/.test(cut[cut.length - 1])) && cut.length > 0) { cut = cut.substring(0, cut.length - 1); } text = cut + '...'; } return text; }, /** * Open a document in new tab. * @inheritdoc */ _openDocument: function(module, id) { var route = app.router.buildRoute(module, id); window.open('#' + route, '_blank'); }, /** * The view doesn't need standard handlers for data change because it use own events and handlers. * * @override */ bindDataChange: function() {} }) }, "create": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.KBContents.CreateView * @alias SUGAR.App.view.views.BaseKBContentsCreateView * @extends View.Views.Base.CreateView */ ({ // Create View (base) extendsFrom: 'CreateView', className: 'kb-contents-create', /** * @inheritdoc * * Add 'KBContent', 'KBNotify' and 'TinymceHtmlEditor' plugins for view. */ initialize: function(options) { this.plugins = _.union(this.plugins || [], [ 'KBContent', 'KBNotify', 'Tinymce' ]); this._super('initialize', [options]); }, /** * Using the model returned from the API call, build the success message. * @param {Data.Bean} model KBContents bean for record that was just created. * @return {string} The success message. */ buildSuccessMessage: function(model) { var message = this._super('buildSuccessMessage', [model]); // If user has no access to view record - don't show record link for him if (!app.acl.hasAccessToModel('view', this.model)) { message = message.replace(/<\/?a[^>]+>/g, ''); } return message; }, /** * @inheritdoc */ save: function() { // this is to handle the issue caused by different value between boolean and tinyint this.model.set('is_external', app.utils.isTruthy(this.model.get('is_external')) ? 1 : 0); this._super('save'); }, /** * Overriding custom save options to trigger kb:collection:updated event when KB model saved. * * @override * @param {Object} options */ getCustomSaveOptions: function(options) { var success = _.compose(options.success, _.bind(function(model) { this.notifyAll('kb:collection:updated', model); return model; }, this)); return {'success': success}; } }) }, "prefilteredlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.PrefilterelistView * @alias SUGAR.App.view.views.BasePrefilteredlistView * @extends View.Views.Base.RecordlistView */ ({ // Prefilteredlist View (base) extendsFrom: 'RecordlistView', /** * Load recordlist templates. * @inheritdoc */ _loadTemplate: function(options) { this.tplName = 'recordlist'; this.template = app.template.getView(this.tplName); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) }, "preview": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.KBContentsPreviewView * @alias SUGAR.App.view.views.BaseKBContentsPreviewView * @extends View.Views.Base.PreviewView */ ({ // Preview View (base) extendsFrom: 'PreviewView', /** * @inheritdoc */ initialize: function(options) { this.plugins = _.union(this.plugins || [], ['KBContent']); this._super('initialize', [options]); }, /** * We don't need to initialize KB listeners. * @override. * @private */ _initKBListeners: function() { } }) }, "kbs-dashlet-localizations": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Kbs-dashlet-localizations View (base) plugins: ['Dashlet'], events: { 'click [data-action=show-more]': 'loadMoreData' }, /** * @inheritdoc * * @property {number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '5'. */ _defaultSettings: { limit: 5 }, /** * KBContents bean collection. * * @property {Data.BeanCollection} */ collection: null, /** * @inheritdoc * * Init collection. */ initDashlet: function () { this._initSettings(); this._initCollection(); }, /** * Sets up settings, starting with defaults. * * @return {View.Views.BaseRelatedDocumentsView} Instance of this view. * @protected */ _initSettings: function () { this.settings.set( _.extend( {}, this._defaultSettings, this.settings.attributes ) ); return this; }, /** * Initialize feature collection. */ _initCollection: function () { this.collection = app.data.createBeanCollection(this.module); this.context.set('collection', this.collection); return this; }, /** * @inheritdoc * * Once collection has been changed, the view should be refreshed. */ bindDataChange: function () { if (this.collection) { this.collection.on('add remove reset', function () { if (this.disposed) { return; } this.render(); }, this); } }, /** * Load more data (paginate). */ loadMoreData: function () { if (this.collection.next_offset > 0) { this.collection.paginate({add: true}); } }, /** * @inheritdoc */ loadData: function (options) { if (this.collection.dataFetched) { return; } var currentContext = this.context.parent || this.context, model = currentContext.get('model'); if (!model.get('kbdocument_id')) { model.once('sync', function() {this.loadData();}, this); return; } options = options || {}; this.collection.setOption({ limit: this.settings.get('limit'), fields: [ 'id', 'name', 'date_entered', 'created_by', 'created_by_name', 'language' ], filter: { 'kbdocument_id': { '$equals': model.get('kbdocument_id') }, 'id' : { '$not_equals': model.get('id') }, 'status': { '$equals': 'published' }, 'active_rev': { '$equals': 1 } } }); if (!options.error) { options.error = _.bind(function(collection, error) { if (error.code === 'not_authorized') { this.$el.find('.block-footer').html(app.lang.get('LBL_NO_DATA_AVAILABLE', this.module)); } }, this); } this.collection.fetch(options); } }) }, "kbs-dashlet-most-useful": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Kbs-dashlet-most-useful View (base) plugins: ['Dashlet'], events: { "click [data-action=show-more]": "loadMoreData" }, /** * KBContents bean collection. * * @property {Data.BeanCollection} */ collection: null, /** * We'll use this property to bind loadData function for event */ refresh: null, /** * @inheritdoc */ initialize: function (options) { var self = this; options.module = 'KBContents'; this._super('initialize', [options]); this.refresh = _.bind(this.loadData, this); if (_.isUndefined(this.meta.config) || this.meta.config === false){ this.listenTo(this.context.parent.get('collection'), 'sync', function () { if (self.collection) { self.collection.dataFetched = false; self.layout.reloadDashlet(options); } }); } this._initCollection(); this.listenTo(app.controller.context.get('model'), 'change:useful', this.refresh); this.listenTo(app.controller.context.get('model'), 'change:notuseful', this.refresh); }, /** * Initialize feature collection. */ _initCollection: function () { this.collection = app.data.createBeanCollection(this.module); this.collection.setOption({ params: { order_by: 'useful:desc,notuseful:asc,viewcount:desc,date_entered:desc', mostUseful: true }, limit: 3, fields: [ 'id', 'name', 'date_entered', 'created_by', 'created_by_name' ], filter: { 'active_rev': { '$equals': 1 }, 'useful': { '$gt': { '$field': 'notuseful' } }, 'status': { '$equals': 'published' } } }); return this; }, /** * @inheritdoc * * Once collection has been changed, the view should be refreshed. */ bindDataChange: function () { if (this.collection) { this.collection.on('add remove reset', function () { if (this.disposed) { return; } this.render(); }, this); } }, /** * Load more data (paginate) */ loadMoreData: function () { if (this.collection.next_offset > 0) { this.collection.paginate({add: true}); } }, /** * @inheritdoc */ loadData: function (options) { this.collection.resetPagination(); this.collection.fetch({ success: function () { if (options && options.complete) { options.complete(); } }, error: _.bind(function(collection, error) { if (error.code === 'not_authorized') { this.$el.find('.block-footer').html(app.lang.get('LBL_NO_DATA_AVAILABLE', this.module)); } }, this) }); }, /** * @inheritdoc * * Dispose listeners for 'change:useful' and 'change:notuseful' events. */ dispose: function() { this.stopListening(app.controller.context.get('model'), 'change:useful', this.refresh); this.stopListening(app.controller.context.get('model'), 'change:notuseful', this.refresh); this._super('dispose'); } }) }, "related-documents": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Related-documents View (base) plugins: ['Dashlet'], events: { 'click [data-action=show-more]': 'loadMoreData' }, /** * @inheritdoc * * @property {Object} _defaultSettings Default settings. * @property {number} _defaultSettings.limit Maximum number of records to * load per request, defaults to '5'. */ _defaultSettings: { limit: 5 }, /** * KBContents bean collection. * * @property {Data.BeanCollection} */ collection: null, /** * @inheritdoc * * Initialize settings and collection. */ initDashlet: function() { this._initSettings(); this._initCollection(); }, /** * Sets up settings, starting with defaults. * * @return {View.Views.BaseRelatedDocumentsView} Instance of this view. * @protected */ _initSettings: function() { this.settings.set( _.extend( {}, this._defaultSettings, this.settings.attributes ) ); return this; }, /** * Initialize feature collection. */ _initCollection: function() { this.collection = app.data.createBeanCollection(this.module); this.collection.options = { limit: this.settings.get('limit'), fields: [ 'id', 'name', 'date_entered', 'created_by', 'created_by_name' ] }; this.collection.sync = _.wrap( this.collection.sync, _.bind(function(sync, method, model, options) { options = options || {}; var viewModelId = this.model.get('id') || this.context.get('model').get('id') || this.context.parent.get('model').get('id'); options.endpoint = function(method, model, options, callbacks) { var url = app.api.buildURL( model.module, 'related_documents', { id: viewModelId }, options.params ); return app.api.call('read', url, {}, callbacks); }; sync(method, model, options); }, this) ); this.context.set('collection', this.collection); return this; }, /** * @inheritdoc * * Once collection has been changed, the view should be refreshed. */ bindDataChange: function() { if (this.collection) { this.collection.on('add remove reset', this.render, this); } }, /** * Load more data (paginate) */ loadMoreData: function() { if (this.collection.next_offset > 0) { this.collection.paginate({add: true}); } }, /** * @inheritdoc * * Fetch collection if it was not fetched before. */ loadData: function(options) { options = options || {}; if (this.collection.dataFetched) { return; } this.collection.fetch(options); } }) } }} , "layouts": { "base": { "sidebar-nav-flyout-module-menu": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.KBContents.SidebarNavFlyoutModuleMenuLayout * @alias SUGAR.App.view.layouts.BaseKBContentsSidebarNavFlyoutModuleMenuLayout * @extends View.Layouts.Base.SidebarNavFlyoutModuleMenuLayout */ ({ // Sidebar-nav-flyout-module-menu Layout (base) extendsFrom: 'SidebarNavFlyoutModuleMenuLayout', /** * Root ID of a shown NestedSet. * @property {string} */ categoryRoot: null, /** * Module which implements NestedSet. */ moduleRoot: 'Categories', /** * Panel label. */ label: 'LNK_LIST_KBCATEGORIES', /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this.listenTo(app.events, 'tree:list:fire', this.displayCategoriesList, this); }, /** * Handle click on KB view categories. */ displayCategoriesList: function() { let route = '#KBContents'; let currentRoute = `#${app.router.getFragment()}`; if (currentRoute !== route) { app.router.navigate(route, {trigger: true}); } let config = app.metadata.getModule('KBContents', 'config'); this.categoryRoot = config.category_root || ''; let treeOptions = { category_root: this.categoryRoot, module_root: this.moduleRoot, plugins: ['dnd', 'contextmenu'], isDrawer: true }; let treeCallbacks = { onSelect: function() { return; }, onRemove: function(node) { if (this.context.parent) { this.context.parent.trigger('kbcontents:category:deleted', node); } } }; app.drawer.open({ layout: 'nested-set-list', context: { module: this.moduleRoot, parent: this.context, title: app.lang.getModString(this.label, this.module), treeoptions: treeOptions, treecallbacks: treeCallbacks } }); }, }) }, "config-drawer": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.KBContentsConfigDrawerLayout * @alias SUGAR.App.view.layouts.BaseKBContentsConfigDrawerLayout * @extends View.Layouts.Base.ConfigDrawerLayout */ ({ // Config-drawer Layout (base) extendsFrom: 'ConfigDrawerLayout', /** * @inheritdoc */ _checkModuleAccess: function() { var acls = app.user.getAcls().KBContents, isSysAdmin = (app.user.get('type') == 'admin'), isAdmin = (!_.has(acls, 'admin')); isDev = (!_.has(acls, 'developer')); return (isSysAdmin || isAdmin || isDev); } }) }, "records-search-tags": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Records-search-tags Layout (base) /** * @inheritdoc */ initialize: function(options) { this._super('initialize', [options]); this._initializeCollectionFilterDef(options); }, /** * Initialize collection in the sub-sub-component recordlist * with specific filterDef using tags for build recordlist * filtered by tags. * * @param {Object} options * @private */ _initializeCollectionFilterDef: function(options) { if (_.isUndefined(options.def.context.tag)) { return; } var filterDef = { filter: [{ tag: { $in: [{ id: false, name: options.def.context.tag }] }, active_rev: { $equals: 1 } }] }; var chain = ['sidebar', 'main-pane', 'list', 'recordlist']; var recordList = _.reduce(chain, function(component, name) { if (!_.isUndefined(component)) { return component.getComponent(name); } }, this); if (!_.isUndefined(recordList)) { recordList.collection.filterDef = filterDef; } } }) }, "subpanels": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Subpanels Layout (base) extendsFrom: 'SubpanelsLayout', /** * @inheritdoc */ _pruneNoAccessComponents: function(components) { var prunedComponents = []; var layoutFromContext = this.context ? this.context.get('layout') || this.context.get('layoutName') : null; this.layoutType = layoutFromContext ? layoutFromContext : app.controller.context.get('layout'); this.aclToCheck = this.aclToCheck || (this.layoutType === 'record') ? 'view' : 'list'; _.each(components, function(component) { var relatedModule, link = component.context ? component.context.link : null; if (link) { relatedModule = app.data.getRelatedModule(this.module, link); var aclToCheck = component.acl_action || this.aclToCheck; if (!relatedModule || relatedModule && app.acl.hasAccess(aclToCheck, relatedModule)) { prunedComponents.push(component); } } }, this); return prunedComponents; } }) }, "config-drawer-content": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Layouts.Base.KBContentsConfigDrawerContentLayout * @alias SUGAR.App.view.layouts.BaseKBContentsConfigDrawerContentLayout * @extends View.Layouts.Base.ConfigDrawerContentLayout */ ({ // Config-drawer-content Layout (base) extendsFrom: 'ConfigDrawerContentLayout', /** * @inheritdoc * @override */ _switchHowToData: function(helpId) { switch (helpId) { case 'config-languages': this.currentHowToData.title = app.lang.get( 'LBL_CONFIG_LANGUAGES_TITLE', this.module ); this.currentHowToData.text = app.lang.get( 'LBL_CONFIG_LANGUAGES_TEXT', this.module ); break; } } }) } }} , "datas": {} }, "KBArticles":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "KBContentTemplates":{"fieldTemplates": { "base": { "htmleditable_tinymce": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Htmleditable_tinymce FieldTemplate (base) extendsFrom: 'BaseKBContentsHtmleditable_tinymceField', /** * Override to load handlebar templates from `KBContents module * @inheritdoc */ _loadTemplate: function() { var module = this.module; this.module = 'KBContents'; this._super('_loadTemplate'); this.module = module; } }) } }} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "EmbeddedFiles":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "TL_ActiveCampaigns":{"fieldTemplates": {} , "views": { "base": { "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} }, "adobe_sign":{"fieldTemplates": {} , "views": { "base": { "recordlist": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/06_Customer_Center/10_Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ ({ // Recordlist View (base) extendsFrom: 'RecordlistView', initialize: function (options) { this._super("initialize", [options]); this.context.on('list:check_file_status:fire', this.check_file_status, this); }, check_file_status: function () { app.alert.show('message-id', { level: 'process', title: 'Loading...' //change title to modify display from 'Loading...' }); //ajaxUI.showLoadingPanel(); var checked = ''; $("input:checkbox[name=check]:checked").each(function () { var id = jQuery(this).closest("tr").attr("name"); if (typeof id !== "undefined") { checked = checked + "___" + id; } }); if (checked.length > 0 && typeof checked !== "undefined") { $.ajax({ type: 'POST', url: 'index.php?module=adobe_sign&action=ajax_cnt&to_pdf=true&AjaxAction=check_file_status', data: {record: checked}, datatype: 'html', success: function (response, status) { if (response == 'ok') { app.alert.dismiss('message-id'); location.reload(); return false; } else { app.alert.dismiss('message-id'); app.alert.show('warning-message-id', { level: 'warning', messages: response, title: 'Warning! ', autoClose: true }); return false; } } }); } else { alert('No checkbox Checked to Check the File Status'); } }, showLoading: function () { SUGAR.ajaxUI.showLoadingPanel(); }, hideLoading: function () { SUGAR.ajaxUI.hideLoadingPanel(); } }) }, "activity-card-header": {"controller": /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ /** * @class View.Views.Base.Basic.ActivityCardHeaderView * @alias SUGAR.App.view.views.BaseBasicActivityCardHeaderView * @extends View.Views.Base.ActivityCardHeaderView */ ({ // Activity-card-header View (base) extendsFrom: 'ActivityCardHeaderView', /** * @inheritdoc */ setUsersFields: function() { var panel = this.getUsersPanel(); this.userField = _.find(panel.fields, function(field) { return field.name === 'assigned_user_name'; }); this.hasAvatarUser = !!this.userField; } }) } }} , "layouts": {} , "datas": {} } }}})(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { /* * Allow modules to extend routing rules. * * Manually create a route for the router. * The route argument may be a routing string or regular expression. */ var homeOptions = { dashboard: 'dashboard', activities: 'activities' }; var getLastHomeKey = function() { return app.user.lastState.buildKey('last-home', 'app-header'); }; var routes = [ { name: 'activities', route: 'activities', callback: function() { // when visiting activity stream, save last state of activities // so future Home routes go back to activities var lastHomeKey = getLastHomeKey(); app.user.lastState.set(lastHomeKey, homeOptions.activities); app.controller.loadView({ layout: 'activities', module: 'Activities' }); } }, { name: 'home', route: 'Home', callback: function() { var lastHomeKey = getLastHomeKey(), lastHome = app.user.lastState.get(lastHomeKey); if (lastHome === homeOptions.dashboard) { app.controller.loadView({ module: 'Home', layout: 'record' }); } else if (lastHome === homeOptions.activities) { app.router.redirect('#activities'); } } }, { name: 'homeCreate', route: 'Home/create', callback: function() { app.controller.loadView({ module: 'Home', layout: 'record', create: true }); } }, { name: 'homeRecord', route: 'Home/:id', callback: function(id) { // when visiting a dashboard, save last state of dashboard // so future Home routes go back to dashboard var lastHomeKey = getLastHomeKey(); app.user.lastState.set(lastHomeKey, homeOptions.dashboard); app.controller.loadView({ module: 'Home', layout: 'record', action: 'detail', modelId: id }); } } ]; /* * Triggering the event on init will go over all those listeners * and add the routes to the router. */ app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { var module = 'Emails'; /** * Open the email compose view in either a drawer or full-page. * * The view will be opened in a drawer if the user is routing from a page * in the application. The view will be opened in full-page if the user is * routing from login or a location outside the application. * * @param {Data.Bean} model The model that is given to the layout. */ function openEmailCompose(model) { var prevLayout = app.controller.context.get('layout'); if (prevLayout && prevLayout !== 'login') { app.utils.openEmailCreateDrawer( 'compose-email', { model: model, fromRouter: true }, function(context, model) { if (model && model.module === app.controller.context.get('module')) { app.controller.context.reloadData(); } } ); } else { options = { module: module, layout: 'compose-email', action: model.isNew() ? 'create' : 'edit', model: model, create: true }; app.controller.loadView(options); } } app.events.on('router:init', function() { var routes = [{ name: 'email_compose', route: module + '(/:id)/compose', callback: function(id) { var model = app.data.createBean(module); if (_.isEmpty(id)) { openEmailCompose(model); } else { model.set('id', id); model.fetch({ view: 'compose-email', params: { erased_fields: true }, success: function(model) { var route; if (model.get('state') === 'Draft' && app.acl.hasAccessToModel('edit', model)) { openEmailCompose(model); } else { // Handle routing for an email that used to be // a draft or a draft the current user cannot // edit. route = '#' + app.router.buildRoute(model.module, model.get('id')); app.router.redirect(route); } } }); } } }]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { const routes = [ { name: 'mainCalendar', route: 'Calendar/center', callback: function() { app.controller.loadView({ 'layout': 'main-scheduler', 'module': 'Calendar' }); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'productBundlesList', route: 'ProductBundles', callback: function() { app.router.redirect('#Quotes'); } }, { name: 'productBundlesCreate', route: 'ProductBundles/create', callback: function() { app.router.redirect('#Quotes'); } }, { name: 'productBundlesRecord', route: 'ProductBundles/:id', callback: function(id) { app.router.redirect('#Quotes'); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var module = 'Reports'; var routes = [ { name: 'ReportsList', route: 'Reports', callback: function(params) { var filterOptions; if (params) { var parsedParams = {filterModule: []}; // FIXME SC-5657 will handle url param parsing var paramsArray = params.split('&'); _.each(paramsArray, function(paramPair) { var keyValueArray = paramPair.split('='); if (keyValueArray.length > 1) { parsedParams[keyValueArray[0]] = keyValueArray[1].split(','); } }); if (!_.isEmpty(parsedParams.filterModule)) { filterOptions = new app.utils.FilterOptions().config({ initial_filter_label: app.lang.get('LBL_MODULE_NAME', parsedParams.filterModule), initial_filter: '$relate', filter_populate: { 'module': {$in: parsedParams.filterModule} } }); } } app.controller.loadView({ module: module, layout: 'records', filterOptions: filterOptions ? filterOptions.format() : null }); } }, { name: 'ReportsBwc', route: 'Reports/:id/bwc', callback: function(id) { app.router.navigate( `#bwc/index.php?module=Reports&action=DetailView&record=${id}&legacyBwc=1`, { trigger: true, replace: true } ); } }, ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'visualPipelineList', route: 'VisualPipeline', callback: function() { app.router.redirect('#Opportunities/pipeline'); } }, { name: 'visualPipelineCreate', route: 'VisualPipeline/create', callback: function() { app.router.redirect('#Opportunities/pipeline'); } }, { name: 'visualPipelineRecord', route: 'VisualPipeline/:id', callback: function(id) { if (id === 'config') { // Making a ping call to update/refresh the metadata. When there is a change made in studio and // the tile view config was opened it wouldn't have the updated metadata until refreshed app.api.call('read', app.api.buildURL('ping'), null, { success: function(data) { let prevLayout = app.controller.context.get('layout'); if (prevLayout && !_.contains(['login', 'bwc'], prevLayout)) { app.drawer.open({ layout: 'config-drawer', context: { module: 'VisualPipeline', fromRouter: true } }); return; } app.controller.loadView({ layout: 'config-drawer', module: 'VisualPipeline' }); }, }); } else { app.router.redirect('#Opportunities/pipeline'); } } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'consoleConfiguration', route: 'ConsoleConfiguration/config/:id', callback: function(id) { app.drawer.open({ layout: 'config-drawer', context: { module: 'ConsoleConfiguration', consoleId: id, fromRouter: true } }); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var module = 'pmse_Inbox'; var routes = [ { name: 'show-case_layout_record_action', route: module + '/:id/layout/:layout/:record(/:action)', callback: function(id, layout, record, action) { if (!this._moduleExists(module)) { return; } var opts = { module: module, modelId: id, layout: layout || 'record', action: record, record: action || 'detail' }; app.controller.loadView(opts); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var module = 'pmse_Project'; var routes = [ { name: 'pd_record_layout', route: module + '/:id/layout/:layout', callback: function(id, layout) { if (!app.router._moduleExists(module)) { return; } app.router.record(module, id, null, layout); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var module = 'pmse_Business_Rules'; var routes = [ { name: 'br_record_layout', route: module + '/:id/layout/:layout', callback: function(id, layout) { if (!app.router._moduleExists(module)) { return; } app.router.record(module, id, null, layout); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var module = 'pmse_Emails_Templates'; var routes = [ { name: 'et_record_layout', route: module + '/:id/layout/:layout', callback: function(id, layout) { if (!app.router._moduleExists(module)) { return; } app.router.record(module, id, null, layout); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'list', route: 'Users', callback: function() { let acls = app.user.getAcls(); if (app.user.get('type') === 'admin' || acls.Users.developer !== 'no') { app.router.list('Users'); } else { app.controller.loadView({layout: 'access-denied'}); } } }, { name: 'user-utilities-copy-content', route: ':Users/copy-content', callback: function(module) { app.controller.loadView({ layout: 'copy-content', module: module }); } }, { name: 'user-utilities-update-locale', route: ':Users/update-locale', callback: function(module) { app.controller.loadView({ layout: 'copy-user-settings', module: module }); } }, { name: 'create-group', route: 'Users/create/group', callback: function() { if (!app.controller.context.get('layout')) { app.controller.loadView({ module: 'Users', layout: 'records' }); } let newUserModel = app.data.createBean('Users', { is_admin: false, is_group: true, portal_only: false }); app.drawer.open({ layout: 'create', context: { module: 'Users', create: true, fromRouter: true, userType: 'group', model: newUserModel } }, function(context, model) { if (model && model.module === app.controller.context.get('module')) { app.controller.context.reloadData(); } }); } }, { name: 'create-portalapi', route: 'Users/create/portalapi', callback: function() { if (!app.controller.context.get('layout')) { app.controller.loadView({ module: 'Users', layout: 'records' }); } let newUserModel = app.data.createBean('Users', { is_admin: false, is_group: false, portal_only: true }); app.drawer.open({ layout: 'create', context: { module: 'Users', create: true, fromRouter: true, userType: 'portalapi', model: newUserModel } }, function(context, model) { if (model && model.module === app.controller.context.get('module')) { app.controller.context.reloadData(); } }); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'administration', route: 'Administration', callback: function() { app.controller.loadView({ layout: 'administration', module: 'Administration' }); } }, { name: 'relate-denormalization', route: ':Administration/denormalization', callback: function(module) { app.controller.loadView({ layout: 'config-drawer', module: module }); } }, { name: 'module-names-and-icons', route: ':Administration/module-names-and-icons', callback: function(module) { let prevLayout = app.controller.context.get('layout'); if (prevLayout && !_.contains(['login', 'bwc'], prevLayout)) { app.drawer.open({ layout: 'module-names-and-icons-drawer', context: { module: module, fromRouter: true } }); return; } app.controller.loadView({ layout: 'module-names-and-icons-drawer', module: module }); } }, { // route for Timeline config name: 'timeline-config', route: ':Administration/config/timeline/:target', callback: function(module, target) { app.controller.loadView({ layout: 'timeline-config', category: 'timeline', target: target, module: module }); } }, { // route for Config Framework name: 'admin-config', route: ':Administration/config/:category', callback: function(module, category) { var layout = app.metadata.getLayout(module, category + '-config') ? category + '-config' : 'config'; app.controller.loadView({ layout: layout, category: category, module: module }); } }, { name: 'drive-path', route: ':Administration/drive-path/:type', callback: function(module, type) { app.controller.loadView({ layout: 'drive-path', module: module, driveType: type, }); } }, { name: 'package-builder', route: ':Administration/package-builder', callback: function(module) { app.controller.loadView({ layout: 'package-builder', module: module }); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function() { var routes = [ { name: 'sg_index', route: 'Styleguide', callback: function() { app.controller.loadView({ module: 'Styleguide', layout: 'styleguide', chapter_name: 'home', content_name: null }); } }, { name: 'sg_module', route: 'Styleguide/:layout/:resource', callback: function(layout, resource) { var chapter_name = '', content_name = ''; switch (layout) { case 'docs': //route: "Styleguide/docs/base" //route: "Styleguide/docs/base-grid" case 'fields': //route: "Styleguide/fields/text" case 'views': //route: "Styleguide/views/list" chapter_name = layout; content_name = resource; break; case 'layout': //route: "Styleguide/layout/records" layout = resource; content_name = 'module'; break; default: app.logger.warn('Invalid route: ' + layout + '/' + resource); break; } app.controller.loadView({ module: 'Styleguide', layout: layout, chapter_name: chapter_name, content_name: content_name, skipFetch: true }); } } ]; app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { /* * Add the Dashboard module routes to Sugar's router */ app.events.on('router:init', function(router) { var routes = [ { name: 'dashboardCreate', route: 'Dashboards/create', callback: function() { app.error.handleHttpError({status: 404}); } }, { name: 'manageDashboards', route: 'Dashboards', callback: function(urlParams) { if (!this._moduleExists('Dashboards')) { return; } // This may be filled in with values from urlParams // Expected (optional) keys are moduleName and viewName var params = {}; if (urlParams) { // FIXME TY-1469: Use the URL splitter in the router // and remove this block of code. var paramsArray = urlParams.split('&'); _.each(paramsArray, function(paramPair) { var keyValueArray = paramPair.split('='); if (keyValueArray.length > 1) { params[keyValueArray[0]] = keyValueArray[1]; } }); } var moduleName = params.moduleName; var viewName = params.viewName; // Initialize the options for `app.controller.loadView` var viewOptions = { module: 'Dashboards', layout: 'records' }; // If `previousModule` is defined, we need to pre-apply a // filter to the dashboards list view. if (!_.isUndefined(moduleName)) { var initialFilter; var translatedModule = app.lang.getModuleName(moduleName, {plural: true}); var filterLabel; var filterOptions; var filterDef = { dashboard_module: [moduleName] }; // FIXME TY-1458: If we're here, then `previousLayout` // should also be defined. The `if` statement and its // contents would no longer be necessary. The `else` // contents would be the only portion remaining. if (_.isUndefined(viewName)) { initialFilter = 'module'; filterLabel = app.lang.get('LBL_FILTER_BY_MODULE', 'Dashboards', { module: translatedModule }); } else { initialFilter = 'module_and_layout'; filterDef.view_name = viewName; filterLabel = app.lang.get('LBL_FILTER_BY_MODULE_AND_VIEW', 'Dashboards', { module: translatedModule, view: app.lang.getAppListStrings('dashboard_view_name_list')[viewName] || viewName }); } filterOptions = new app.utils.FilterOptions(); filterOptions.setInitialFilter(initialFilter); filterOptions.setInitialFilterLabel(filterLabel); filterOptions.setFilterPopulate(filterDef); viewOptions.filterOptions = filterOptions.format(); } app.controller.loadView(viewOptions); } } ]; /* * Triggering the event on init will go over all those listeners * and add the routes to the router. */ app.router.addRoutes(routes); }); })(SUGAR.App); /* * Your installation or use of this SugarCRM file is subject to the applicable * terms available at * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/. * If you do not agree to all of the applicable terms or do not have the * authority to bind the entity as an authorized representative, then do not * install or use this SugarCRM file. * * Copyright (C) SugarCRM Inc. All rights reserved. */ (function(app) { app.events.on('router:init', function(router) { var routes = [ { name: 'docusign-settings', route: 'DocuSign/settings', callback: function(id) { app.controller.loadView({ module: 'DocuSignEnvelopes', layout: 'admin-settings', }); } } ]; /* * Triggering the event on init will go over all those listeners * and add the routes to the router. */ app.router.addRoutes(routes); }); })(SUGAR.App);