FEATURE: unified UI for pinning/banner topics

REFACTOR: ES6ified all the modals
This commit is contained in:
Régis Hanol 2015-03-14 01:18:05 +01:00
parent 7c1540e5ab
commit 424a3b042a
64 changed files with 410 additions and 230 deletions

View file

@ -1,5 +1,4 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend(ModalFunctionality, {

View file

@ -1,7 +1,7 @@
import ChangeSiteCustomizationDetailsController from "admin/controllers/change-site-customization-details";
import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details";
export default ChangeSiteCustomizationDetailsController.extend({
onShow: function() {
this.selectPrevious();
this.send("selectPrevious");
}
});

View file

@ -25,7 +25,7 @@ export default Ember.Route.extend({
editGroupings() {
const groupings = this.controllerFor('admin-badges').get('badgeGroupings');
showModal('admin_edit_badge_groupings', groupings);
showModal('modals/admin-edit-badge-groupings', groupings);
},
preview(badge, explain) {
@ -40,7 +40,7 @@ export default Ember.Route.extend({
}
}).then(function(json) {
badge.set('preview_loading', false);
showModal('admin_badge_preview', json);
showModal('modals/admin-badge-preview', json);
}).catch(function(error) {
badge.set('preview_loading', false);
Em.Logger.error(error);

View file

@ -13,12 +13,12 @@ export default Discourse.Route.extend({
actions: {
showAgreeFlagModal(flaggedPost) {
showModal('admin_agree_flag', flaggedPost);
showModal('modals/admin-agree-flag', flaggedPost);
this.controllerFor('modal').set('modalClass', 'agree-flag-modal');
},
showDeleteFlagModal(flaggedPost) {
showModal('admin_delete_flag', flaggedPost);
showModal('modals/admin-delete-flag', flaggedPost);
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
}

View file

@ -13,12 +13,13 @@ export default Discourse.Route.extend({
actions: {
showDetailsModal(logRecord) {
showModal('admin_staff_action_log_details', logRecord);
showModal('modals/admin-staff-action-log-details', logRecord);
this.controllerFor('modal').set('modalClass', 'log-details-modal');
},
showCustomDetailsModal(logRecord) {
showModal(logRecord.action_name + '_details', logRecord);
const modalName = "modals/" + (logRecord.action_name + '_details').replace("_", "-");
showModal(modalName, logRecord);
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
}
}

View file

@ -25,7 +25,7 @@ export default Discourse.Route.extend({
actions: {
showSuspendModal(user) {
showModal('admin_suspend_user', user);
showModal('modals/admin-suspend-user', user);
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
}
}

View file

@ -1,4 +1,6 @@
Discourse.AdminAgreeFlagView = Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_agree_flag',
title: I18n.t('admin.flags.agree_flag_modal_title')
});

View file

@ -1,5 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
Discourse.AdminBadgePreviewView = Discourse.ModalBodyView.extend({
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_badge_preview',
title: I18n.t('admin.badges.preview.modal_title')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_delete_flag',
title: I18n.t('admin.flags.delete_flag_modal_title')
});

View file

@ -1,5 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
Discourse.AdminEditBadgeGroupingsView = Discourse.ModalBodyView.extend({
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_edit_badge_groupings',
title: I18n.t('admin.badges.badge_groupings.modal_title')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/details_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -1,4 +1,6 @@
Discourse.AdminStartBackupView = Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_start_backup',
title: I18n.t('admin.backups.operations.backup.confirm')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_suspend_user',
title: I18n.t('admin.user.suspend_modal_title')
});

View file

@ -1,12 +0,0 @@
/**
A modal view for deleting a flag.
@class AdminDeleteFlagView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminDeleteFlagView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_delete_flag',
title: I18n.t('admin.flags.delete_flag_modal_title')
});

View file

@ -1,12 +0,0 @@
/**
A modal view for details of a staff action log record in a modal.
@class AdminStaffActionLogDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminStaffActionLogDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/details_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -1,12 +0,0 @@
/**
A modal view for suspending a user.
@class AdminSuspendUserView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.AdminSuspendUserView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/modal/admin_suspend_user',
title: I18n.t('admin.user.suspend_modal_title')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -1,13 +0,0 @@
/**
A modal view for details of a staff action log record in a modal
for when a site customization is created or changed.
@class ChangeSiteCustomizationDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.ChangeSiteCustomizationDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -1,13 +0,0 @@
/**
A modal view for details of a staff action log record in a modal
for when a site customization is deleted.
@class DeleteSiteCustomizationDetailsView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.DeleteSiteCustomizationDetailsView = Discourse.ModalBodyView.extend({
templateName: 'admin/templates/logs/site_customization_change_modal',
title: I18n.t('admin.logs.staff_actions.modal_title')
});

View file

@ -0,0 +1,77 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { categoryLinkHTML } from 'discourse/helpers/category-link';
export default ObjectController.extend(ModalFunctionality, {
needs: ["topic"],
loading: true,
pinnedInCategoryCount: 0,
pinnedGloballyCount: 0,
bannerCount: 0,
categoryLink: function() {
return categoryLinkHTML(this.get("category"), { allowUncategorized: true });
}.property("category"),
unPinMessage: function() {
return this.get("pinned_globally") ?
I18n.t("topic.feature_topic.unpin_globally") :
I18n.t("topic.feature_topic.unpin", { categoryLink: this.get("categoryLink") });
}.property("categoryLink", "pinned_globally"),
pinMessage: function() {
return I18n.t("topic.feature_topic.pin", { categoryLink: this.get("categoryLink") });
}.property("categoryLink"),
alreadyPinnedMessage: function() {
return I18n.t("topic.feature_topic.already_pinned", { categoryLink: this.get("categoryLink"), count: this.get("pinnedInCategoryCount") });
}.property("categoryLink", "pinnedInCategoryCount"),
onShow() {
const self = this;
this.set("loading", true);
return Discourse.ajax("/topics/feature_stats.json", {
data: { category_id: this.get("category.id") }
}).then(function(result) {
if (result) {
self.setProperties({
pinnedInCategoryCount: result.pinned_in_category_count,
pinnedGloballyCount: result.pinned_globally_count,
bannerCount: result.banner_count,
});
}
}).finally(function() {
self.set("loading", false);
});
},
_forwardAction(name) {
this.get("controllers.topic").send(name);
this.send("closeModal");
},
_confirmBeforePinning(count, name, action) {
if (count < 4) {
this._forwardAction(action);
} else {
this.send("hideModal");
const message = I18n.t("topic.feature_topic.confirm_" + name, { count: count });
bootbox.confirm(
message, I18n.t("no_value"), I18n.t("yes_value"),
(confirmed) => confirmed ? this._forwardAction(action) : this.send("reopenModal")
);
}
},
actions: {
pin() { this._confirmBeforePinning(this.get("pinnedInCategoryCount"), "pin", "togglePinned"); },
pinGlobally() { this._confirmBeforePinning(this.get("pinnedGloballyCount"), "pin_globally", "pinGlobally"); },
unpin() { this._forwardAction("togglePinned"); },
makeBanner() { this._forwardAction("makeBanner"); },
removeBanner() { this._forwardAction("removeBanner"); },
}
});

View file

@ -94,6 +94,19 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
this.set('selectedReplies', []);
}.on('init'),
_togglePinnedStates(property) {
const value = this.get('pinned_at') ? false : true,
topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: value,
pinned_globally: value
});
return topic.saveStatus(property, value);
},
actions: {
deleteTopic() {
this.deleteTopic();
@ -352,13 +365,28 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
},
togglePinned() {
// Note that this is different than clearPin
this.get('content').setStatus('pinned', this.get('pinned_at') ? false : true);
const value = this.get('pinned_at') ? false : true,
topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: value ? moment() : null,
pinned_globally: false
});
return topic.saveStatus("pinned", value);
},
togglePinnedGlobally() {
// Note that this is different than clearPin
this.get('content').setStatus('pinned_globally', this.get('pinned_at') ? false : true);
pinGlobally() {
const topic = this.get('content');
// optimistic update
topic.setProperties({
pinned_at: moment(),
pinned_globally: true
});
return topic.saveStatus("pinned_globally", true);
},
toggleArchived() {

View file

@ -1,19 +1,15 @@
export default function showModal(name, model) {
// We use the container here because modals are like singletons
// in Discourse. Only one can be shown with a particular state.
const route = Discourse.__container__.lookup('route:application');
route.controllerFor('modal').set('modalClass', null);
route.render(name, {into: 'modal', outlet: 'modalBody'});
route.render(name, { into: 'modal', outlet: 'modalBody' });
const controller = route.controllerFor(name);
if (controller) {
if (model) {
controller.set('model', model);
}
if (controller.onShow) {
controller.onShow();
}
if (model) { controller.set('model', model); }
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);
}
return controller;

View file

@ -145,24 +145,16 @@ const Topic = Discourse.Model.extend({
toggleStatus(property) {
this.toggleProperty(property);
this.saveStatus(property, this.get(property) ? true : false);
},
setStatus(property, value) {
this.set(property, value);
this.saveStatus(property, value);
this.saveStatus(property, !this.get(property));
},
saveStatus(property, value) {
if (property === 'closed' && value === true) {
this.set('details.auto_close_at', null);
}
if (property === 'pinned') {
this.set('pinned_at', value ? moment() : null);
}
return Discourse.ajax(this.get('url') + "/status", {
type: 'PUT',
data: {status: property, enabled: value ? 'true' : 'false' }
data: { status: property, enabled: !!value }
});
},

View file

@ -102,7 +102,7 @@ const ApplicationRoute = Discourse.Route.extend({
// Close the current modal, and destroy its state.
closeModal() {
this.render('hide-modal', {into: 'modal', outlet: 'modalBody'});
this.render('hide-modal', { into: 'modal', outlet: 'modalBody' });
},
/**

View file

@ -59,6 +59,11 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
},
showFeatureTopic() {
showModal('featureTopic', this.modelFor('topic'));
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
},
showInvite() {
showModal('invite', this.modelFor('topic'));
this.controllerFor('invite').reset();

View file

@ -0,0 +1,62 @@
<div class="modal-body">
{{#if pinned_at}}
<div>
{{d-button action="unpin" icon="thumb-tack" label="topic.actions.unpin" class="btn-primary btn-small"}}
<p>{{{unPinMessage}}}</p>
{{#if pinned_globally}}
<p>{{i18n "topic.feature_topic.global_pin_note"}}</p>
<p>
{{#loading-spinner size="small" condition=loading}}
{{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}}
{{/loading-spinner}}
</p>
{{else}}
<p>{{i18n "topic.feature_topic.pin_note"}}</p>
<p>
{{#loading-spinner size="small" condition=loading}}
{{{alreadyPinnedMessage}}}
{{/loading-spinner}}
</p>
{{/if}}
</div>
{{else}}
<div>
{{d-button action="pin" icon="thumb-tack" label="topic.actions.pin" class="btn-primary btn-small"}}
<p>{{{pinMessage}}}</p>
<p>{{i18n "topic.feature_topic.pin_note"}}</p>
<p>
{{#loading-spinner size="small" condition=loading}}
{{{alreadyPinnedMessage}}}
{{/loading-spinner}}
</p>
</div>
<div>
{{d-button action="pinGlobally" icon="thumb-tack" label="topic.actions.pin_globally" class="btn-primary btn-small"}}
<p>{{i18n "topic.feature_topic.pin_globally"}}</p>
<p>{{i18n "topic.feature_topic.global_pin_note"}}</p>
<p>
{{#loading-spinner size="small" condition=loading}}
{{{i18n "topic.feature_topic.already_pinned_globally" count=pinnedGloballyCount}}}
{{/loading-spinner}}
</p>
</div>
{{/if}}
<div>
{{#if isBanner}}
{{d-button action="removeBanner" icon="bullhorn" label="topic.actions.remove_banner" class="btn-primary btn-small"}}
<p>{{i18n "topic.feature_topic.remove_banner"}}</p>
{{else}}
{{d-button action="makeBanner" icon="bullhorn" label="topic.actions.make_banner" class="btn-primary btn-small"}}
<p>{{i18n "topic.feature_topic.make_banner"}}</p>
{{/if}}
<p>{{i18n "topic.feature_topic.banner_note"}}</p>
<p>
{{#loading-spinner size="small" condition=loading}}
{{{i18n "topic.feature_topic.already_banner" count=bannerCount}}}
{{/loading-spinner}}
</p>
</div>
</div>
<div class="modal-footer">
<a class="pull-right" {{action "closeModal"}}>{{i18n "cancel"}}</a>
</div>

View file

@ -27,26 +27,11 @@
</li>
{{#unless isPrivateMessage}}
<li>
{{#if isBanner}}
{{d-button action="removeBanner" icon="bullhorn" label="topic.actions.remove_banner" class="btn-admin"}}
{{else}}
{{#if visible}}
{{d-button action="makeBanner" icon="bullhorn" label="topic.actions.make_banner" class="btn-admin"}}
{{/if}}
{{/if}}
</li>
<li>
{{#if pinned_at}}
{{d-button action="togglePinned" icon="thumb-tack" label="topic.actions.unpin" class="btn-admin"}}
{{else}}
{{#if visible}}
{{d-button action="togglePinned" icon="thumb-tack" label="topic.actions.pin" class="btn-admin"}}
{{d-button action="togglePinnedGlobally" icon="thumb-tack" label="topic.actions.pin_globally" class="btn-admin"}}
{{/if}}
{{/if}}
</li>
{{#if visible}}
<li>
{{d-button action="showFeatureTopic" icon="bullhorn" label="topic.actions.feature" class="btn-admin"}}
</li>
{{/if}}
{{/unless}}
<li>

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/archetype_options',
title: I18n.t('topic.options')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/avatar_selector',
classNames: ['avatar-selector'],
title: I18n.t('user.change_avatar.title'),
@ -9,10 +11,6 @@ export default Discourse.ModalBodyView.extend({
// *HACK* used to select the proper radio button, cause {{action}}
// stops the default behavior
selectedChanged: function() {
var self = this;
Em.run.next(function() {
var value = self.get('controller.selected');
$('input:radio[name="avatar"]').val([value]);
});
Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')]) );
}.observes('controller.selected')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/change_owner',
title: I18n.t('topic.change_owner.title')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/create-account',
title: I18n.t('create_account.title'),
classNames: ['create-account'],
@ -6,7 +8,7 @@ export default Discourse.ModalBodyView.extend({
_setup: function() {
// allows the submission the form when pressing 'ENTER' on *any* text input field
// but only when the submit button is enabled
var createAccountController = this.get('controller');
const createAccountController = this.get('controller');
Em.run.schedule('afterRender', function() {
$("input[type='text'], input[type='password']").keydown(function(e) {
if (createAccountController.get('submitDisabled') === false && e.keyCode === 13) {

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/edit-category',
_initializePanels: function() {

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/auto_close',
title: I18n.t('topic.auto_close_title')
});

View file

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/feature-topic',
title: I18n.t('topic.feature_topic.title')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/flag',
title: function() {
@ -6,12 +8,13 @@ export default Discourse.ModalBodyView.extend({
}.property('controller.flagTopic'),
selectedChanged: function() {
var self = this;
const self = this;
Em.run.next(function() {
self.$("input[type='radio']").prop('checked', false);
var nameKey = self.get('controller.selected.name_key');
if (!nameKey) return;
const nameKey = self.get('controller.selected.name_key');
if (!nameKey) { return; }
self.$('#radio_' + nameKey).prop('checked', 'true');
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/forgot_password',
title: I18n.t('forgot_password.title'),
});

View file

@ -1,9 +1,11 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/history',
title: I18n.t('history'),
resizeModal: function(){
var viewPortHeight = $(window).height();
const viewPortHeight = $(window).height();
this.$(".modal-body").css("max-height", Math.floor(0.8 * viewPortHeight) + "px");
}.on("didInsertElement")
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/invite_private',
title: I18n.t('topic.invite_private.title')
});

View file

@ -1,10 +1,12 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/invite',
title: function() {
if (this.get('controller.invitingToTopic')) {
return I18n.t('topic.invite_reply.title');
} else {
return I18n.t('user.invited.create');
}
return this.get('controller.invitingToTopic') ?
I18n.t('topic.invite_reply.title') :
I18n.t('user.invited.create');
}.property('controller.invitingToTopic')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/keyboard_shortcuts_help',
title: I18n.t('keyboard_shortcuts_help.title')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/login',
title: I18n.t('login.title'),
classNames: ['login-modal'],
@ -9,7 +11,7 @@ export default Discourse.ModalBodyView.extend({
},
_setup: function() {
var loginController = this.get('controller');
const loginController = this.get('controller');
// Get username and password from the browser's password manager,
// if it filled the hidden static login form:
@ -18,10 +20,8 @@ export default Discourse.ModalBodyView.extend({
Em.run.schedule('afterRender', function() {
$('#login-account-password, #login-account-name').keydown(function(e) {
if (e.keyCode === 13) {
if (!loginController.get('loginDisabled')) {
loginController.send('login');
}
if (e.keyCode === 13 && !loginController.get('loginDisabled')) {
loginController.send('login');
}
});
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/merge_topic',
title: I18n.t('topic.merge_topic.title')
});

View file

@ -0,0 +1,30 @@
export default Discourse.View.extend({
focusInput: true,
_setupModal: function() {
$('#modal-alert').hide();
$('#discourse-modal').modal('show');
// Focus on first element
if (!Discourse.Mobile.mobileView && this.get('focusInput')) {
Em.run.schedule('afterRender', () => this.$('input:first').focus() );
}
const title = this.get('title');
if (title) {
this.set('controller.controllers.modal.title', title);
}
}.on('didInsertElement'),
flashMessageChanged: function() {
const flashMessage = this.get('controller.flashMessage');
if (flashMessage) {
const messageClass = flashMessage.get('messageClass') || 'success';
$('#modal-alert').hide()
.removeClass('alert-error', 'alert-success')
.addClass("alert alert-" + messageClass).html(flashMessage.get('message'))
.fadeIn();
}
}.observes('controller.flashMessage')
});

View file

@ -4,13 +4,13 @@ export default Ember.View.extend({
classNameBindings: [':modal', ':hidden', 'controller.modalClass'],
click: function(e) {
var $target = $(e.target);
const $target = $(e.target);
if ($target.hasClass("modal-middle-container") ||
$target.hasClass("modal-outer-container")) {
// Delegate click to modal backdrop if clicked outside. We do this
// because some CSS of ours seems to cover the backdrop and makes it
// unclickable.
$('.modal-backdrop').click();
// Delegate click to modal close if clicked outside.
// We do this because some CSS of ours seems to cover
// the backdrop and makes it unclickable.
$('.modal-header a.close').click();
}
}
});

View file

@ -1,48 +0,0 @@
/**
A base class for helping us display modal content
@class ModalBodyView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.ModalBodyView = Discourse.View.extend({
focusInput: true,
_setupModal: function() {
var self = this,
$discourseModal = $('#discourse-modal');
$discourseModal.modal('show');
$discourseModal.one("hide", function () {
self.get("controller").send("closeModal");
});
$('#modal-alert').hide();
// Focus on first element
if (!Discourse.Mobile.mobileView && self.get('focusInput')) {
Em.run.schedule('afterRender', function() {
self.$('input:first').focus();
});
}
var title = this.get('title');
if (title) {
this.set('controller.controllers.modal.title', title);
}
}.on('didInsertElement'),
flashMessageChanged: function() {
var flashMessage = this.get('controller.flashMessage');
if (flashMessage) {
var messageClass = flashMessage.get('messageClass') || 'success';
var $alert = $('#modal-alert').hide().removeClass('alert-error', 'alert-success');
$alert.addClass("alert alert-" + messageClass).html(flashMessage.get('message'));
$alert.fadeIn();
}
}.observes('controller.flashMessage')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/not_activated',
title: I18n.t('log_in')
});

View file

@ -1,9 +1,11 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/raw_email',
title: I18n.t('raw_email.title'),
resizeModal: function(){
var viewPortHeight = $(window).height();
const viewPortHeight = $(window).height();
this.$(".modal-body").css("max-height", Math.floor(0.8 * viewPortHeight) + "px");
}.on("didInsertElement")
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/search_help',
title: I18n.t('search_help.title'),
focusInput: false

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend(Discourse.SelectedPostsCount, {
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend(Discourse.SelectedPostsCount, {
templateName: 'modal/split_topic',
title: I18n.t('topic.split_topic.title')
});

View file

@ -1,4 +1,6 @@
export default Discourse.ModalBodyView.extend({
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/topic-bulk-actions',
title: I18n.t('topics.bulk.actions')
});

View file

@ -1,10 +1,12 @@
import ModalBodyView from "discourse/views/modal-body";
function uploadTranslate(key, options) {
var opts = options || {};
const opts = options || {};
if (Discourse.Utilities.allowsAttachments()) { key += "_with_attachments"; }
return I18n.t("upload_selector." + key, opts);
}
export default Discourse.ModalBodyView.extend({
export default ModalBodyView.extend({
templateName: 'modal/upload_selector',
classNames: ['upload-selector'],
@ -12,17 +14,16 @@ export default Discourse.ModalBodyView.extend({
uploadIcon: function() { return Discourse.Utilities.allowsAttachments() ? "fa-upload" : "fa-picture-o"; }.property(),
tip: function() {
var source = this.get("controller.local") ? "local" : "remote";
var opts = { authorized_extensions: Discourse.Utilities.authorizedExtensions() };
const source = this.get("controller.local") ? "local" : "remote",
opts = { authorized_extensions: Discourse.Utilities.authorizedExtensions() };
return uploadTranslate(source + "_tip", opts);
}.property("controller.local"),
hint: function() {
// cf. http://stackoverflow.com/a/9851769/11983
var isChrome = !!window.chrome && !(!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0);
var isFirefox = typeof InstallTrigger !== 'undefined';
var isSupported = isChrome || isFirefox;
const isChrome = !!window.chrome && !(!!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0),
isFirefox = typeof InstallTrigger !== 'undefined',
isSupported = isChrome || isFirefox;
// chrome is the only browser that support copy & paste of images.
return I18n.t("upload_selector.hint" + (isSupported ? "_for_supported_browsers" : ""));
}.property(),
@ -32,7 +33,7 @@ export default Discourse.ModalBodyView.extend({
}.on('didInsertElement'),
selectedChanged: function() {
var self = this;
const self = this;
Em.run.next(function() {
// *HACK* to select the proper radio button
var value = self.get('controller.local') ? 'local' : 'remote';
@ -47,9 +48,9 @@ export default Discourse.ModalBodyView.extend({
if (this.get("controller.local")) {
$('#reply-control').fileupload('add', { fileInput: $('#filename-input') });
} else {
var imageUrl = $('#fileurl-input').val();
var imageLink = $('#link-input').val();
var composerView = this.get('controller.composerView');
const imageUrl = $('#fileurl-input').val(),
imageLink = $('#link-input').val(),
composerView = this.get('controller.composerView');
if (this.get("controller.showMore") && imageLink.length > 3) {
composerView.addMarkdown("[![](" + imageUrl +")](" + imageLink + ")");
} else {

View file

@ -36,7 +36,7 @@
//= require ./discourse/controllers/navigation/default
//= require ./discourse/views/view
//= require ./discourse/views/container
//= require ./discourse/views/modal_body_view
//= require ./discourse/views/modal-body
//= require ./discourse/views/flag
//= require ./discourse/views/combo-box
//= require ./discourse/views/button

View file

@ -2,7 +2,6 @@
//= require admin/models/user-field
//= require admin/models/site-setting
//= require admin/controllers/admin-email-skipped
//= require admin/controllers/change-site-customization-details
//= require discourse/lib/export-result
//= require_tree ./admin

View file

@ -158,6 +158,19 @@ class TopicsController < ApplicationController
render_serialized(topics, BasicTopicSerializer)
end
def feature_stats
params.require(:category_id)
category_id = params[:category_id].to_i
topics = Topic.listable_topics.visible
render json: {
pinned_in_category_count: topics.where(category_id: category_id).where(pinned_globally: false).where.not(pinned_at: nil).count,
pinned_globally_count: topics.where(pinned_globally: true).where.not(pinned_at: nil).count,
banner_count: topics.where(archetype: Archetype.banner).count,
}
end
def status
params.require(:status)
params.require(:enabled)
@ -492,7 +505,7 @@ class TopicsController < ApplicationController
end
def check_for_status_presence(key, attr)
invalid_param(key) unless %w(pinned_globally visible closed pinned archived).include?(attr)
invalid_param(key) unless %w(pinned pinned_globally visible closed archived).include?(attr)
end
def invalid_param(key)

View file

@ -973,10 +973,11 @@ en:
open: "Open Topic"
close: "Close Topic"
auto_close: "Auto Close"
feature: "Feature Topic"
make_banner: "Banner Topic"
remove_banner: "Remove Banner Topic"
unpin: "Un-Pin Topic"
pin: "Pin Topic"
unpin: "Un-Pin Topic"
pin_globally: "Pin Topic Globally"
unarchive: "Unarchive Topic"
archive: "Archive Topic"
@ -1002,6 +1003,31 @@ en:
help: 'privately flag this topic for attention or send a private notification about it'
success_message: 'You successfully flagged this topic.'
feature_topic:
title: "Feature this topic"
pin: "Make this topic appear at the top of the {{categoryLink}} category."
confirm_pin: "Are you sure? You already have {{count}} pinned topics -- too many pinned topics can obscure other active topics."
unpin: "Remove this topic from the topic of the {{categoryLink}} category."
pin_note: "Users can unpin the topic individually for themselves."
already_pinned:
zero: "No topic currently pinned in {{categoryLink}}."
one: "Topic currently pinned in {{categoryLink}}: <strong>1.</strong>"
other: "Topics currently pinned in {{categoryLink}}: <strong>{{count}}</strong>."
pin_globally: "Make this topic appear at the top of all topic lists, until a staff member unpins it."
confirm_pin_globally: "Are you sure? You already have {{count}} globally pinned topics -- too many pinned topics can obscure other active topics."
unpin_globally: "Remove this topic from the top of all topic lists."
global_pin_note: "Users can unpin the topic individually for themselves."
already_pinned_globally:
zero: "No topic currently pinned globally."
one: "Topic currently pinned globally: <strong>1.</strong>"
other: "Topics currently pinned globally: <strong>{{count}}</strong>."
make_banner: "Make this topic into a banner that appears at the top of all pages."
remove_banner: "Remove the banner that appears at the top of all pages."
banner_note: "Users can dismiss the banner by closing it. Only one topic can be bannered at any given time."
already_banner:
zero: "There is currently no banner topic."
one: "There is currently a banner topic."
inviting: "Inviting..."
automatically_add_to_groups_optional: "This invite also includes access to these groups: (optional, admin only)"
automatically_add_to_groups_required: "This invite also includes access to these groups: (<b>Required</b>, admin only)"

View file

@ -392,6 +392,7 @@ Discourse::Application.routes.draw do
put "topics/reset-new" => 'topics#reset_new'
post "topics/timings"
get "topics/similar_to"
get "topics/feature_stats"
get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: {username: USERNAME_ROUTE_FORMAT}
get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: {username: USERNAME_ROUTE_FORMAT}
get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: {username: USERNAME_ROUTE_FORMAT}

View file

@ -19,7 +19,7 @@ test("modal", function() {
click('.login-button');
andThen(function() {
ok(find('#discourse-modal:visible').length === 1, 'modal should appear');
ok(find('#discourse-modal:visible').length === 1, 'modal should reappear');
});
keyEvent('#main-outlet', 'keyup', 27);