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',
classNameBindings: [':btn', 'noText', 'btnType'],
attributeBindings: ['disabled', 'translatedTitle:title'],
attributeBindings: ['disabled', 'translatedTitle:title', 'tabindex'],
btnIcon: Ember.computed.notEmpty('icon'),
@ -23,7 +23,7 @@ export default Ember.Component.extend({
@computed("title")
translatedTitle(title) {
if (title) return I18n.t(title);
return title ? I18n.t(title) : this.get('translatedLabel');
},
@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'),
canSendWarning: function() {
if (this.get("flagTopic")) return false;
return (Discourse.User.currentProp('staff') && this.get('selected.name_key') === 'notify_user');
}.property('selected.name_key'),
@computed('flagTopic', 'selected.name_key')
canSendWarning(flagTopic, nameKey) {
return !flagTopic && this.currentUser.get('staff') && nameKey === 'notify_user';
},
usernameChanged: function() {
this.set('userDetails', null);

View File

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

View File

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

View File

@ -76,7 +76,7 @@
{{popup-input-tip validation=categoryValidation}}
</div>
{{#if model.archetype.hasOptions}}
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
{{d-button action="showOptions" label="topic.options"}}
{{/if}}
{{/if}}
</div>
@ -107,7 +107,12 @@
{{#if canEditTags}}
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
{{/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>
{{#if site.mobileView}}

View File

@ -13,17 +13,38 @@
{{/d-modal-body}}
<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}}
<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 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 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}}
</div>

View File

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

View File

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

View File

@ -1,20 +1,15 @@
import { iconNode } from 'discourse-common/lib/icon-library';
import { createWidget } from 'discourse/widgets/widget';
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',
buildClasses(attrs) {
return attrs.className;
},
html(attrs) {
return [iconNode(attrs.icon), I18n.t(attrs.label)];
},
click() {
this.sendWidgetAction('closeAdminMenu');
return this.sendWidgetAction(this.attrs.action);
}
});
}));
export default createWidget('post-admin-menu', {
tagName: 'div.post-admin-menu.popup-menu',

View File

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

View File

@ -2,11 +2,12 @@ import componentTest from 'helpers/component-test';
moduleForComponent('d-button', {integration: true});
componentTest('icon only button', {
template: '{{d-button icon="plus"}}',
template: '{{d-button icon="plus" tabindex="3"}}',
test(assert) {
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.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');
});