UX: Convert buttons to d-button

This commit is contained in:
Robin Ward 2017-08-01 16:06:51 -04:00
parent 75d10a4098
commit 8dd7c0c984
15 changed files with 110 additions and 154 deletions

View File

@ -0,0 +1,7 @@
import Button from 'discourse/components/d-button';
export default Button.extend({
tabindex: 5,
classNameBindings: [':btn-primary', ':create', 'disableSubmit:disabled'],
title: 'composer.title',
});

View File

@ -6,7 +6,7 @@ export default Ember.Component.extend({
tagName: 'button', tagName: 'button',
classNameBindings: [':btn', 'noText', 'btnType'], classNameBindings: [':btn', 'noText', 'btnType'],
attributeBindings: ['disabled', 'translatedTitle:title'], attributeBindings: ['disabled', 'translatedTitle:title', 'tabindex'],
btnIcon: Ember.computed.notEmpty('icon'), btnIcon: Ember.computed.notEmpty('icon'),
@ -23,7 +23,7 @@ export default Ember.Component.extend({
@computed("title") @computed("title")
translatedTitle(title) { translatedTitle(title) {
if (title) return I18n.t(title); return title ? I18n.t(title) : this.get('translatedLabel');
}, },
@computed("label") @computed("label")

View File

@ -0,0 +1,13 @@
import Button from 'discourse/components/d-button';
export default Button.extend({
classNames: ['share'],
icon: 'link',
title: 'topic.share.help',
label: 'topic.share.title',
attributeBindings: ['url:data-share-url'],
click() {
return true;
}
});

View File

@ -146,11 +146,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
} }
}.property('selected.name_key', 'userDetails.can_be_deleted', 'userDetails.can_delete_all_posts'), }.property('selected.name_key', 'userDetails.can_be_deleted', 'userDetails.can_delete_all_posts'),
canSendWarning: function() { @computed('flagTopic', 'selected.name_key')
if (this.get("flagTopic")) return false; canSendWarning(flagTopic, nameKey) {
return !flagTopic && this.currentUser.get('staff') && nameKey === 'notify_user';
return (Discourse.User.currentProp('staff') && this.get('selected.name_key') === 'notify_user'); },
}.property('selected.name_key'),
usernameChanged: function() { usernameChanged: function() {
this.set('userDetails', null); this.set('userDetails', null);

View File

@ -240,25 +240,25 @@ const Composer = RestModel.extend({
return (this.get('titleLength') <= this.siteSettings.max_topic_title_length); return (this.get('titleLength') <= this.siteSettings.max_topic_title_length);
}.property('minimumTitleLength', 'titleLength', 'post.static_doc'), }.property('minimumTitleLength', 'titleLength', 'post.static_doc'),
// The icon for the save button @computed('action')
saveIcon: function () { saveIcon(action) {
switch (this.get('action')) { switch (action) {
case EDIT: return iconHTML('pencil'); case EDIT: return 'pencil';
case REPLY: return iconHTML('reply'); case REPLY: return 'reply';
case CREATE_TOPIC: iconHTML('plus'); case CREATE_TOPIC: 'plus';
case PRIVATE_MESSAGE: iconHTML('envelope'); case PRIVATE_MESSAGE: 'envelope';
} }
}.property('action'), },
// The text for the save button @computed('action')
saveText: function() { saveLabel(action) {
switch (this.get('action')) { switch (action) {
case EDIT: return I18n.t('composer.save_edit'); case EDIT: return 'composer.save_edit';
case REPLY: return I18n.t('composer.reply'); case REPLY: return 'composer.reply';
case CREATE_TOPIC: return I18n.t('composer.create_topic'); case CREATE_TOPIC: return 'composer.create_topic';
case PRIVATE_MESSAGE: return I18n.t('composer.create_pm'); case PRIVATE_MESSAGE: return 'composer.create_pm';
} }
}.property('action'), },
hasMetaData: function() { hasMetaData: function() {
const metaData = this.get('metaData'); const metaData = this.get('metaData');

View File

@ -27,10 +27,7 @@
icon="bookmark" icon="bookmark"
action=toggleBookmark}} action=toggleBookmark}}
<button class="btn share" data-share-url={{topic.shareUrl}} title={{i18n "topic.share.help"}}> {{share-button url=topic.shareUrl}}
{{d-icon "link"}}
{{i18n "topic.share.title"}}
</button>
{{#if topic.details.can_flag_topic}} {{#if topic.details.can_flag_topic}}
{{d-button class="flag-topic" {{d-button class="flag-topic"

View File

@ -76,7 +76,7 @@
{{popup-input-tip validation=categoryValidation}} {{popup-input-tip validation=categoryValidation}}
</div> </div>
{{#if model.archetype.hasOptions}} {{#if model.archetype.hasOptions}}
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button> {{d-button action="showOptions" label="topic.options"}}
{{/if}} {{/if}}
{{/if}} {{/if}}
</div> </div>
@ -107,7 +107,12 @@
{{#if canEditTags}} {{#if canEditTags}}
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}} {{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
{{/if}} {{/if}}
<button {{action "save"}} tabindex="5" class="btn btn-primary create {{if disableSubmit 'disabled'}}" title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button> {{composer-save-button
action=(action "save")
icon=model.saveIcon
label=model.saveLabel
disableSubmit=disableSubmit}}
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a> <a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
{{#if site.mobileView}} {{#if site.mobileView}}

View File

@ -13,17 +13,38 @@
{{/d-modal-body}} {{/d-modal-body}}
<div class="modal-footer"> <div class="modal-footer">
<button class='btn btn-primary' {{action "createFlag"}} disabled={{submitDisabled}} title="{{i18n 'flagging.submit_tooltip'}}">{{{submitText}}}</button> {{d-button
class="btn-primary"
action=(action "createFlag")
disabled=submitDisabled
title="flagging.submit_tooltip"
translatedLabel=submitText}}
{{#if canSendWarning}} {{#if canSendWarning}}
<button class="btn btn-danger" {{action "createFlagAsWarning" }} disabled={{submitDisabled}} title="{{i18n 'flagging.official_warning'}}">{{d-icon "exclamation-triangle"}} {{i18n 'flagging.official_warning'}}</button> {{d-button
class="btn-danger"
action=(action "createFlagAsWarning")
disabled=submitDisabled
icon="exclamation-triangle"
label="flagging.official_warning"}}
{{/if}} {{/if}}
{{#if canTakeAction}} {{#if canTakeAction}}
<button class='btn btn-danger' {{action "takeAction"}} disabled={{submitDisabled}} title="{{i18n 'flagging.take_action_tooltip'}}">{{d-icon "gavel"}}{{i18n 'flagging.take_action'}}</button> {{d-button
class="btn-danger"
action=(action "takeAction")
disabled=submitDisabled
title="flagging.take_action_tooltip"
icon="gavel"
label="flagging.take_action"}}
{{/if}} {{/if}}
{{#if canDeleteSpammer}} {{#if canDeleteSpammer}}
<button class="btn btn-danger" {{action "deleteSpammer" userDetails}} disabled={{submitDisabled}} title="{{i18n 'flagging.delete_spammer'}}">{{d-icon "exclamation-triangle"}} {{i18n 'flagging.delete_spammer'}}</button> {{d-button
class="btn-danger"
action=(route-action "deleteSpammer" userDetails)
disabled=submitDisabled
icon="exclamation-triangle"
label="flagging.delete_spammer"}}
{{/if}} {{/if}}
</div> </div>

View File

@ -2,7 +2,7 @@ import { createWidget } from 'discourse/widgets/widget';
import { iconNode } from 'discourse-common/lib/icon-library'; import { iconNode } from 'discourse-common/lib/icon-library';
import { h } from 'virtual-dom'; import { h } from 'virtual-dom';
const ButtonClass = { export const ButtonClass = {
tagName: 'button.widget-button.btn', tagName: 'button.widget-button.btn',
buildClasses(attrs) { buildClasses(attrs) {
@ -20,7 +20,7 @@ const ButtonClass = {
className += '-text'; className += '-text';
} }
} else if (hasText) { } else if (hasText) {
className += 'btn-text'; className += ' btn-text';
} }
return className; return className;

View File

@ -90,11 +90,18 @@ createWidget('header-dropdown', jQuery.extend({
body.push(attrs.contents.call(this)); body.push(attrs.contents.call(this));
} }
return h('a.icon', { attributes: { href: attrs.href, return h(
'data-auto-route': true, 'a.icon.btn-flat',
title, { attributes: {
'aria-label': title, href: attrs.href,
id: attrs.iconId } }, body); 'data-auto-route': true,
title,
'aria-label': title,
id: attrs.iconId
}
},
body
);
} }
}, dropdown)); }, dropdown));

View File

@ -1,20 +1,15 @@
import { iconNode } from 'discourse-common/lib/icon-library'; import { iconNode } from 'discourse-common/lib/icon-library';
import { createWidget } from 'discourse/widgets/widget'; import { createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom'; import { h } from 'virtual-dom';
import { ButtonClass } from 'discourse/widgets/button';
createWidget('post-admin-menu-button', { createWidget('post-admin-menu-button', jQuery.extend(ButtonClass, {
tagName: 'li.btn', tagName: 'li.btn',
buildClasses(attrs) {
return attrs.className;
},
html(attrs) {
return [iconNode(attrs.icon), I18n.t(attrs.label)];
},
click() { click() {
this.sendWidgetAction('closeAdminMenu'); this.sendWidgetAction('closeAdminMenu');
return this.sendWidgetAction(this.attrs.action); return this.sendWidgetAction(this.attrs.action);
} }
}); }));
export default createWidget('post-admin-menu', { export default createWidget('post-admin-menu', {
tagName: 'div.post-admin-menu.popup-menu', tagName: 'div.post-admin-menu.popup-menu',

View File

@ -5,7 +5,6 @@
// Base // Base
// -------------------------------------------------- // --------------------------------------------------
.btn { .btn {
display: inline-block; display: inline-block;
margin: 0; margin: 0;

View File

@ -2,11 +2,12 @@ import componentTest from 'helpers/component-test';
moduleForComponent('d-button', {integration: true}); moduleForComponent('d-button', {integration: true});
componentTest('icon only button', { componentTest('icon only button', {
template: '{{d-button icon="plus"}}', template: '{{d-button icon="plus" tabindex="3"}}',
test(assert) { test(assert) {
assert.ok(this.$('button.btn.btn-icon.no-text').length, 'it has all the classes'); assert.ok(this.$('button.btn.btn-icon.no-text').length, 'it has all the classes');
assert.ok(this.$('button .d-icon.d-icon-plus').length, 'it has the icon'); assert.ok(this.$('button .d-icon.d-icon-plus').length, 'it has the icon');
assert.equal(this.$('button').attr('tabindex'), "3", 'it has the tabindex');
} }
}); });

View File

@ -0,0 +1,16 @@
import componentTest from 'helpers/component-test';
moduleForComponent('share-button', {integration: true});
componentTest('share button', {
template: '{{share-button url="https://eviltrout.com"}}',
test(assert) {
assert.ok(this.$(`button.share`).length, 'it has all the classes');
assert.ok(
this.$(`button[data-share-url="https://eviltrout.com"]`).length,
'it has the data attribute for sharing'
);
}
});

View File

@ -1,104 +0,0 @@
import createStore from 'helpers/create-store';
import AdminUser from 'admin/models/admin-user';
import { mapRoutes } from 'discourse/mapping-router';
var buildPost = function(args) {
return Discourse.Post.create(_.merge({
id: 1,
can_delete: true,
version: 1
}, args || {}));
};
var buildAdminUser = function(args) {
return AdminUser.create(_.merge({
id: 11,
username: 'urist'
}, args || {}));
};
moduleFor("controller:flag", "controller:flag", {
beforeEach() {
this.registry.register('router:main', mapRoutes());
},
needs: ['controller:modal']
});
QUnit.test("canDeleteSpammer not staff", function(assert) {
const store = createStore();
var flagController = this.subject({ model: buildPost() });
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false);
const spamFlag = store.createRecord('post-action-type', {name_key: 'spam'});
flagController.set('selected', spamFlag);
assert.equal(flagController.get('canDeleteSpammer'), false, 'false if current user is not staff');
});
var canDeleteSpammer = function(assert, flagController, postActionType, expected, testName) {
const store = createStore();
const flag = store.createRecord('post-action-type', {name_key: postActionType});
flagController.set('selected', flag);
assert.equal(flagController.get('canDeleteSpammer'), expected, testName);
};
QUnit.test("canDeleteSpammer spam not selected", function(assert) {
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
var flagController = this.subject({ model: buildPost() });
flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true}));
canDeleteSpammer(assert, flagController, 'off_topic', false, 'false if current user is staff, but selected is off_topic');
canDeleteSpammer(assert, flagController, 'inappropriate', false, 'false if current user is staff, but selected is inappropriate');
canDeleteSpammer(assert, flagController, 'notify_user', false, 'false if current user is staff, but selected is notify_user');
canDeleteSpammer(assert, flagController, 'notify_moderators', false, 'false if current user is staff, but selected is notify_moderators');
});
QUnit.test("canDeleteSpammer spam selected", function(assert) {
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
var flagController = this.subject({ model: buildPost() });
flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true}));
canDeleteSpammer(assert, flagController, 'spam', true, 'true if current user is staff, selected is spam, posts and user can be deleted');
flagController.set('userDetails', buildAdminUser({can_delete_all_posts: false, can_be_deleted: true}));
canDeleteSpammer(assert, flagController, 'spam', false, 'false if current user is staff, selected is spam, posts cannot be deleted');
flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: false}));
canDeleteSpammer(assert, flagController, 'spam', false, 'false if current user is staff, selected is spam, user cannot be deleted');
flagController.set('userDetails', buildAdminUser({can_delete_all_posts: false, can_be_deleted: false}));
canDeleteSpammer(assert, flagController, 'spam', false, 'false if current user is staff, selected is spam, user cannot be deleted');
});
QUnit.test("canSendWarning not staff", function(assert) {
const store = createStore();
var flagController = this.subject({ model: buildPost() });
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false);
const notifyUserFlag = store.createRecord('post-action-type', {name_key: 'notify_user'});
flagController.set('selected', notifyUserFlag);
assert.equal(flagController.get('canSendWarning'), false, 'false if current user is not staff');
});
var canSendWarning = function(assert, flagController, postActionType, expected, testName) {
const store = createStore();
const flag = store.createRecord('post-action-type', {name_key: postActionType});
flagController.set('selected', flag);
assert.equal(flagController.get('canSendWarning'), expected, testName);
};
QUnit.test("canSendWarning notify_user not selected", function(assert) {
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
var flagController = this.subject({ model: buildPost() });
canSendWarning(assert, flagController, 'off_topic', false, 'false if current user is staff, but selected is off_topic');
canSendWarning(assert, flagController, 'inappropriate', false, 'false if current user is staff, but selected is inappropriate');
canSendWarning(assert, flagController, 'spam', false, 'false if current user is staff, but selected is spam');
canSendWarning(assert, flagController, 'notify_moderators', false, 'false if current user is staff, but selected is notify_moderators');
});
QUnit.test("canSendWarning notify_user selected", function(assert) {
sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
var flagController = this.subject({ model: buildPost() });
canSendWarning(assert, flagController, 'notify_user', true, 'true if current user is staff, selected is notify_user');
});