diff options
Diffstat (limited to 'sonar-server/src/main/js/navigator')
16 files changed, 2080 insertions, 0 deletions
diff --git a/sonar-server/src/main/js/navigator/filters/action-plan-filters.js b/sonar-server/src/main/js/navigator/filters/action-plan-filters.js new file mode 100644 index 00000000000..7503e344d2a --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/action-plan-filters.js @@ -0,0 +1,104 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/choice-filters'], function (Backbone, BaseFilters, ChoiceFilters) { + + return ChoiceFilters.ChoiceFilterView.extend({ + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.apply(this, arguments); + this.projectFilter = this.model.get('projectFilter'); + this.listenTo(this.projectFilter, 'change:value', this.onChangeProjectFilter); + this.onChangeProjectFilter(); + }, + + + onChangeProjectFilter: function() { + var projects = this.projectFilter.get('value'); + if (_.isArray(projects) && projects.length === 1) { + return this.fetchActionPlans(projects[0]); + } else { + return this.makeInactive(); + } + }, + + + showDetails: function() { + if (!this.$el.is('.navigator-filter-inactive')) { + ChoiceFilters.ChoiceFilterView.prototype.showDetails.apply(this, arguments); + } + }, + + + makeActive: function() { + this.model.set({ + inactive: false, + title: '' + }); + this.model.trigger('change:enabled'); + this.$el.removeClass('navigator-filter-inactive').prop('title', ''); + }, + + + makeInactive: function() { + this.model.set({ + inactive: true, + title: window.SS.phrases.actionPlanNotAvailable + }); + this.model.trigger('change:enabled'); + this.choices.reset([]); + this.detailsView.updateLists(); + this.detailsView.updateValue(); + this.$el.addClass('navigator-filter-inactive') + .prop('title', window.SS.phrases.actionPlanNotAvailable); + }, + + + fetchActionPlans: function(project) { + var that = this; + return jQuery.ajax({ + url: baseUrl + '/api/action_plans/search', + type: 'GET', + data: { project: project } + }).done(function(r) { + var nonClosedActionPlans = + _.sortBy(_.reject(r.actionPlans, function(plan) { + return plan.status === 'CLOSED'; + }), 'name'); + + that.choices.reset(nonClosedActionPlans.map(function(plan) { + return { + id: plan.key, + text: plan.name, + category: plan.fDeadLine + } + })); + _.each(that.model.get('choices'), function(v, k) { + that.choices.add(new Backbone.Model({ id: k, text: v })); + }); + var value = that.model.get('value'); + _.each(value, function(v) { + var cModel = that.choices.findWhere({ id: v }); + cModel.set('checked', true); + }); + that.detailsView.updateValue(); + that.render(); + + that.makeActive(); + }); + }, + + + restore: function(value) { + if (_.isString(value)) { + value = value.split(','); + } + + if (this.choices && value.length > 0) { + this.model.set({ value: value, enabled: true }); + this.onChangeProjectFilter(); + } else { + this.clear(); + } + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js b/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js new file mode 100644 index 00000000000..dd2e246d217 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js @@ -0,0 +1,465 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/choice-filters', 'common/handlebars-extensions'], function (Backbone, BaseFilters, ChoiceFilters) { + + var PAGE_SIZE = 100; + + + var Suggestions = Backbone.Collection.extend({ + comparator: 'checked', + + initialize: function() { + this.more = false; + this.page = 0; + }, + + + parse: function(r) { + this.more = r.more; + return r.results; + }, + + + fetch: function(options) { + this.data = _.extend({ + p: 1, + ps: PAGE_SIZE + }, options.data || {}); + + var settings = _.extend({}, options, { data: this.data }); + return Backbone.Collection.prototype.fetch.call(this, settings); + }, + + + fetchNextPage: function(options) { + if (this.more) { + this.data.p += 1; + var settings = _.extend({ remove: false }, options, { data: this.data }); + return this.fetch(settings); + } + return false; + } + + }); + + + + var UserSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/users/search?f=s2'; + } + + }); + + + + var ProjectSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/resources/search?f=s2&q=TRK&display_key=true'; + } + + }); + + + + var ComponentSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/resources/search?f=s2&qp=supportsGlobalDashboards&display_key=true'; + }, + + parse: function(r) { + this.more = r.more; + + // If results are divided into categories + if (r.results.length > 0 && r.results[0].children) { + var results = []; + _.each(r.results, function(category) { + _.each(category.children, function(child) { + child.category = category.text; + results.push(child); + }); + }); + return results; + } else { + return r.results; + } + } + + }); + + + + var AjaxSelectDetailsFilterView = ChoiceFilters.DetailsChoiceFilterView.extend({ + template: getTemplate('#ajax-select-filter-template'), + listTemplate: getTemplate('#choice-filter-template'), + + + render: function() { + ChoiceFilters.DetailsChoiceFilterView.prototype.render.apply(this, arguments); + + var that = this, + keyup = function(e) { + if (e.keyCode !== 38 && e.keyCode !== 40) { + that.search(); + } + }, + debouncedKeyup = _.debounce(keyup, 250), + scroll = function() { that.scroll(); }, + throttledScroll = _.throttle(scroll, 1000); + + this.$('.navigator-filter-search input') + .off('keyup keydown') + .on('keyup', debouncedKeyup) + .on('keydown', this.keydown); + + this.$('.choices') + .off('scroll') + .on('scroll', throttledScroll); + }, + + + search: function() { + var that = this; + this.query = this.$('.navigator-filter-search input').val(); + if (this.query.length > 1) { + this.$el.addClass('fetching'); + var selected = that.options.filterView.getSelected(); + this.options.filterView.choices.fetch({ + data: { + s: this.query, + ps: PAGE_SIZE + }, + success: function() { + selected.forEach(function(item) { + that.options.filterView.choices.unshift(item); + }); + _.each(that.model.get('choices'), function(v, k) { + that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); + }); + that.updateLists(); + that.$el.removeClass('fetching'); + } + }); + } else { + this.resetChoices(); + this.updateLists(); + } + }, + + + scroll: function() { + var that = this, + el = this.$('.choices'), + scrollBottom = el.scrollTop() >= el[0].scrollHeight - el.outerHeight(); + + if (scrollBottom) { + this.options.filterView.choices.fetchNextPage().done(function() { + that.updateLists(); + }); + } + }, + + + keydown: function(e) { + if (_([37, 38, 39, 40, 13]).indexOf(e.keyCode) !== -1) { + e.preventDefault(); + } + }, + + + resetChoices: function() { + var that = this; + this.options.filterView.choices.reset(this.options.filterView.choices.filter(function(item) { + return item.get('checked'); + })); + _.each(this.model.get('choices'), function(v, k) { + that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); + }); + }, + + + onShow: function() { + ChoiceFilters.DetailsChoiceFilterView.prototype.onShow.apply(this, arguments); + this.resetChoices(); + this.render(); + this.$('.navigator-filter-search input').focus(); + } + + }); + + + + var AjaxSelectFilterView = ChoiceFilters.ChoiceFilterView.extend({ + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + }, + + + isDefaultValue: function() { + return this.getSelected().length === 0; + }, + + + renderInput: function() { + var value = this.model.get('value') || [], + input = $j('<input>') + .prop('name', this.model.get('property')) + .prop('type', 'hidden') + .css('display', 'none') + .val(value.join()); + input.appendTo(this.$el); + }, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + + if (this.model.get('choices')) { + _.each(this.model.get('choices'), function(v, k) { + if (k[0] === '!') { + var x = _.findWhere(q, { key: k.substr(1) }); + if (x) { + if (!param) { + param = { value: k }; + } else { + param.value += ',' + k; + } + } + } + }); + } + + if (param && param.value) { + this.model.set('enabled', true); + this.restore(param.value, param); + } else { + this.clear(); + } + }, + + + restore: function(value, param) { + var that = this; + if (_.isString(value)) { + value = value.split(','); + } + + if (this.choices && value.length > 0) { + this.model.set({ value: value, enabled: true }); + + var opposite = _.filter(value, function(item) { + return item[0] === '!'; + }); + opposite.forEach(function(item) { + that.choices.add(new Backbone.Model({ + id: item, + text: that.model.get('choices')[item], + checked: true + })); + }); + + value = _.reject(value, function(item) { + return item[0] === '!'; + }); + if (_.isArray(param.text) && param.text.length === value.length) { + this.restoreFromText(value, param.text); + } else { + this.restoreByRequests(value); + } + } else { + this.clear(); + } + }, + + + restoreFromText: function(value, text) { + var that = this; + _.each(value, function(v, i) { + that.choices.add(new Backbone.Model({ + id: v, + text: text[i], + checked: true + })); + }); + this.onRestore(value); + }, + + + restoreByRequests: function(value) { + var that = this, + requests = _.map(value, function(v) { + return that.createRequest(v); + }); + + $j.when.apply($j, requests).done(function () { + that.onRestore(value); + }); + }, + + + onRestore: function() { + this.detailsView.updateLists(); + this.renderBase(); + }, + + + clear: function() { + this.model.unset('value'); + if (this.choices) { + this.choices.reset([]); + } + this.render(); + }, + + + createRequest: function() {} + + }); + + + + var ComponentFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + AjaxSelectFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + this.choices = new ComponentSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/resources', + type: 'GET', + data: { resource: v } + }) + .done(function (r) { + that.selection.add(new Backbone.Model({ + id: r[0].key, + text: r[0].name + })); + }); + } + + }); + + + + var ProjectFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.choices = new ProjectSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/resources', + type: 'GET', + data: { resource: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r[0].key, + text: r[0].name, + checked: true + })); + }); + } + + }); + + + + var AssigneeFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.choices = new UserSuggestions(); + }, + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/users/search', + type: 'GET', + data: { logins: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.users[0].login, + text: r.users[0].name + ' (' + r.users[0].login + ')', + checked: true + })); + }); + } + + }); + + + + var ReporterFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.selection = new UserSuggestions(); + this.choices = new UserSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/users/search', + type: 'GET', + data: { logins: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.users[0].login, + text: r.users[0].name + ' (' + r.users[0].login + ')', + checked: true + })); + }); + } + + }); + + + + /* + * Export public classes + */ + + return { + Suggestions: Suggestions, + AjaxSelectDetailsFilterView: AjaxSelectDetailsFilterView, + AjaxSelectFilterView: AjaxSelectFilterView, + ProjectFilterView: ProjectFilterView, + ComponentFilterView: ComponentFilterView, + AssigneeFilterView: AssigneeFilterView, + ReporterFilterView: ReporterFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/base-filters.js b/sonar-server/src/main/js/navigator/filters/base-filters.js new file mode 100644 index 00000000000..e07a3517bfe --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/base-filters.js @@ -0,0 +1,225 @@ +define(['backbone', 'backbone.marionette', 'common/handlebars-extensions'], function (Backbone, Marionette) { + + var Filter = Backbone.Model.extend({ + + defaults: { + enabled: true, + optional: false, + multiple: true, + placeholder: '' + } + + }); + + + + var Filters = Backbone.Collection.extend({ + model: Filter + }); + + + + var DetailsFilterView = Marionette.ItemView.extend({ + template: getTemplate('#base-details-filter-template'), + className: 'navigator-filter-details', + + + initialize: function() { + this.$el.on('click', function(e) { + e.stopPropagation(); + }); + }, + + + onShow: function() {}, + onHide: function() {} + }); + + + + var BaseFilterView = Marionette.ItemView.extend({ + template: getTemplate('#base-filter-template'), + className: 'navigator-filter', + + + events: function() { + return { + 'click': 'toggleDetails', + 'click .navigator-filter-disable': 'disable' + }; + }, + + + modelEvents: { + 'change:enabled': 'focus', + 'change:value': 'renderBase', + + // for more criteria filter + 'change:filters': 'render' + }, + + + initialize: function(options) { + Marionette.ItemView.prototype.initialize.apply(this, arguments); + + var detailsView = (options && options.detailsView) || DetailsFilterView; + this.detailsView = new detailsView({ + model: this.model, + filterView: this + }); + + this.model.view = this; + }, + + + attachDetailsView: function() { + this.detailsView.$el.detach().appendTo($j('body')); + }, + + + render: function() { + this.renderBase(); + + this.attachDetailsView(); + this.detailsView.render(); + + this.$el.toggleClass( + 'navigator-filter-disabled', + !this.model.get('enabled')); + + this.$el.toggleClass( + 'navigator-filter-optional', + this.model.get('optional')); + }, + + + renderBase: function() { + Marionette.ItemView.prototype.render.apply(this, arguments); + this.renderInput(); + }, + + + renderInput: function() {}, + + + focus: function() { + this.render(); +// this.showDetails(); + }, + + + toggleDetails: function(e) { + e.stopPropagation(); + if (this.$el.hasClass('active')) { + this.hideDetails(); + } else { + this.showDetails(); + } + }, + + + showDetails: function() { + this.registerShowedDetails(); + + var top = this.$el.offset().top + this.$el.outerHeight() - 1, + left = this.$el.offset().left; + + this.detailsView.$el.css({ top: top, left: left }).addClass('active'); + this.$el.addClass('active'); + this.detailsView.onShow(); + }, + + + registerShowedDetails: function() { + this.options.filterBarView.hideDetails(); + this.options.filterBarView.showedView = this; + }, + + + hideDetails: function() { + this.detailsView.$el.removeClass('active'); + this.$el.removeClass('active'); + this.detailsView.onHide(); + }, + + + isActive: function() { + return this.$el.is('.active'); + }, + + + renderValue: function() { + return this.model.get('value') || 'unset'; + }, + + + isDefaultValue: function() { + return true; + }, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + if (param && param.value) { + this.model.set('enabled', true); + this.restore(param.value, param); + } else { + this.clear(); + } + }, + + + restore: function(value) { + this.model.set({ value: value }, { silent: true }); + this.renderBase(); + }, + + + clear: function() { + this.model.unset('value'); + }, + + + disable: function(e) { + e.stopPropagation(); + this.hideDetails(); + this.options.filterBarView.hideDetails(); + this.model.set({ + enabled: false, + value: null + }); + }, + + + formatValue: function() { + var q = {}; + if (this.model.has('property') && this.model.has('value') && this.model.get('value')) { + q[this.model.get('property')] = this.model.get('value'); + } + return q; + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + value: this.renderValue(), + defaultValue: this.isDefaultValue() + }); + } + + }); + + + + /* + * Export public classes + */ + + return { + Filter: Filter, + Filters: Filters, + BaseFilterView: BaseFilterView, + DetailsFilterView: DetailsFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/checkbox-filters.js b/sonar-server/src/main/js/navigator/filters/checkbox-filters.js new file mode 100644 index 00000000000..025ce9dc649 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/checkbox-filters.js @@ -0,0 +1,50 @@ +define(['backbone', 'backbone.marionette', 'navigator/filters/base-filters', 'common/handlebars-extensions'], function (Backbone, Marionette, BaseFilters) { + + return BaseFilters.BaseFilterView.extend({ + template: getTemplate('#checkbox-filter-template'), + className: 'navigator-filter navigator-filter-inline', + + + events: function() { + return { + 'click .navigator-filter-disable': 'disable' + }; + }, + + + showDetails: function() {}, + + + renderInput: function() { + if (this.model.get('enabled')) { + $j('<input>') + .prop('name', this.model.get('property')) + .prop('type', 'checkbox') + .prop('value', 'true') + .prop('checked', true) + .css('display', 'none') + .appendTo(this.$el); + } + }, + + + renderValue: function() { + return this.model.get('value') || false; + }, + + + isDefaultValue: function() { + return false; + }, + + + restore: function(value) { + this.model.set({ + value: value, + enabled: true + }); + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/choice-filters.js b/sonar-server/src/main/js/navigator/filters/choice-filters.js new file mode 100644 index 00000000000..cf74a21562e --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/choice-filters.js @@ -0,0 +1,363 @@ +define(['handlebars', 'navigator/filters/base-filters', 'common/handlebars-extensions'], function (Handlebars, BaseFilters) { + + var DetailsChoiceFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#choice-filter-template'), + itemTemplate: getTemplate('#choice-filter-item-template'), + + + events: function() { + return { + 'change input[type=checkbox]': 'onCheck' + }; + }, + + + render: function() { + BaseFilters.DetailsFilterView.prototype.render.apply(this, arguments); + this.updateLists(); + }, + + + renderList: function(collection, selector) { + var that = this, + container = this.$(selector); + + container.empty().toggleClass('hidden', collection.length === 0); + collection.each(function(item) { + container.append(that.itemTemplate(item.toJSON())); + }); + }, + + + updateLists: function() { + var choices = new Backbone.Collection(this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!'; + })), + opposite = new Backbone.Collection(this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!'; + })); + + this.renderList(choices, '.choices'); + this.renderList(opposite, '.opposite'); + + var current = this.currentChoice || 0; + this.updateCurrent(current); + }, + + + onCheck: function(e) { + var checkbox = jQuery(e.target), + id = checkbox.val(), + checked = checkbox.prop('checked'); + + if (this.model.get('multiple')) { + if (checkbox.closest('.opposite').length > 0) { + this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!' + }).forEach(function(item) { + item.set('checked', false); + }); + } else { + this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!' + }).forEach(function(item) { + item.set('checked', false); + }); + } + } else { + this.options.filterView.choices.each(function(item) { + item.set('checked', false); + }); + } + + this.options.filterView.choices.get(id).set('checked', checked); + this.updateValue(); + this.updateLists(); + }, + + + updateValue: function() { + this.model.set('value', this.options.filterView.getSelected().map(function(m) { + return m.get('id'); + })); + }, + + + updateCurrent: function(index) { + this.currentChoice = index; + this.$('label').removeClass('current') + .eq(this.currentChoice).addClass('current'); + }, + + + onShow: function() { + this.bindedOnKeyDown = _.bind(this.onKeyDown, this); + $j('body').on('keydown', this.bindedOnKeyDown); + }, + + + onHide: function() { + $j('body').off('keydown', this.bindedOnKeyDown); + }, + + + onKeyDown: function(e) { + switch (e.keyCode) { + case 38: + e.preventDefault(); + this.selectPrevChoice(); + break; + case 40: + e.preventDefault(); + this.selectNextChoice(); + break; + case 13: + e.preventDefault(); + this.selectCurrent(); + break; + } + }, + + + selectNextChoice: function() { + if (this.$('label').length > this.currentChoice + 1) { + this.updateCurrent(this.currentChoice + 1); + this.scrollNext(); + } + }, + + + scrollNext: function() { + var currentLabel = this.$('label').eq(this.currentChoice); + if (currentLabel.length > 0) { + var list = currentLabel.closest('ul'), + labelPos = currentLabel.offset().top - list.offset().top + list.scrollTop(), + deltaScroll = labelPos - list.height() + currentLabel.outerHeight(); + + if (deltaScroll > 0) { + list.scrollTop(deltaScroll); + } + } + }, + + + selectPrevChoice: function() { + if (this.currentChoice > 0) { + this.updateCurrent(this.currentChoice - 1); + this.scrollPrev(); + } + }, + + + scrollPrev: function() { + var currentLabel = this.$('label').eq(this.currentChoice); + if (currentLabel.length > 0) { + var list = currentLabel.closest('ul'), + labelPos = currentLabel.offset().top - list.offset().top; + + if (labelPos < 0) { + list.scrollTop(list.scrollTop() + labelPos); + } + } + }, + + + selectCurrent: function() { + this.$('label').eq(this.currentChoice).click(); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + choices: new Backbone.Collection(this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!'; + })).toJSON(), + opposite: new Backbone.Collection(this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!'; + })).toJSON() + }); + } + + }); + + + + var ChoiceFilterView = BaseFilters.BaseFilterView.extend({ + + initialize: function(options) { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: (options && options.detailsView) ? options.detailsView : DetailsChoiceFilterView + }); + + var index = 0, + icons = this.model.get('choiceIcons'); + + this.choices = new Backbone.Collection( + _.map(this.model.get('choices'), function(value, key) { + var model = new Backbone.Model({ + id: key, + text: value, + checked: false, + index: index++ + }); + + if (icons && icons[key]) { + model.set('icon', icons[key]); + } + + return model; + }), { comparator: 'index' } + ); + }, + + + getSelected: function() { + return this.choices.filter(function(m) { + return m.get('checked'); + }); + }, + + + renderInput: function() { + var input = $j('<select>') + .prop('name', this.model.get('property')) + .prop('multiple', true) + .css('display', 'none'); + this.choices.each(function(item) { + var option = $j('<option>') + .prop('value', item.get('id')) + .prop('selected', item.get('checked')) + .text(item.get('text')); + option.appendTo(input); + }); + input.appendTo(this.$el); + }, + + + renderValue: function() { + var value = this.getSelected().map(function(item) { + return item.get('text'); + }), + defaultValue = this.model.has('defaultValue') ? + this.model.get('defaultValue') : + this.model.get('multiple') ? t('all') : t('any'); + + return this.isDefaultValue() ? defaultValue : value.join(', '); + }, + + + isDefaultValue: function() { + var selected = this.getSelected(); + return selected.length === 0; + }, + + + disable: function() { + this.choices.each(function(item) { + item.set('checked', false); + }); + BaseFilters.BaseFilterView.prototype.disable.apply(this, arguments); + }, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + + if (this.choices) { + this.choices.forEach(function(item) { + if (item.get('id')[0] === '!') { + var x = _.findWhere(q, { key: item.get('id').substr(1) }); + if (x) { + if (!param) { + param = { value: item.get('id') }; + } else { + param.value += ',' + item.get('id'); + } + } + } + }); + } + + if (param && param.value) { + this.model.set('enabled', true); + this.restore(param.value, param); + } else { + this.clear(); + } + }, + + + restore: function(value) { + if (_.isString(value)) { + value = value.split(','); + } + + if (this.choices && value.length > 0) { + var that = this; + + that.choices.each(function(item) { + item.set('checked', false); + }); + + _.each(value, function(v) { + var cModel = that.choices.findWhere({ id: v }); + cModel.set('checked', true); + }); + + this.model.set({ + value: value, + enabled: true + }); + + this.render(); + } else { + this.clear(); + } + }, + + + clear: function() { + if (this.choices) { + this.choices.each(function(item) { + item.set('checked', false); + }); + } + this.model.unset('value'); + this.detailsView.render(); + if (this.detailsView.updateCurrent) { + this.detailsView.updateCurrent(0); + } + }, + + + formatValue: function() { + var q = {}; + if (this.model.has('property') && this.model.has('value') && this.model.get('value').length > 0) { + var opposite = _.filter(this.model.get('value'), function(item) { + return item[0] === '!'; + }); + if (opposite.length > 0) { + opposite.forEach(function(item) { + q[item.substr(1)] = false; + }); + } else { + q[this.model.get('property')] = this.model.get('value').join(','); + } + } + return q; + } + + }); + + + + /* + * Export public classes + */ + + return { + DetailsChoiceFilterView: DetailsChoiceFilterView, + ChoiceFilterView: ChoiceFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/context-filters.js b/sonar-server/src/main/js/navigator/filters/context-filters.js new file mode 100644 index 00000000000..ec71ff3f774 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/context-filters.js @@ -0,0 +1,66 @@ +define(['backbone'], function (Backbone) { + + return Backbone.View.extend({ + + initialize: function() { + this.model.view = this; + }, + + + render: function() { + this.$el.hide(); + }, + + + renderBase: function() {}, + renderInput: function() {}, + focus: function() {}, + showDetails: function() {}, + registerShowedDetails: function() {}, + hideDetails: function() {}, + isActive: function() {}, + renderValue: function() {}, + isDefaultValue: function() {}, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + if (param && param.value) { + this.restore(param.value); + } else { + this.clear(); + } + }, + + + restore: function(value) { + this.model.set({ value: value }); + }, + + + clear: function() { + this.model.unset('value'); + }, + + + disable: function(e) { + e.stopPropagation(); + this.hideDetails(); + this.options.filterBarView.hideDetails(); + this.model.set({ + enabled: false, + value: null + }); + }, + + + formatValue: function() { + var q = {}; + if (this.model.has('property') && this.model.has('value') && this.model.get('value')) { + q[this.model.get('property')] = this.model.get('value'); + } + return q; + } + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/date-filter-view.coffee b/sonar-server/src/main/js/navigator/filters/date-filter-view.coffee new file mode 100644 index 00000000000..da9fc81679b --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/date-filter-view.coffee @@ -0,0 +1,11 @@ +define [ + 'navigator/filters/string-filters' +], ( + StringFilterView +) -> + + class DateFilterView extends StringFilterView + + render: -> + super + @detailsView.$('input').prop 'placeholder', '1970-01-31' diff --git a/sonar-server/src/main/js/navigator/filters/date-filter-view.js b/sonar-server/src/main/js/navigator/filters/date-filter-view.js new file mode 100644 index 00000000000..30b6a7df873 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/date-filter-view.js @@ -0,0 +1,24 @@ +(function() { + var __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + define(['navigator/filters/string-filters'], function(StringFilterView) { + var DateFilterView; + return DateFilterView = (function(_super) { + __extends(DateFilterView, _super); + + function DateFilterView() { + return DateFilterView.__super__.constructor.apply(this, arguments); + } + + DateFilterView.prototype.render = function() { + DateFilterView.__super__.render.apply(this, arguments); + return this.detailsView.$('input').prop('placeholder', '1970-01-31'); + }; + + return DateFilterView; + + })(StringFilterView); + }); + +}).call(this); diff --git a/sonar-server/src/main/js/navigator/filters/favorite-filters.js b/sonar-server/src/main/js/navigator/filters/favorite-filters.js new file mode 100644 index 00000000000..a5c8134b07d --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/favorite-filters.js @@ -0,0 +1,79 @@ +define(['backbone', 'backbone.marionette', 'navigator/filters/base-filters', 'navigator/filters/choice-filters', 'common/handlebars-extensions'], function (Backbone, Marionette, BaseFilters, ChoiceFilters) { + + var DetailsFavoriteFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#favorite-details-filter-template'), + + + events: { + 'click label[data-id]': 'applyFavorite', + 'click .manage label': 'manage' + }, + + + applyFavorite: function(e) { + var id = $j(e.target).data('id'); + window.location = baseUrl + this.model.get('favoriteUrl') + '/' + id; + }, + + + manage: function() { + window.location = baseUrl + this.model.get('manageUrl'); + }, + + + serializeData: function() { + var choices = this.model.get('choices'), + choicesArray = + _.sortBy( + _.map(choices, function (v, k) { + return { v: v, k: k }; + }), + 'v'); + + return _.extend({}, this.model.toJSON(), { + choicesArray: choicesArray + }); + } + + }); + + + + var FavoriteFilterView = ChoiceFilters.ChoiceFilterView.extend({ + template: getTemplate('#favorite-filter-template'), + className: 'navigator-filter navigator-filter-favorite', + + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { + detailsView: DetailsFavoriteFilterView + }); + }, + + + renderValue: function() { + return ''; + }, + + + renderInput: function() {}, + + + isDefaultValue: function() { + return false; + } + + }); + + + + /* + * Export public classes + */ + + return { + DetailsFavoriteFilterView: DetailsFavoriteFilterView, + FavoriteFilterView: FavoriteFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/filter-bar.js b/sonar-server/src/main/js/navigator/filters/filter-bar.js new file mode 100644 index 00000000000..a859c721382 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/filter-bar.js @@ -0,0 +1,107 @@ +define( + [ + 'backbone.marionette', + 'navigator/filters/base-filters', + 'navigator/filters/more-criteria-filters', + 'navigator/filters/favorite-filters', + 'common/handlebars-extensions' + ], + function (Marionette, BaseFilters) { + + return Marionette.CompositeView.extend({ + template: getTemplate('#filter-bar-template'), + itemViewContainer: '.navigator-filters-list', + + + collectionEvents: { + 'change:enabled': 'changeEnabled' + }, + + + getItemView: function (item) { + return item.get('type') || BaseFilters.BaseFilterView; + }, + + + itemViewOptions: function () { + return { + filterBarView: this, + app: this.options.app + }; + }, + + + initialize: function () { + Marionette.CompositeView.prototype.initialize.apply(this, arguments); + + var that = this; + $j('body').on('click', function () { + that.hideDetails(); + }); + this.addMoreCriteriaFilter(); + }, + + + addMoreCriteriaFilter: function() { + var disabledFilters = this.collection.where({ enabled: false }); + if (disabledFilters.length > 0) { + this.moreCriteriaFilter = new BaseFilters.Filter({ + type: require('navigator/filters/more-criteria-filters').MoreCriteriaFilterView, + enabled: true, + optional: false, + filters: disabledFilters + }); + this.collection.add(this.moreCriteriaFilter); + } + }, + + + onAfterItemAdded: function (itemView) { + if (itemView.model.get('type') === require('navigator/filters/favorite-filters').FavoriteFilterView) { + jQuery('.navigator-header').addClass('navigator-header-favorite'); + } + }, + + + restoreFromQuery: function (q) { + this.collection.each(function (item) { + item.set('enabled', !item.get('optional')); + item.view.clear(); + item.view.restoreFromQuery(q); + }); + }, + + + hideDetails: function () { + if (_.isObject(this.showedView)) { + this.showedView.hideDetails(); + } + }, + + + enableFilter: function (id) { + var filter = this.collection.get(id), + filterView = filter.view; + + filterView.$el.detach().insertBefore(this.$('.navigator-filter-more-criteria')); + filter.set('enabled', true); + filterView.showDetails(); + }, + + + changeEnabled: function () { + var disabledFilters = _.reject(this.collection.where({ enabled: false }), function (filter) { + return filter.get('type') === require('navigator/filters/more-criteria-filters').MoreCriteriaFilterView; + }); + + if (disabledFilters.length === 0) { + this.moreCriteriaFilter.set({ enabled: false }, { silent: true }); + } else { + this.moreCriteriaFilter.set({ enabled: true }, { silent: true }); + } + this.moreCriteriaFilter.set('filters', disabledFilters); + } + + }); + + }); diff --git a/sonar-server/src/main/js/navigator/filters/metric-filters.js b/sonar-server/src/main/js/navigator/filters/metric-filters.js new file mode 100644 index 00000000000..f680d0e06e1 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/metric-filters.js @@ -0,0 +1,169 @@ +define(['navigator/filters/base-filters', 'common/handlebars-extensions'], function (BaseFilters) { + + var DetailsMetricFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#metric-filter-template'), + + + events: { + 'change :input': 'inputChanged' + }, + + + inputChanged: function() { + var value = { + metric: this.$('[name=metric]').val(), + metricText: this.$('[name=metric] option:selected').text(), + period: this.$('[name=period]').val(), + periodText: this.$('[name=period] option:selected').text(), + op: this.$('[name=op]').val(), + opText: this.$('[name=op] option:selected').text(), + val: this.$('[name=val]').val(), + valText: this.$('[name=val]').originalVal() + }; + this.updateDataType(value); + this.model.set('value', value); + }, + + + updateDataType: function(value) { + var metric = _.find(window.SS.metrics, function(m) { + return m.metric.name === value.metric; + }); + if (metric) { + this.$('[name=val]').data('type', metric.metric.val_type); + switch (metric.metric.val_type) { + case 'WORK_DUR': + this.$('[name=val]').prop('placeholder', '1d 7h 59min'); + break; + case 'RATING': + this.$('[name=val]').prop('placeholder', 'A'); + break; + } + } + }, + + + onRender: function() { + var value = this.model.get('value') || {}; + this.$('[name=metric]').val(value.metric).select2({ + width: '100%', + placeholder: window.SS.phrases.metric + }); + this.$('[name=period]').val(value.period || 0).select2({ + width: '100%', + minimumResultsForSearch: 100 + }); + this.$('[name=op]').val(value.op || 'eq').select2({ + width: '60px', + placeholder: '=', + minimumResultsForSearch: 100 + }); + this.updateDataType(value); + this.$('[name=val]').val(value.val); + this.inputChanged(); + }, + + + onShow: function() { + var select = this.$('[name=metric]'); + if (!select.val()) { + select.select2('open'); + } + } + + }); + + + + return BaseFilters.BaseFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsMetricFilterView + }); + + this.groupMetrics(); + }, + + + groupMetrics: function() { + var metrics = _.map(this.model.get('metrics'), function (metric) { + return metric.metric; + }), + groupedMetrics = + _.sortBy( + _.map( + _.groupBy(metrics, 'domain'), + function (metrics, domain) { + return { + domain: domain, + metrics: _.sortBy(metrics, 'short_name') + }; + }), + 'domain' + ); + this.model.set('groupedMetrics', groupedMetrics); + }, + + + renderValue: function() { + return this.isDefaultValue() ? + window.SS.phrases.notSet : + this.model.get('value').metricText + ' ' + this.model.get('value').opText + ' ' + this.model.get('value').valText; + }, + + + renderInput: function() { + var that = this, + value = this.model.get('value'); + + if (_.isObject(value) && value.metric && value.op && value.val) { + _.each(['metric', 'period', 'op', 'val'], function(key) { + var v = value[key]; + if (key === 'period' && v === '0') { + v = ''; + } + + $j('<input>') + .prop('name', that.model.get('property') + '_' + key) + .prop('type', 'hidden') + .css('display', 'none') + .val(v) + .appendTo(that.$el); + }); + } + }, + + + isDefaultValue: function() { + var value = this.model.get('value'); + if (!_.isObject(value)) { + return true; + } + return !(value.metric && value.op && value.val); + }, + + + restoreFromQuery: function(q) { + var that = this, + value = {}; + _.each(['metric', 'period', 'op', 'val'], function(p) { + var property = that.model.get('property') + '_' + p, + pValue = _.findWhere(q, { key: property }); + + if (pValue && pValue.value) { + value[p] = pValue.value; + } + }); + + if (value && value.metric && value.op && value.val) { + this.model.set({ + value: value, + enabled: true + }); + } + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js b/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js new file mode 100644 index 00000000000..755c4bbfb42 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js @@ -0,0 +1,70 @@ +define(['navigator/filters/base-filters', 'navigator/filters/choice-filters', 'common/handlebars-extensions'], function (BaseFilters, ChoiceFilters) { + + var DetailsMoreCriteriaFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#more-criteria-details-filter-template'), + + + events: { + 'click label[data-id]:not(.inactive)': 'enableFilter' + }, + + + enableFilter: function(e) { + var id = $j(e.target).data('id'); + this.model.view.options.filterBarView.enableFilter(id); + this.model.view.hideDetails(); + }, + + + serializeData: function() { + var filters = this.model.get('filters').map(function(filter) { + return _.extend(filter.toJSON(), { id: filter.cid }); + }), + uniqueFilters = _.unique(filters, function(filter) { + return filter.name; + }); + return _.extend(this.model.toJSON(), { filters: uniqueFilters }); + } + + }); + + + + var MoreCriteriaFilterView = ChoiceFilters.ChoiceFilterView.extend({ + template: getTemplate('#more-criteria-filter-template'), + className: 'navigator-filter navigator-filter-more-criteria', + + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { + detailsView: DetailsMoreCriteriaFilterView + }); + }, + + + renderValue: function() { + return ''; + }, + + + renderInput: function() {}, + + + isDefaultValue: function() { + return false; + } + + }); + + + + /* + * Export public classes + */ + + return { + DetailsMoreCriteriaFilterView: DetailsMoreCriteriaFilterView, + MoreCriteriaFilterView: MoreCriteriaFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/range-filters.js b/sonar-server/src/main/js/navigator/filters/range-filters.js new file mode 100644 index 00000000000..910d37c7fa5 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/range-filters.js @@ -0,0 +1,195 @@ +define(['navigator/filters/base-filters', 'common/handlebars-extensions'], function (BaseFilters) { + + var DetailsRangeFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#range-filter-template'), + + + events: { + 'change input': 'change' + }, + + + change: function() { + var value = {}, + valueFrom = this.$('input').eq(0).val(), + valueTo = this.$('input').eq(1).val(); + + if (valueFrom.length > 0) { + value[this.model.get('propertyFrom')] = valueFrom; + } + + if (valueTo.length > 0) { + value[this.model.get('propertyTo')] = valueTo; + } + + this.model.set('value', value); + }, + + + populateInputs: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + this.$('input').eq(0).val(valueFrom || ''); + this.$('input').eq(1).val(valueTo || ''); + }, + + + onShow: function() { + this.$(':input:first').focus(); + } + + }); + + + + var RangeFilterView = BaseFilters.BaseFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsRangeFilterView + }); + }, + + + renderValue: function() { + if (!this.isDefaultValue()) { + var value = _.values(this.model.get('value')); + return value.join(' — '); + } else { + return window.SS.phrases.any; + } + }, + + + renderInput: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + $j('<input>') + .prop('name', propertyFrom) + .prop('type', 'hidden') + .css('display', 'none') + .val(valueFrom || '') + .appendTo(this.$el); + + $j('<input>') + .prop('name', propertyTo) + .prop('type', 'hidden') + .css('display', 'none') + .val(valueTo || '') + .appendTo(this.$el); + }, + + + isDefaultValue: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + return !valueFrom && !valueTo; + }, + + + restoreFromQuery: function(q) { + var paramFrom = _.findWhere(q, { key: this.model.get('propertyFrom') }), + paramTo = _.findWhere(q, { key: this.model.get('propertyTo') }), + value = {}; + + if ((paramFrom && paramFrom.value) || (paramTo && paramTo.value)) { + if (paramFrom && paramFrom.value) { + value[this.model.get('propertyFrom')] = paramFrom.value; + } + + if (paramTo && paramTo.value) { + value[this.model.get('propertyTo')] = paramTo.value; + } + + this.model.set({ + value: value, + enabled: true + }); + + this.detailsView.populateInputs(); + } + }, + + + restore: function(value) { + if (this.choices && this.selection && value.length > 0) { + var that = this; + this.choices.add(this.selection.models); + this.selection.reset([]); + + _.each(value, function(v) { + var cModel = that.choices.findWhere({ id: v }); + + if (cModel) { + that.selection.add(cModel); + that.choices.remove(cModel); + } + }); + + this.detailsView.updateLists(); + + this.model.set({ + value: value, + enabled: true + }); + } + }, + + + formatValue: function() { + return this.model.get('value'); + }, + + + clear: function() { + this.model.unset('value'); + this.detailsView.render(); + } + + }); + + + + var DateRangeFilterView = RangeFilterView.extend({ + + render: function() { + RangeFilterView.prototype.render.apply(this, arguments); + this.detailsView.$('input').prop('placeholder', '1970-01-31'); + }, + + + renderValue: function() { + if (!this.isDefaultValue()) { + var value = _.values(this.model.get('value')); + return value.join(' — '); + } else { + return window.SS.phrases.anytime; + } + } + + }); + + + + /* + * Export public classes + */ + + return { + RangeFilterView: RangeFilterView, + DateRangeFilterView: DateRangeFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/read-only-filters.js b/sonar-server/src/main/js/navigator/filters/read-only-filters.js new file mode 100644 index 00000000000..5a203e5214f --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/read-only-filters.js @@ -0,0 +1,25 @@ +define(['backbone', 'navigator/filters/base-filters'], function (Backbone, BaseFilters) { + + return BaseFilters.BaseFilterView.extend({ + className: 'navigator-filter navigator-filter-read-only', + + + events: { + 'click .navigator-filter-disable': 'disable' + }, + + + isDefaultValue: function() { + return false; + }, + + + renderValue: function() { + var value = this.model.get('value'), + format = this.model.get('format'); + return value ? (format ? format(value) : value) : ''; + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/rule-filters.js b/sonar-server/src/main/js/navigator/filters/rule-filters.js new file mode 100644 index 00000000000..aed4b8d894a --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/rule-filters.js @@ -0,0 +1,50 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/ajax-select-filters'], function (Backbone, BaseFilters, AjaxSelectFilters) { + + var RuleSuggestions = AjaxSelectFilters.Suggestions.extend({ + + url: function() { + return baseUrl + '/api/rules/list'; + }, + + + parse: function(r) { + this.more = r.more; + return r.results.map(function(r) { + return { id: r.key, text: r.name, category: r.language }; + }); + } + + }); + + return AjaxSelectFilters.AjaxSelectFilterView.extend({ + + initialize: function() { + AjaxSelectFilters.AjaxSelectFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectFilters.AjaxSelectDetailsFilterView + }); + + this.choices = new RuleSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return jQuery + .ajax({ + url: baseUrl + '/api/rules/show', + type: 'GET', + data: { key: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.rule.key, + text: r.rule.name, + category: r.rule.language, + checked: true + })); + }); + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/string-filters.js b/sonar-server/src/main/js/navigator/filters/string-filters.js new file mode 100644 index 00000000000..2e1278f350f --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/string-filters.js @@ -0,0 +1,77 @@ +define(['navigator/filters/base-filters', 'common/handlebars-extensions'], function (BaseFilters) { + + var DetailsStringFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#string-filter-template'), + + + events: { + 'change input': 'change' + }, + + + change: function(e) { + this.model.set('value', $j(e.target).val()); + }, + + + onShow: function() { + BaseFilters.DetailsFilterView.prototype.onShow.apply(this, arguments); + this.$(':input').focus(); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + value: this.model.get('value') || '' + }); + } + + }); + + + + return BaseFilters.BaseFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsStringFilterView + }); + }, + + + renderValue: function() { + return this.isDefaultValue() ? '—' : this.model.get('value'); + }, + + + renderInput: function() { + $j('<input>') + .prop('name', this.model.get('property')) + .prop('type', 'hidden') + .css('display', 'none') + .val(this.model.get('value') || '') + .appendTo(this.$el); + }, + + + isDefaultValue: function() { + return !this.model.get('value'); + }, + + + restore: function(value) { + this.model.set({ + value: value, + enabled: true + }); + }, + + + clear: function() { + this.model.unset('value'); + this.detailsView.render(); + } + + }); + +}); |