Merge pull request #3748 from riking/category-reorder

Category reordering fixes
This commit is contained in:
Sam 2015-09-14 13:27:27 +10:00
commit 6d7915eae1
10 changed files with 122 additions and 84 deletions

View File

@ -0,0 +1,39 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
import DropdownButton from 'discourse/components/dropdown-button';
import computed from "ember-addons/ember-computed-decorators";
export default DropdownButton.extend({
buttonExtraClasses: 'no-text',
title: '',
text: iconHTML('bars') + ' ' + iconHTML('caret-down'),
classNames: ['category-notification-menu', 'category-admin-menu'],
@computed()
dropDownContent() {
const includeReorder = this.get('siteSettings.fixed_category_positions');
const items = [
{ id: 'create',
title: I18n.t('category.create'),
description: I18n.t('category.create_long'),
styleClasses: 'fa fa-plus' }
];
if (includeReorder) {
items.push({
id: 'reorder',
title: I18n.t('categories.reorder.title'),
description: I18n.t('categories.reorder.title_long'),
styleClasses: 'fa fa-random'
});
}
return items;
},
actionNames: {
create: 'createCategory',
reorder: 'reorderCategories'
},
clicked(id) {
this.sendAction('actionNames.' + id);
}
});

View File

@ -29,9 +29,7 @@ export default Ember.Component.extend(StringBuffer, {
buffer.push("<h4 class='title'>" + title + "</h4>"); buffer.push("<h4 class='title'>" + title + "</h4>");
} }
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>"); buffer.push(`<button class='btn standard dropdown-toggle ${this.get('buttonExtraClasses')}' data-toggle='dropdown'>${this.get('text')}</button>`);
buffer.push(this.get('text'));
buffer.push("</button>");
buffer.push("<ul class='dropdown-menu'>"); buffer.push("<ul class='dropdown-menu'>");
const contents = this.get('dropDownContent'); const contents = this.get('dropDownContent');

View File

@ -1,10 +1,11 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ModalFunctionality from 'discourse/mixins/modal-functionality';
const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy'; const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy';
import binarySearch from 'discourse/lib/binary-search';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators";
import Ember from 'ember'; import Ember from 'ember';
const SortableArrayProxy = Ember.ArrayProxy.extend(Ember.SortableMixin);
export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, { export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
@computed("site.categories") @computed("site.categories")
@ -13,24 +14,23 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
return categories.map(c => bufProxy.create({ content: c })); return categories.map(c => bufProxy.create({ content: c }));
}, },
// uses propertyDidChange() categoriesOrdered: function() {
@computed('categoriesBuffered') return SortableArrayProxy.create({
categoriesGrouped(cats) { sortProperties: ['content.position'],
const map = {}; content: this.get('categoriesBuffered')
cats.forEach((cat) => { });
const p = cat.get('position') || 0; }.property('categoriesBuffered'),
if (!map[p]) {
map[p] = {pos: p, cats: [cat]}; showFixIndices: function() {
} else { const cats = this.get('categoriesOrdered');
map[p].cats.push(cat); const len = cats.get('length');
for (let i = 0; i < len; i++) {
if (cats.objectAt(i).get('position') !== i) {
return true;
} }
}); }
const result = []; return false;
Object.keys(map).map(p => parseInt(p)).sort((a,b) => a-b).forEach(function(pos) { }.property('categoriesOrdered.@each.position'),
result.push(map[pos]);
});
return result;
},
showApplyAll: function() { showApplyAll: function() {
let anyChanged = false; let anyChanged = false;
@ -38,29 +38,19 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
return anyChanged; return anyChanged;
}.property('categoriesBuffered.@each.hasBufferedChanges'), }.property('categoriesBuffered.@each.hasBufferedChanges'),
saveDisabled: Ember.computed.alias('showApplyAll'), saveDisabled: Ember.computed.or('showApplyAll', 'showFixIndices'),
moveDir(cat, dir) { moveDir(cat, dir) {
const grouped = this.get('categoriesGrouped'), const cats = this.get('categoriesOrdered');
curPos = cat.get('position'), const curIdx = cats.indexOf(cat);
curGroupIdx = binarySearch(grouped, curPos, "pos"), const desiredIdx = curIdx + dir;
curGroup = grouped[curGroupIdx]; if (desiredIdx >= 0 && desiredIdx < cats.get('length')) {
const curPos = cat.get('position');
if (curGroup.cats.length === 1 && ((dir === -1 && curGroupIdx !== 0) || (dir === 1 && curGroupIdx !== (grouped.length - 1)))) {
const nextGroup = grouped[curGroupIdx + dir],
nextPos = nextGroup.pos;
cat.set('position', nextPos);
} else {
cat.set('position', curPos + dir); cat.set('position', curPos + dir);
const otherCat = cats.objectAt(desiredIdx);
otherCat.set('position', curPos - dir);
this.send('commit');
} }
cat.applyBufferedChanges();
Ember.run.next(this, () => {
this.propertyDidChange('categoriesGrouped');
Ember.run.schedule('afterRender', this, () => {
this.set('scrollIntoViewId', cat.get('id'));
this.trigger('scrollIntoView');
});
});
}, },
actions: { actions: {
@ -72,13 +62,22 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
this.moveDir(cat, 1); this.moveDir(cat, 1);
}, },
fixIndices() {
const cats = this.get('categoriesOrdered');
const len = cats.get('length');
for (let i = 0; i < len; i++) {
cats.objectAt(i).set('position', i);
}
this.send('commit');
},
commit() { commit() {
this.get('categoriesBuffered').forEach(bc => { this.get('categoriesBuffered').forEach(bc => {
if (bc.get('hasBufferedChanges')) { if (bc.get('hasBufferedChanges')) {
bc.applyBufferedChanges(); bc.applyBufferedChanges();
} }
}); });
this.propertyDidChange('categoriesGrouped'); this.propertyDidChange('categoriesBuffered');
}, },
saveOrder() { saveOrder() {

View File

@ -34,9 +34,8 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
setupController(controller, model) { setupController(controller, model) {
controller.set("model", model); controller.set("model", model);
// Only show either the Create Category or Create Topic button
this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category")); this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category"));
this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category")); this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic"));
this.openTopicDraft(model); this.openTopicDraft(model);
}, },

View File

@ -1,36 +1,33 @@
<div> <div class="modal-body reorder-categories full-height-modal">
<div class="modal-body reorder-categories"> <div id="rc-scroll-anchor"></div>
<div id="rc-scroll-anchor"></div> <table>
<table> <thead>
<thead> <th class="th-pos">{{i18n "categories.reorder.position"}}</th>
<th class="th-pos">Position</th> <th class="th-cat">{{i18n "categories.category"}}</th>
<th class="th-cat">Category</th> </thead>
</thead> {{#each categoriesOrdered as |cat|}}
{{#each categoriesGrouped as |group|}} <tr data-category-id="{{cat.id}}">
<tbody> <td>
{{#each group.cats as |cat|}} {{number-field number=cat.position}}
<tr data-category-id="{{cat.id}}"> {{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}}
<td> {{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}}
{{number-field number=cat.position}} {{#if cat.hasBufferedChanges}}
{{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}} {{d-button class="no-text" action="commit" icon="check"}}
{{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}} {{/if}}
{{#if cat.hasBufferedChanges}} </td>
{{d-button class="no-text" action="commit" icon="check"}} <td>{{category-badge cat allowUncategorized="true"}}</td>
{{/if}} </tr>
</td> {{/each}}
<td>{{category-badge cat allowUncategorized="true"}}</td> </table>
</tr> <div id="rc-scroll-bottom"></div>
{{/each}} </div>
</tbody>
{{/each}} <div class="modal-footer">
</table> {{#if showFixIndices}}
<div id="rc-scroll-bottom"></div> {{d-button action="fixIndices" icon="random" label="categories.reorder.fix_order" title="categories.reorder.fix_order_tooltip"}}
</div> {{/if}}
{{#if showApplyAll}}
<div class="modal-footer"> {{d-button action="commit" icon="check" label="categories.reorder.apply_all"}}
{{#if showApplyAll}} {{/if}}
{{d-button action="commit" icon="check" label="categories.reorder.apply_all"}} {{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
{{/if}}
{{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
</div>
</div> </div>

View File

@ -3,10 +3,7 @@
{{navigation-bar navItems=navItems filterMode=filterMode}} {{navigation-bar navItems=navItems filterMode=filterMode}}
{{#if canCreateCategory}} {{#if canCreateCategory}}
{{d-button action="createCategory" icon="plus" label="category.create"}} {{categories-admin-dropdown}}
{{#if siteSettings.fixed_category_positions}}
{{d-button action="reorderCategories" icon="random" label="category.reorder"}}
{{/if}}
{{/if}} {{/if}}
{{#if canCreateTopic}} {{#if canCreateTopic}}
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button> <button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>

View File

@ -1,2 +1 @@
@import "common/admin/admin_base"; @import "common/admin/admin_base";
@import "common/admin/cat_reorder";

View File

@ -26,3 +26,6 @@
padding-bottom: 150px; padding-bottom: 150px;
} }
} }
.category-admin-menu ul {
width: 320px;
}

View File

@ -129,6 +129,9 @@
} }
.modal-body { .modal-body {
&.full-height-modal {
max-height: calc(100vh - 150px);
}
textarea { textarea {
width: 99%; width: 99%;
height: 80px; height: 80px;

View File

@ -370,8 +370,12 @@ en:
category: "Category" category: "Category"
reorder: reorder:
title: "Reorder Categories" title: "Reorder Categories"
title_long: "Reorganize the category list"
fix_order: "Fix Positions"
fix_order_tooltip: "Not all categories have a unique position number, which may cause unexpected results."
save: "Save Order" save: "Save Order"
apply_all: "Apply" apply_all: "Apply"
position: "Position"
posts: "Posts" posts: "Posts"
topics: "Topics" topics: "Topics"
latest: "Latest" latest: "Latest"
@ -1519,6 +1523,7 @@ en:
topic_template: "Topic Template" topic_template: "Topic Template"
delete: 'Delete Category' delete: 'Delete Category'
create: 'New Category' create: 'New Category'
create_long: 'Create a new category'
save: 'Save Category' save: 'Save Category'
slug: 'Category Slug' slug: 'Category Slug'
slug_placeholder: '(Optional) dashed-words for url' slug_placeholder: '(Optional) dashed-words for url'
@ -1555,7 +1560,6 @@ en:
add_permission: "Add Permission" add_permission: "Add Permission"
this_year: "this year" this_year: "this year"
position: "position" position: "position"
reorder: "Reorder"
default_position: "Default Position" default_position: "Default Position"
position_disabled: "Categories will be displayed in order of activity. To control the order of categories in lists, " position_disabled: "Categories will be displayed in order of activity. To control the order of categories in lists, "
position_disabled_click: 'enable the "fixed category positions" setting.' position_disabled_click: 'enable the "fixed category positions" setting.'