Moved to regions for quality profile editor

This commit is contained in:
Mark McDowall 2014-01-29 20:45:30 -08:00
parent 75decad601
commit 191db1c541
17 changed files with 243 additions and 181 deletions

View File

@ -137,6 +137,7 @@
<Compile Include="Missing\MissingModule.cs" />
<Compile Include="Config\NamingSampleResource.cs" />
<Compile Include="NzbDroneRestModuleWithSignalR.cs" />
<Compile Include="Qualities\QualityProfileValidation.cs" />
<Compile Include="Queue\QueueModule.cs" />
<Compile Include="Queue\QueueResource.cs" />
<Compile Include="ResourceChangeMessage.cs" />

View File

@ -1,7 +1,6 @@
using System.Collections.Generic;
using NzbDrone.Core.Qualities;
using NzbDrone.Api.Mapping;
using System.Linq;
using FluentValidation;
namespace NzbDrone.Api.Qualities
@ -9,27 +8,19 @@ namespace NzbDrone.Api.Qualities
public class QualityProfileModule : NzbDroneRestModule<QualityProfileResource>
{
private readonly IQualityProfileService _qualityProfileService;
private readonly IQualityDefinitionService _qualityDefinitionService;
public QualityProfileModule(IQualityProfileService qualityProfileService,
IQualityDefinitionService qualityDefinitionService)
public QualityProfileModule(IQualityProfileService qualityProfileService)
: base("/qualityprofiles")
{
_qualityProfileService = qualityProfileService;
_qualityDefinitionService = qualityDefinitionService;
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Cutoff).NotNull();
SharedValidator.RuleFor(c => c.Items).NotEmpty();
SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();//.SetValidator(new AllowedValidator<QualityProfileItemResource>());
GetResourceAll = GetAll;
GetResourceById = GetById;
UpdateResource = Update;
CreateResource = Create;
DeleteResource = DeleteProfile;
}

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Validators;
namespace NzbDrone.Api.Qualities
{
public static class QualityProfileValidation
{
public static IRuleBuilderOptions<T, IList<QualityProfileItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<QualityProfileItemResource>> ruleBuilder)
{
ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new AllowedValidator<T>());
}
}
public class AllowedValidator<T> : PropertyValidator
{
public AllowedValidator()
: base("Must contain at least one allowed quality")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
var list = context.PropertyValue as IList<QualityProfileItemResource>;
if (list == null)
{
return false;
}
if (!list.Any(c => c.Allowed))
{
return false;
}
return true;
}
}
}

View File

@ -4,6 +4,6 @@ define(
'marionette'
], function (Marionette) {
return Marionette.ItemView.extend({
template : 'Settings/Quality/Profile/EditQualityProfileItemViewTemplate'
template : 'Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate'
});
});

View File

@ -0,0 +1,92 @@
'use strict';
define(
[
'underscore',
'vent',
'marionette',
'backbone',
'Settings/Quality/Profile/Edit/EditQualityProfileItemView',
'Settings/Quality/Profile/Edit/QualitySortableCollectionView',
'Settings/Quality/Profile/Edit/EditQualityProfileView',
'Config'
], function (_, vent, Marionette, Backbone, EditQualityProfileItemView, QualitySortableCollectionView, EditQualityProfileView, Config) {
return Marionette.Layout.extend({
template: 'Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate',
regions: {
fields : '#x-fields',
qualities: '#x-qualities'
},
events: {
'click .x-save': '_saveQualityProfile'
},
initialize: function (options) {
this.profileCollection = options.profileCollection;
this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse());
},
onShow: function () {
this.fieldsView = new EditQualityProfileView({ model: this.model });
this._showFieldsView();
this.sortableListView = new QualitySortableCollectionView({
selectable : true,
selectMultiple : true,
clickToSelect : true,
clickToToggle : true,
sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false),
sortableOptions : {
handle: '.x-drag-handle'
},
collection: this.itemsCollection,
model : this.model
});
this.sortableListView.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; }));
this.qualities.show(this.sortableListView);
this.listenTo(this.sortableListView, 'selectionChanged', this._selectionChanged);
this.listenTo(this.sortableListView, 'sortStop', this._updateModel);
},
_selectionChanged: function(newSelectedModels, oldSelectedModels) {
var addedModels = _.difference(newSelectedModels, oldSelectedModels);
var removeModels = _.difference(oldSelectedModels, newSelectedModels);
_.each(removeModels, function(item) { item.set('allowed', false); });
_.each(addedModels, function(item) { item.set('allowed', true); });
this._updateModel();
},
_updateModel: function() {
this.model.set('items', this.itemsCollection.toJSON().reverse());
this._showFieldsView();
},
_saveQualityProfile: function () {
var self = this;
var cutoff = this.fieldsView.getCutoff();
this.model.set('cutoff', cutoff);
var promise = this.model.save();
if (promise) {
promise.done(function () {
self.profileCollection.add(self.model, { merge: true });
vent.trigger(vent.Commands.CloseModalCommand);
});
}
},
_showFieldsView: function () {
this.fields.show(this.fieldsView);
}
});
});

View File

@ -0,0 +1,29 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit</h3>
{{else}}
<h3>Add</h3>
{{/if}}
</div>
<div class="modal-body">
<div class="form-horizontal">
<div id="x-fields"></div>
<div class="control-group">
<label class="control-label">Qualities</label>
<div class="controls qualities-controls">
<span id="x-qualities"></span>
<span class="help-inline">
<i class="icon-question-sign" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/>
</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-primary x-save">save</button>
</div>

View File

@ -0,0 +1,26 @@
'use strict';
define(
[
'underscore',
'marionette',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView'
], function (_, Marionette, AsModelBoundView, AsValidatedView) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate',
ui: {
cutoff : '.x-cutoff'
},
getCutoff: function () {
var self = this;
return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)});
}
});
AsValidatedView.call(view);
return AsModelBoundView.call(view);
});

View File

@ -0,0 +1,21 @@
<div class="control-group">
<label class="control-label">Name</label>
<div class="controls">
<input type="text" name="name">
</div>
</div>
<div class="control-group">
<label class="control-label">Cutoff</label>
<div class="controls">
<select class="x-cutoff" name="cutoff.id" validation-name="cutoff">
{{#eachReverse items}}
{{#if allowed}}
<option value="{{quality.id}}">{{quality.name}}</option>
{{/if}}
{{/eachReverse}}
</select>
<span class="help-inline">
<i class="icon-question-sign" title="Once this quality is reached NzbDrone will no longer download episodes"/>
</span>
</div>
</div>

View File

@ -0,0 +1,24 @@
'use strict';
define(
[
'backbone.collectionview',
'Settings/Quality/Profile/Edit/EditQualityProfileItemView'
], function (BackboneSortableCollectionView, EditQualityProfileItemView) {
return BackboneSortableCollectionView.extend({
className: 'qualities',
modelView: EditQualityProfileItemView,
attributes: {
'validation-name': 'items'
},
events: {
'click li, td' : '_listItem_onMousedown',
'dblclick li, td' : '_listItem_onDoubleClick',
'click' : '_listBackground_onClick',
'click ul.collection-list, table.collection-list' : '_listBackground_onClick',
'keydown' : '_onKeydown'
}
});
});

View File

@ -1,89 +0,0 @@
'use strict';
define(
[
'underscore',
'vent',
'marionette',
'backbone',
'Settings/Quality/Profile/QualitySortableCollectionView',
'Settings/Quality/Profile/EditQualityProfileItemView',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView',
'Config'
], function (_, vent, Marionette, Backbone, QualitySortableCollectionView, EditQualityProfileItemView, AsModelBoundView, AsValidatedView, Config) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/EditQualityProfileViewTemplate',
ui: {
allowed : '.x-allowed-list',
cutoff : '.x-cutoff'
},
events: {
'click .x-save': '_saveQualityProfile'
},
initialize: function (options) {
this.profileCollection = options.profileCollection;
this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse());
},
onRender: function() {
var listViewAllowed = new QualitySortableCollectionView({
el : this.ui.allowed,
modelView : EditQualityProfileItemView,
selectable : true,
selectMultiple : true,
clickToSelect : true,
clickToToggle : true,
sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false),
collection : this.itemsCollection
});
listViewAllowed.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; }));
listViewAllowed.render();
this.listenTo(listViewAllowed, 'selectionChanged', this._selectionChanged);
this.listenTo(listViewAllowed, 'sortStop', this._updateModel);
},
_selectionChanged: function(newSelectedModels, oldSelectedModels) {
var addedModels = _.difference(newSelectedModels, oldSelectedModels);
var removeModels = _.difference(oldSelectedModels, newSelectedModels);
_.each(removeModels, function(item) { item.set('allowed', false); });
_.each(addedModels, function(item) { item.set('allowed', true); });
this._updateModel();
},
_updateModel: function() {
this.model.set('items', this.itemsCollection.toJSON().reverse());
this.render();
},
_saveQualityProfile: function () {
var self = this;
var cutoff = _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)});
this.model.set('cutoff', cutoff);
var promise = this.model.save();
if (promise) {
promise.done(function () {
self.profileCollection.add(self.model, { merge: true });
vent.trigger(vent.Commands.CloseModalCommand);
});
}
}
});
AsValidatedView.call(view);
return AsModelBoundView.call(view);
});

View File

@ -1,49 +0,0 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit</h3>
{{else}}
<h3>Add</h3>
{{/if}}
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="control-group">
<label class="control-label">Name</label>
<div class="controls">
<input type="text" name="name">
</div>
</div>
<div class="control-group">
<label class="control-label">Cutoff</label>
<div class="controls">
<select class="x-cutoff" name="cutoff.id" validation-name="cutoff">
{{#eachReverse items}}
{{#if allowed}}
<option value="{{quality.id}}">{{quality.name}}</option>
{{/if}}
{{/eachReverse}}
</select>
<span class="help-inline">
<i class="icon-question-sign" title="Once this quality is reached NzbDrone will no longer download episodes"/>
</span>
</div>
</div>
<div class="control-group">
<label class="control-label">Qualities</label>
<div class="controls qualities-controls">
<ul class="qualities x-allowed-list" validation-name="allowed"></ul>
<span class="help-inline">
<i class="icon-question-sign" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/>
</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-primary x-save">save</button>
</div>

View File

@ -3,7 +3,7 @@
define(['AppLayout',
'marionette',
'Settings/Quality/Profile/QualityProfileView',
'Settings/Quality/Profile/EditQualityProfileView',
'Settings/Quality/Profile/Edit/EditQualityProfileLayout',
'Settings/Quality/Profile/QualityProfileSchemaCollection',
'underscore'
], function (AppLayout, Marionette, QualityProfileView, EditProfileView, ProfileCollection, _) {

View File

@ -4,7 +4,7 @@ define(
[
'AppLayout',
'marionette',
'Settings/Quality/Profile/EditQualityProfileView',
'Settings/Quality/Profile/Edit/EditQualityProfileLayout',
'Settings/Quality/Profile/DeleteView',
'Series/SeriesCollection',
'Mixins/AsModelBoundView',
@ -13,7 +13,7 @@ define(
], function (AppLayout, Marionette, EditProfileView, DeleteProfileView, SeriesCollection, AsModelBoundView) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/QualityProfileTemplate',
template: 'Settings/Quality/Profile/QualityProfileViewTemplate',
tagName : 'li',
ui: {

View File

@ -1,27 +0,0 @@
'use strict';
define(
[
'backbone.collectionview'
], function (BackboneSortableCollectionView) {
return BackboneSortableCollectionView.extend({
events : {
'click li, td' : '_listItem_onMousedown',
'dblclick li, td' : '_listItem_onDoubleClick',
'click' : '_listBackground_onClick',
'click ul.collection-list, table.collection-list' : '_listBackground_onClick',
'keydown' : '_onKeydown',
'click .x-move' : '_onClickMove'
},
_onClickMove: function( theEvent ) {
var clickedItemId = this._getClickedItemId( theEvent );
if( clickedItemId )
{
var clickedModel = this.collection.get( clickedItemId );
this.trigger('moveClicked', clickedModel);
}
}
});
});

View File

@ -78,7 +78,7 @@ ul.qualities {
.drag-handle {
opacity: 1.0;
cursor: pointer;
cursor: move;
}
}