From f50280a889b6a02c453bca632ed8eadc884c1a44 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 10 Mar 2015 15:01:15 -0400 Subject: [PATCH] Split out bulk operations modal and `Discourse.Route.showModal` This makes it easier to share bulk topic operations, for example from a plugin's custom topic list. --- .../controllers/admin-suspend-user.js.es6 | 2 +- ..._backups_route.js => admin-backups.js.es6} | 65 +++++--------- .../admin/routes/admin-badges-show.js.es6 | 24 ++--- .../admin/routes/admin-flags-list.js.es6 | 14 +-- .../admin-logs-staff-action-logs.js.es6 | 10 ++- .../admin/routes/admin-user-index.js.es6 | 32 +++++++ .../admin/routes/admin-user.js.es6 | 20 +++++ .../admin/routes/admin_user_route.js | 51 ----------- .../components/bulk-select-button.js.es6 | 10 +++ .../controllers/discovery/topics.js.es6 | 45 +--------- .../controllers/edit-category.js.es6 | 4 +- .../controllers/edit-topic-auto-close.js.es6 | 4 +- .../discourse/controllers/login.js.es6 | 5 +- .../controllers/topic-bulk-actions.js.es6 | 10 ++- .../discourse/lib/show-modal.js.es6 | 20 +++++ .../mixins/bulk-topic-selection.js.es6 | 49 ++++++++++ .../discourse/routes/application.js.es6 | 30 ++++--- .../{discourse_route.js => discourse.js.es6} | 90 ++++++++----------- .../routes/discovery-categories-route.js.es6 | 5 +- ...iscovery-route.js.es6 => discovery.js.es6} | 17 +--- .../discourse/routes/preferences.js.es6 | 19 ++-- .../javascripts/discourse/routes/topic.js.es6 | 62 ++++++------- .../discourse/routes/user-invited.js.es6 | 17 ++-- .../components/bulk-select-button.hbs | 5 ++ .../discourse/templates/discovery/topics.hbs | 6 +- app/assets/javascripts/main_include.js | 3 +- .../tilt/es6_module_transpiler_template.rb | 1 + 27 files changed, 313 insertions(+), 307 deletions(-) rename app/assets/javascripts/admin/routes/{admin_backups_route.js => admin-backups.js.es6} (75%) create mode 100644 app/assets/javascripts/admin/routes/admin-user-index.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-user.js.es6 delete mode 100644 app/assets/javascripts/admin/routes/admin_user_route.js create mode 100644 app/assets/javascripts/discourse/components/bulk-select-button.js.es6 create mode 100644 app/assets/javascripts/discourse/lib/show-modal.js.es6 create mode 100644 app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 rename app/assets/javascripts/discourse/routes/{discourse_route.js => discourse.js.es6} (88%) rename app/assets/javascripts/discourse/routes/{discovery-route.js.es6 => discovery.js.es6} (62%) create mode 100644 app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs diff --git a/app/assets/javascripts/admin/controllers/admin-suspend-user.js.es6 b/app/assets/javascripts/admin/controllers/admin-suspend-user.js.es6 index a8c19fe563b..b2d30f96d00 100644 --- a/app/assets/javascripts/admin/controllers/admin-suspend-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-suspend-user.js.es6 @@ -19,7 +19,7 @@ export default ObjectController.extend(ModalFunctionality, { window.location.reload(); }, function(e) { var error = I18n.t('admin.user.suspend_failed', { error: "http: " + e.status + " - " + e.body }); - bootbox.alert(error, function() { self.send('showModal'); }); + bootbox.alert(error, function() { self.send('reopenModal'); }); }); } } diff --git a/app/assets/javascripts/admin/routes/admin_backups_route.js b/app/assets/javascripts/admin/routes/admin-backups.js.es6 similarity index 75% rename from app/assets/javascripts/admin/routes/admin_backups_route.js rename to app/assets/javascripts/admin/routes/admin-backups.js.es6 index da5dd9a8acd..8234048c281 100644 --- a/app/assets/javascripts/admin/routes/admin_backups_route.js +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -1,12 +1,14 @@ -Discourse.AdminBackupsRoute = Discourse.Route.extend({ +import showModal from 'discourse/lib/show-modal'; + +export default Discourse.Route.extend({ LOG_CHANNEL: "/admin/backups/logs", - activate: function() { + activate() { Discourse.MessageBus.subscribe(this.LOG_CHANNEL, this._processLogMessage.bind(this)); }, - _processLogMessage: function(log) { + _processLogMessage(log) { if (log.message === "[STARTED]") { this.controllerFor("adminBackups").set("isOperationRunning", true); this.controllerFor("adminBackupsLogs").clear(); @@ -25,7 +27,7 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ } }, - model: function() { + model() { return PreloadStore.getAndRemove("operations_status", function() { return Discourse.ajax("/admin/backups/status.json"); }).then(function (status) { @@ -37,35 +39,24 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ }); }, - deactivate: function() { + deactivate() { Discourse.MessageBus.unsubscribe(this.LOG_CHANNEL); }, actions: { - /** - Starts a backup and redirect the user to the logs tab - - @method startBackup - **/ - startBackup: function() { - Discourse.Route.showModal(this, 'admin_start_backup'); + startBackup() { + showModal('admin_start_backup'); this.controllerFor('modal').set('modalClass', 'start-backup-modal'); }, - backupStarted: function () { + backupStarted() { this.modelFor("adminBackups").set("isOperationRunning", true); this.transitionTo("admin.backups.logs"); this.send("closeModal"); }, - /** - Destroys a backup - - @method destroyBackup - @param {Discourse.Backup} backup the backup to destroy - **/ - destroyBackup: function(backup) { - var self = this; + destroyBackup(backup) { + const self = this; bootbox.confirm( I18n.t("admin.backups.operations.destroy.confirm"), I18n.t("no_value"), @@ -80,14 +71,8 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ ); }, - /** - Start a restore and redirect the user to the logs tab - - @method startRestore - @param {Discourse.Backup} backup the backup to restore - **/ - startRestore: function(backup) { - var self = this; + startRestore(backup) { + const self = this; bootbox.confirm( I18n.t("admin.backups.operations.restore.confirm"), I18n.t("no_value"), @@ -105,13 +90,8 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ ); }, - /** - Cancels the current operation - - @method cancelOperation - **/ - cancelOperation: function() { - var self = this; + cancelOperation() { + const self = this; bootbox.confirm( I18n.t("admin.backups.operations.cancel.confirm"), I18n.t("no_value"), @@ -126,12 +106,7 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ ); }, - /** - Rollback to previous working state - - @method rollback - **/ - rollback: function() { + rollback() { bootbox.confirm( I18n.t("admin.backups.operations.rollback.confirm"), I18n.t("no_value"), @@ -142,8 +117,8 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ ); }, - uploadSuccess: function(filename) { - var self = this; + uploadSuccess(filename) { + const self = this; bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }), function() { Discourse.Backup.find().then(function (backups) { self.controllerFor("adminBackupsIndex").set("model", backups); @@ -151,7 +126,7 @@ Discourse.AdminBackupsRoute = Discourse.Route.extend({ }); }, - uploadError: function(filename, message) { + uploadError(filename, message) { bootbox.alert(I18n.t("admin.backups.upload.error", { filename: filename, message: message })); } } diff --git a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 index 7cbb1ce4f25..3d2ac344594 100644 --- a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 @@ -1,9 +1,11 @@ +import showModal from 'discourse/lib/show-modal'; + export default Ember.Route.extend({ - serialize: function(m) { + serialize(m) { return {badge_id: Em.get(m, 'id') || 'new'}; }, - model: function(params) { + model(params) { if (params.badge_id === "new") { return Discourse.Badge.create({ name: I18n.t('admin.badges.new_badge') @@ -13,22 +15,20 @@ export default Ember.Route.extend({ }, actions: { - saveError: function(e) { - var msg = I18n.t("generic_error"); + saveError(e) { + let msg = I18n.t("generic_error"); if (e.responseJSON && e.responseJSON.errors) { msg = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}); } bootbox.alert(msg); }, - editGroupings: function() { - var groupings = this.controllerFor('admin-badges').get('badgeGroupings'); - Discourse.Route.showModal(this, 'admin_edit_badge_groupings', groupings); + editGroupings() { + const groupings = this.controllerFor('admin-badges').get('badgeGroupings'); + showModal('admin_edit_badge_groupings', groupings); }, - preview: function(badge, explain) { - var self = this; - + preview(badge, explain) { badge.set('preview_loading', true); Discourse.ajax('/admin/badges/preview.json', { method: 'post', @@ -36,11 +36,11 @@ export default Ember.Route.extend({ sql: badge.get('query'), target_posts: !!badge.get('target_posts'), trigger: badge.get('trigger'), - explain: explain + explain } }).then(function(json) { badge.set('preview_loading', false); - Discourse.Route.showModal(self, 'admin_badge_preview', json); + showModal('admin_badge_preview', json); }).catch(function(error) { badge.set('preview_loading', false); Em.Logger.error(error); diff --git a/app/assets/javascripts/admin/routes/admin-flags-list.js.es6 b/app/assets/javascripts/admin/routes/admin-flags-list.js.es6 index 973c59ae3d3..afe3d8ab460 100644 --- a/app/assets/javascripts/admin/routes/admin-flags-list.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-flags-list.js.es6 @@ -1,22 +1,24 @@ +import showModal from 'discourse/lib/show-modal'; + export default Discourse.Route.extend({ - model: function(params) { + model(params) { this.filter = params.filter; return Discourse.FlaggedPost.findAll(params.filter); }, - setupController: function(controller, model) { + setupController(controller, model) { controller.set('model', model); controller.set('query', this.filter); }, actions: { - showAgreeFlagModal: function (flaggedPost) { - Discourse.Route.showModal(this, 'admin_agree_flag', flaggedPost); + showAgreeFlagModal(flaggedPost) { + showModal('admin_agree_flag', flaggedPost); this.controllerFor('modal').set('modalClass', 'agree-flag-modal'); }, - showDeleteFlagModal: function (flaggedPost) { - Discourse.Route.showModal(this, 'admin_delete_flag', flaggedPost); + showDeleteFlagModal(flaggedPost) { + showModal('admin_delete_flag', flaggedPost); this.controllerFor('modal').set('modalClass', 'delete-flag-modal'); } diff --git a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 index bcc6a5fc173..29a40ed77cc 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 @@ -1,3 +1,5 @@ +import showModal from 'discourse/lib/show-modal'; + export default Discourse.Route.extend({ // TODO: make this automatic using an `{{outlet}}` renderTemplate: function() { @@ -10,13 +12,13 @@ export default Discourse.Route.extend({ }, actions: { - showDetailsModal: function(logRecord) { - Discourse.Route.showModal(this, 'admin_staff_action_log_details', logRecord); + showDetailsModal(logRecord) { + showModal('admin_staff_action_log_details', logRecord); this.controllerFor('modal').set('modalClass', 'log-details-modal'); }, - showCustomDetailsModal: function(logRecord) { - Discourse.Route.showModal(this, logRecord.action_name + '_details', logRecord); + showCustomDetailsModal(logRecord) { + showModal(logRecord.action_name + '_details', logRecord); this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal'); } } diff --git a/app/assets/javascripts/admin/routes/admin-user-index.js.es6 b/app/assets/javascripts/admin/routes/admin-user-index.js.es6 new file mode 100644 index 00000000000..e9c084ab3e5 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-user-index.js.es6 @@ -0,0 +1,32 @@ +import showModal from 'discourse/lib/show-modal'; + +export default Discourse.Route.extend({ + model() { + return this.modelFor('adminUser'); + }, + + afterModel(model) { + if (this.currentUser.get('admin')) { + const self = this; + return Discourse.Group.findAll().then(function(groups){ + self._availableGroups = groups.filterBy('automatic', false); + return model; + }); + } + }, + + setupController(controller, model) { + controller.setProperties({ + originalPrimaryGroupId: model.get('primary_group_id'), + availableGroups: this._availableGroups, + model + }); + }, + + actions: { + showSuspendModal(user) { + showModal('admin_suspend_user', user); + this.controllerFor('modal').set('modalClass', 'suspend-user-modal'); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-user.js.es6 b/app/assets/javascripts/admin/routes/admin-user.js.es6 new file mode 100644 index 00000000000..03c236ab9be --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-user.js.es6 @@ -0,0 +1,20 @@ +export default Discourse.Route.extend({ + serialize(model) { + return { username: model.get('username').toLowerCase() }; + }, + + model(params) { + return Discourse.AdminUser.find(Em.get(params, 'username').toLowerCase()); + }, + + renderTemplate() { + this.render({into: 'admin'}); + }, + + afterModel(adminUser) { + return adminUser.loadDetails().then(function () { + adminUser.setOriginalTrustLevel(); + return adminUser; + }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js b/app/assets/javascripts/admin/routes/admin_user_route.js deleted file mode 100644 index 798b2d13d75..00000000000 --- a/app/assets/javascripts/admin/routes/admin_user_route.js +++ /dev/null @@ -1,51 +0,0 @@ -Discourse.AdminUserRoute = Discourse.Route.extend({ - serialize: function(model) { - return { username: model.get('username').toLowerCase() }; - }, - - model: function(params) { - return Discourse.AdminUser.find(Em.get(params, 'username').toLowerCase()); - }, - - renderTemplate: function() { - this.render({into: 'admin'}); - }, - - afterModel: function(adminUser) { - return adminUser.loadDetails().then(function () { - adminUser.setOriginalTrustLevel(); - return adminUser; - }); - } -}); - -Discourse.AdminUserIndexRoute = Discourse.Route.extend({ - model: function() { - return this.modelFor('adminUser'); - }, - - afterModel: function(model) { - if(Discourse.User.currentProp('admin')) { - var self = this; - return Discourse.Group.findAll().then(function(groups){ - self._availableGroups = groups.filterBy('automatic', false); - return model; - }); - } - }, - - setupController: function(controller, model) { - controller.setProperties({ - originalPrimaryGroupId: model.get('primary_group_id'), - availableGroups: this._availableGroups, - model: model - }); - }, - - actions: { - showSuspendModal: function(user) { - Discourse.Route.showModal(this, 'admin_suspend_user', user); - this.controllerFor('modal').set('modalClass', 'suspend-user-modal'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/bulk-select-button.js.es6 b/app/assets/javascripts/discourse/components/bulk-select-button.js.es6 new file mode 100644 index 00000000000..ec47bbd56ff --- /dev/null +++ b/app/assets/javascripts/discourse/components/bulk-select-button.js.es6 @@ -0,0 +1,10 @@ +import showModal from 'discourse/lib/show-modal'; + +export default Ember.Component.extend({ + actions: { + showBulkActions() { + const controller = showModal('topicBulkActions', this.get('selected')); + controller.set('refreshTarget', this.get('refreshTarget')); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index dd7d2568ca2..27ae37afd96 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -1,11 +1,9 @@ import DiscoveryController from 'discourse/controllers/discovery'; import { queryParams } from 'discourse/controllers/discovery-sortable'; -import NotificationLevels from 'discourse/lib/notification-levels'; +import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection'; var controllerOpts = { needs: ['discovery'], - bulkSelectEnabled: false, - selected: [], period: null, canStar: Em.computed.alias('controllers.discovery/topics.currentUser.id'), @@ -53,7 +51,8 @@ var controllerOpts = { Discourse.TopicList.find(filter).then(function(list) { Discourse.TopicList.hideUniformCategory(list, self.get('category')); - self.setProperties({ model: list, selected: [] }); + self.setProperties({ model: list }); + self.resetSelected(); var tracking = Discourse.TopicTrackingState.current(); if (tracking) { @@ -64,10 +63,6 @@ var controllerOpts = { }); }, - toggleBulkSelect: function() { - this.toggleProperty('bulkSelectEnabled'); - this.get('selected').clear(); - }, resetNew: function() { var self = this; @@ -76,40 +71,9 @@ var controllerOpts = { Discourse.Topic.resetNew().then(function() { self.send('refresh'); }); - }, - - dismissRead: function(operationType) { - var self = this, - selected = this.get('selected'), - operation; - - if(operationType === "posts"){ - operation = { type: 'dismiss_posts' }; - } else { - operation = { type: 'change_notification_level', - notification_level_id: NotificationLevels.REGULAR }; - } - - var promise; - if (selected.length > 0) { - promise = Discourse.Topic.bulkOperation(selected, operation); - } else { - promise = Discourse.Topic.bulkOperationByFilter('unread', operation, this.get('category.id')); - } - promise.then(function(result) { - if (result && result.topic_ids) { - var tracker = Discourse.TopicTrackingState.current(); - result.topic_ids.forEach(function(t) { - tracker.removeTopic(t); - }); - tracker.incrementMessageCount(); - } - self.send('refresh'); - }); } }, - topicTrackingState: function() { return Discourse.TopicTrackingState.current(); }.property(), @@ -132,7 +96,6 @@ var controllerOpts = { this.get('topics.length') >= 30; }.property('filter', 'topics.length'), - canBulkSelect: Em.computed.alias('currentUser.staff'), hasTopics: Em.computed.gt('topics.length', 0), allLoaded: Em.computed.empty('more_topics_url'), latest: Discourse.computed.endWith('filter', 'latest'), @@ -187,4 +150,4 @@ Ember.keys(queryParams).forEach(function(p) { } }); -export default DiscoveryController.extend(controllerOpts); +export default DiscoveryController.extend(controllerOpts, BulkTopicSelection); diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index 5928e97ea02..09f6d065553 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -174,12 +174,12 @@ export default ObjectController.extend(ModalFunctionality, { self.flash(I18n.t('generic_error')); } - self.send('showModal'); + self.send('reopenModal'); self.displayErrors([I18n.t("category.delete_error")]); self.set('deleting', false); }); } else { - self.send('showModal'); + self.send('reopenModal'); self.set('deleting', false); } }); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index 9955c2197fc..559bb80e069 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -43,10 +43,10 @@ export default ObjectController.extend(ModalFunctionality, { self.set('details.auto_close_at', result.auto_close_at); self.set('details.auto_close_hours', result.auto_close_hours); } else { - bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } ); + bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); } }, function () { - bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('showModal'); } ); + bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); }); } diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index bbed56d36b8..ccb68864ec2 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -1,11 +1,8 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseController from 'discourse/controllers/controller'; +import showModal from 'discourse/lib/show-modal'; // This is happening outside of the app via popup -function showModal(modal) { - const route = Discourse.__container__.lookup('route:application'); - Discourse.Route.showModal(route, modal); -} const AuthErrors = ['requires_invite', 'awaiting_approval', 'awaiting_confirmation', 'admin_not_allowed_from_ip_address', 'not_allowed_from_ip_address']; diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 index 3d5285017eb..aa986a183fb 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -16,7 +16,6 @@ addBulkButton('resetRead', 'reset_read'); // Modal for performing bulk actions on topics export default Ember.ArrayController.extend(ModalFunctionality, { - needs: ['discovery/topics'], buttonRows: null, onShow: function() { @@ -34,6 +33,7 @@ export default Ember.ArrayController.extend(ModalFunctionality, { if (row.length) { buttonRows.push(row); } this.set('buttonRows', buttonRows); + this.send('changeBulkTemplate', 'modal/bulk_actions_buttons'); }, perform: function(operation) { @@ -66,9 +66,10 @@ export default Ember.ArrayController.extend(ModalFunctionality, { }, performAndRefresh: function(operation) { - var self = this; + const self = this; return this.perform(operation).then(function() { - self.get('controllers.discovery/topics').send('refresh'); + const refreshTarget = self.get('refreshTarget'); + if (refreshTarget) { refreshTarget.send('refresh'); } self.send('closeModal'); }); }, @@ -107,7 +108,8 @@ export default Ember.ArrayController.extend(ModalFunctionality, { topics.forEach(function(t) { t.set('category', category); }); - self.get('controllers.discovery/topics').send('refresh'); + const refreshTarget = self.get('refreshTarget'); + if (refreshTarget) { refreshTarget.send('refresh'); } self.send('closeModal'); }); }, diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6 new file mode 100644 index 00000000000..f12435c4966 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6 @@ -0,0 +1,20 @@ +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'}); + const controller = route.controllerFor(name); + if (controller) { + if (model) { + controller.set('model', model); + } + if (controller.onShow) { + controller.onShow(); + } + controller.set('flashMessage', null); + } + return controller; +} diff --git a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 new file mode 100644 index 00000000000..a30cba1f939 --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 @@ -0,0 +1,49 @@ +import NotificationLevels from 'discourse/lib/notification-levels'; + +export default Ember.Mixin.create({ + bulkSelectEnabled: false, + selected: null, + + canBulkSelect: Em.computed.alias('currentUser.staff'), + + resetSelected: function() { + this.set('selected', []); + }.on('init'), + + actions: { + toggleBulkSelect() { + this.toggleProperty('bulkSelectEnabled'); + this.get('selected').clear(); + }, + + dismissRead(operationType) { + const self = this, + selected = this.get('selected'); + + let operation; + if(operationType === "posts"){ + operation = { type: 'dismiss_posts' }; + } else { + operation = { type: 'change_notification_level', + notification_level_id: NotificationLevels.REGULAR }; + } + + let promise; + if (selected.length > 0) { + promise = Discourse.Topic.bulkOperation(selected, operation); + } else { + promise = Discourse.Topic.bulkOperationByFilter('unread', operation, this.get('category.id')); + } + promise.then(function(result) { + if (result && result.topic_ids) { + const tracker = Discourse.TopicTrackingState.current(); + result.topic_ids.forEach(function(t) { + tracker.removeTopic(t); + }); + tracker.incrementMessageCount(); + } + self.send('refresh'); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 9371e7e053f..bfd914197e6 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -1,3 +1,5 @@ +import showModal from 'discourse/lib/show-modal'; + function unlessReadOnly(method) { return function() { if (this.site.get("isReadOnly")) { @@ -74,31 +76,28 @@ const ApplicationRoute = Discourse.Route.extend({ showCreateAccount: unlessReadOnly('handleShowCreateAccount'), showForgotPassword() { - Discourse.Route.showModal(this, 'forgotPassword'); + showModal('forgotPassword'); }, showNotActivated(props) { - Discourse.Route.showModal(this, 'notActivated'); + showModal('notActivated'); this.controllerFor('notActivated').setProperties(props); }, showUploadSelector(composerView) { - Discourse.Route.showModal(this, 'uploadSelector'); + showModal('uploadSelector'); this.controllerFor('upload-selector').setProperties({ composerView: composerView }); }, showKeyboardShortcutsHelp() { - Discourse.Route.showModal(this, 'keyboardShortcutsHelp'); + showModal('keyboardShortcutsHelp'); }, showSearchHelp() { - const self = this; - // TODO: @EvitTrout how do we get a loading indicator here? Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(html){ - Discourse.Route.showModal(self, 'searchHelp', html); + showModal('searchHelp', html); }); - }, // Close the current modal, and destroy its state. @@ -109,13 +108,13 @@ const ApplicationRoute = Discourse.Route.extend({ /** Hide the modal, but keep it with all its state so that it can be shown again later. This is useful if you want to prompt for confirmation. hideModal, ask "Are you sure?", - user clicks "No", showModal. If user clicks "Yes", be sure to call closeModal. + user clicks "No", reopenModal. If user clicks "Yes", be sure to call closeModal. **/ hideModal() { $('#discourse-modal').modal('hide'); }, - showModal() { + reopenModal() { $('#discourse-modal').modal('show'); }, @@ -123,7 +122,7 @@ const ApplicationRoute = Discourse.Route.extend({ const self = this; Discourse.Category.reloadById(category.get('id')).then(function (c) { self.site.updateCategory(c); - Discourse.Route.showModal(self, 'editCategory', c); + showModal(self, 'editCategory', c); self.controllerFor('editCategory').set('selectedTab', 'general'); }); }, @@ -135,6 +134,13 @@ const ApplicationRoute = Discourse.Route.extend({ checkEmail: function (user) { user.checkEmail(); + }, + + changeBulkTemplate(w) { + const controllerName = w.replace('modal/', ''), + factory = this.container.lookupFactory('controller:' + controllerName); + + this.render(w, {into: 'topicBulkActions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'}); } }, @@ -164,7 +170,7 @@ const ApplicationRoute = Discourse.Route.extend({ if (!this.siteSettings.enable_local_logins && methods.length === 1) { this.controllerFor('login').send('externalLogin', methods[0]); } else { - Discourse.Route.showModal(this, modal); + showModal(modal); if (notAuto) { notAuto(); } } }, diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse.js.es6 similarity index 88% rename from app/assets/javascripts/discourse/routes/discourse_route.js rename to app/assets/javascripts/discourse/routes/discourse.js.es6 index 8694eacc89f..dcd4d686918 100644 --- a/app/assets/javascripts/discourse/routes/discourse_route.js +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -1,24 +1,45 @@ -/** - The base route for all routes on Discourse. Includes global enter functionality. +import showModal from 'discourse/lib/show-modal'; - @class Route - @extends Em.Route - @namespace Discourse - @module Discourse -**/ -Discourse.Route = Ember.Route.extend({ +const DiscourseRoute = Ember.Route.extend({ /** NOT called every time we enter a route on Discourse. Only called the FIRST time we enter a route. So, when going from one topic to another, activate will only be called on the TopicRoute for the first topic. - - @method activate **/ activate: function() { this._super(); - Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM'); + Em.run.scheduleOnce('afterRender', Ember.Route, this._cleanDOM); + }, + + _cleanDOM() { + // Close mini profiler + $('.profiler-results .profiler-result').remove(); + + // Close some elements that may be open + $('.d-dropdown').hide(); + $('header ul.icons li').removeClass('active'); + $('[data-toggle="dropdown"]').parent().removeClass('open'); + // close the lightbox + if ($.magnificPopup && $.magnificPopup.instance) { + $.magnificPopup.instance.close(); + $('body').removeClass('mfp-zoom-out-cur'); + } + + // Remove any link focus + // NOTE: the '.not("body")' is here to prevent a bug in IE10 on Win7 + // cf. https://stackoverflow.com/questions/5657371/ie9-window-loses-focus-due-to-jquery-mobile + $(document.activeElement).not("body").blur(); + + Discourse.set('notifyCount',0); + $('#discourse-modal').modal('hide'); + var hideDropDownFunction = $('html').data('hide-dropdown'); + if (hideDropDownFunction) { hideDropDownFunction(); } + + // TODO: Avoid container lookup here + var appEvents = Discourse.__container__.lookup('app-events:main'); + appEvents.trigger('dom:clean'); }, _refreshTitleOnce: function() { @@ -79,7 +100,7 @@ Discourse.Route = Ember.Route.extend({ var routeBuilder; -Discourse.Route.reopenClass({ +DiscourseRoute.reopenClass({ buildRoutes: function(builder) { var oldBuilder = routeBuilder; @@ -174,48 +195,11 @@ Discourse.Route.reopenClass({ }); }, - cleanDOM: function() { - // Close mini profiler - $('.profiler-results .profiler-result').remove(); - - // Close some elements that may be open - $('.d-dropdown').hide(); - $('header ul.icons li').removeClass('active'); - $('[data-toggle="dropdown"]').parent().removeClass('open'); - // close the lightbox - if ($.magnificPopup && $.magnificPopup.instance) { - $.magnificPopup.instance.close(); - $('body').removeClass('mfp-zoom-out-cur'); - } - - // Remove any link focus - // NOTE: the '.not("body")' is here to prevent a bug in IE10 on Win7 - // cf. https://stackoverflow.com/questions/5657371/ie9-window-loses-focus-due-to-jquery-mobile - $(document.activeElement).not("body").blur(); - - Discourse.set('notifyCount',0); - $('#discourse-modal').modal('hide'); - var hideDropDownFunction = $('html').data('hide-dropdown'); - if (hideDropDownFunction) { hideDropDownFunction(); } - - // TODO: Avoid container lookup here - var appEvents = Discourse.__container__.lookup('app-events:main'); - appEvents.trigger('dom:clean'); - }, - showModal: function(route, name, model) { - route.controllerFor('modal').set('modalClass', null); - route.render(name, {into: 'modal', outlet: 'modalBody'}); - var controller = route.controllerFor(name); - if (controller) { - if (model) { - controller.set('model', model); - } - if(controller && controller.onShow) { - controller.onShow(); - } - controller.set('flashMessage', null); - } + Ember.warn('DEPRECATED `Discourse.Route.showModal` - use `showModal` instead'); + showModal(name, model); } }); + +export default DiscourseRoute; diff --git a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 index c30f26a6b32..30d36bb3328 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 @@ -1,4 +1,5 @@ -import ShowFooter from "discourse/mixins/show-footer"; +import ShowFooter from 'discourse/mixins/show-footer'; +import showModal from 'discourse/lib/show-modal'; Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenComposer, ShowFooter, { renderTemplate() { @@ -45,7 +46,7 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenCompos const groups = this.site.groups, everyoneName = groups.findBy('id', 0).name; - Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({ + showModal('editCategory', Discourse.Category.create({ color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyoneName, permission_type: 1}], available_groups: groups.map(g => g.name), allow_badges: true diff --git a/app/assets/javascripts/discourse/routes/discovery-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6 similarity index 62% rename from app/assets/javascripts/discourse/routes/discovery-route.js.es6 rename to app/assets/javascripts/discourse/routes/discovery.js.es6 index 3d6522fe83a..c11e5ede4cc 100644 --- a/app/assets/javascripts/discourse/routes/discovery-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery.js.es6 @@ -5,7 +5,7 @@ import ShowFooter from "discourse/mixins/show-footer"; -Discourse.DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, Discourse.OpenComposer, ShowFooter, { +const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, Discourse.OpenComposer, ShowFooter, { redirect: function() { return this.redirectIfLoginRequired(); }, beforeModel: function(transition) { @@ -42,22 +42,9 @@ Discourse.DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, Discourse createTopic: function() { this.openComposer(this.controllerFor('discovery/topics')); - }, - - changeBulkTemplate: function(w) { - var controllerName = w.replace('modal/', ''), - factory = this.container.lookupFactory('controller:' + controllerName); - - this.render(w, {into: 'topicBulkActions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'}); - }, - - showBulkActions: function() { - var selected = this.controllerFor('discovery/topics').get('selected'); - Discourse.Route.showModal(this, 'topicBulkActions', selected); - this.send('changeBulkTemplate', 'modal/bulk_actions_buttons'); } } }); -export default Discourse.DiscoveryRoute; +export default DiscoveryRoute; diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 3f28b1b409f..79712f495eb 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -1,23 +1,24 @@ import ShowFooter from "discourse/mixins/show-footer"; import RestrictedUserRoute from "discourse/routes/restricted-user"; +import showModal from 'discourse/lib/show-modal'; export default RestrictedUserRoute.extend(ShowFooter, { - model: function() { + model() { return this.modelFor('user'); }, - setupController: function(controller, user) { + setupController(controller, user) { controller.setProperties({ model: user, newNameInput: user.get('name') }); }, actions: { - showAvatarSelector: function() { - Discourse.Route.showModal(this, 'avatar-selector'); + showAvatarSelector() { + showModal('avatar-selector'); // all the properties needed for displaying the avatar selector modal - var controller = this.controllerFor('avatar-selector'); - var user = this.modelFor('user'); - var props = user.getProperties( + const controller = this.controllerFor('avatar-selector'); + const user = this.modelFor('user'); + const props = user.getProperties( 'username', 'email', 'uploaded_avatar_id', 'system_avatar_upload_id', @@ -40,8 +41,8 @@ export default RestrictedUserRoute.extend(ShowFooter, { }, saveAvatarSelection: function() { - var user = this.modelFor('user'); - var avatarSelector = this.controllerFor('avatar-selector'); + const user = this.modelFor('user'); + const avatarSelector = this.controllerFor('avatar-selector'); // sends the information to the server if it has changed if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) { diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 9fa8fdd5339..1b147428617 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -1,12 +1,14 @@ -var isTransitioning = false, +let isTransitioning = false, scheduledReplace = null, - lastScrollPos = null, - SCROLL_DELAY = 500; + lastScrollPos = null; + +const SCROLL_DELAY = 500; import ShowFooter from "discourse/mixins/show-footer"; import Topic from 'discourse/models/topic'; +import showModal from 'discourse/lib/show-modal'; -var TopicRoute = Discourse.Route.extend(ShowFooter, { +const TopicRoute = Discourse.Route.extend(ShowFooter, { redirect() { return this.redirectIfLoginRequired(); }, queryParams: { @@ -16,16 +18,16 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { }, titleToken() { - var model = this.modelFor('topic'); + const model = this.modelFor('topic'); if (model) { - var result = model.get('title'), - cat = model.get('category'); + const result = model.get('title'), + cat = model.get('category'); // Only display uncategorized in the title tag if it was renamed if (cat && !(cat.get('isUncategorizedCategory') && cat.get('name').toLowerCase() === "uncategorized")) { - var catName = cat.get('name'), - parentCategory = cat.get('parentCategory'); + let catName = cat.get('name'); + const parentCategory = cat.get('parentCategory'); if (parentCategory) { catName = parentCategory.get('name') + " / " + catName; } @@ -43,27 +45,27 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { }, showFlags(post) { - Discourse.Route.showModal(this, 'flag', post); + showModal('flag', post); this.controllerFor('flag').setProperties({ selected: null }); }, showFlagTopic(topic) { - Discourse.Route.showModal(this, 'flag', topic); + showModal('flag', topic); this.controllerFor('flag').setProperties({ selected: null, flagTopic: true }); }, showAutoClose() { - Discourse.Route.showModal(this, 'editTopicAutoClose', this.modelFor('topic')); + showModal('editTopicAutoClose', this.modelFor('topic')); this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal'); }, showInvite() { - Discourse.Route.showModal(this, 'invite', this.modelFor('topic')); + showModal('invite', this.modelFor('topic')); this.controllerFor('invite').reset(); }, showPrivateInvite() { - Discourse.Route.showModal(this, 'invitePrivate', this.modelFor('topic')); + showModal('invitePrivate', this.modelFor('topic')); this.controllerFor('invitePrivate').setProperties({ email: null, error: false, @@ -73,26 +75,26 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { }, showHistory(post) { - Discourse.Route.showModal(this, 'history', post); + showModal('history', post); this.controllerFor('history').refresh(post.get("id"), "latest"); this.controllerFor('modal').set('modalClass', 'history-modal'); }, showRawEmail(post) { - Discourse.Route.showModal(this, 'raw-email', post); + showModal('raw-email', post); this.controllerFor('raw_email').loadRawEmail(post.get("id")); }, mergeTopic() { - Discourse.Route.showModal(this, 'mergeTopic', this.modelFor('topic')); + showModal('mergeTopic', this.modelFor('topic')); }, splitTopic() { - Discourse.Route.showModal(this, 'split-topic', this.modelFor('topic')); + showModal('split-topic', this.modelFor('topic')); }, changeOwner() { - Discourse.Route.showModal(this, 'changeOwner', this.modelFor('topic')); + showModal('changeOwner', this.modelFor('topic')); }, // Use replaceState to update the URL once it changes @@ -100,9 +102,9 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { // do nothing if we are transitioning to another route if (isTransitioning || Discourse.TopicRoute.disableReplaceState) { return; } - var topic = this.modelFor('topic'); + const topic = this.modelFor('topic'); if (topic && currentPost) { - var postUrl = topic.get('url'); + let postUrl = topic.get('url'); if (currentPost > 1) { postUrl += "/" + currentPost; } Em.run.cancel(scheduledReplace); @@ -128,7 +130,7 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { // replaceState can be very slow on Android Chrome. This function debounces replaceState // within a topic until scrolling stops _replaceUnlessScrolling(url) { - var currentPos = parseInt($(document).scrollTop(), 10); + const currentPos = parseInt($(document).scrollTop(), 10); if (currentPos === lastScrollPos) { Discourse.URL.replaceState(url); return; @@ -138,11 +140,11 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { }, setupParams(topic, params) { - var postStream = topic.get('postStream'); + const postStream = topic.get('postStream'); postStream.set('summary', Em.get(params, 'filter') === 'summary'); postStream.set('show_deleted', !!Em.get(params, 'show_deleted')); - var usernames = Em.get(params, 'username_filters'), + const usernames = Em.get(params, 'username_filters'), userFilters = postStream.get('userFilters'); userFilters.clear(); @@ -154,9 +156,9 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { }, model(params, transition) { - var queryParams = transition.queryParams; + const queryParams = transition.queryParams; - var topic = this.modelFor('topic'); + const topic = this.modelFor('topic'); if (topic && (topic.get('id') === parseInt(params.id, 10))) { this.setupParams(topic, queryParams); // If we have the existing model, refresh it @@ -172,7 +174,7 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { this._super(); isTransitioning = false; - var topic = this.modelFor('topic'); + const topic = this.modelFor('topic'); this.session.set('lastTopicIdViewed', parseInt(topic.get('id'), 10)); this.controllerFor('search').set('searchContext', topic.get('searchContext')); }, @@ -184,7 +186,7 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { this.controllerFor('search').set('searchContext', null); this.controllerFor('user-card').set('visible', false); - var topicController = this.controllerFor('topic'), + const topicController = this.controllerFor('topic'), postStream = topicController.get('postStream'); postStream.cancelFilter(); @@ -193,8 +195,8 @@ var TopicRoute = Discourse.Route.extend(ShowFooter, { this.controllerFor('composer').set('topic', null); Discourse.ScreenTrack.current().stop(); - var headerController; - if (headerController = this.controllerFor('header')) { + const headerController = this.controllerFor('header'); + if (headerController) { headerController.set('topic', null); headerController.set('showExtraInfo', false); } diff --git a/app/assets/javascripts/discourse/routes/user-invited.js.es6 b/app/assets/javascripts/discourse/routes/user-invited.js.es6 index 5a03f2797ac..b0e03559e6c 100644 --- a/app/assets/javascripts/discourse/routes/user-invited.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited.js.es6 @@ -1,15 +1,16 @@ -import ShowFooter from "discourse/mixins/show-footer"; +import ShowFooter from 'discourse/mixins/show-footer'; +import showModal from 'discourse/lib/show-modal'; export default Discourse.Route.extend(ShowFooter, { - renderTemplate: function() { + renderTemplate() { this.render({ into: 'user' }); }, - model: function() { + model() { return Discourse.Invite.findInvitedBy(this.modelFor('user')); }, - setupController: function(controller, model) { + setupController(controller, model) { controller.setProperties({ model: model, user: this.controllerFor('user').get('model'), @@ -19,16 +20,16 @@ export default Discourse.Route.extend(ShowFooter, { }, actions: { - showInvite: function() { - Discourse.Route.showModal(this, 'invite', Discourse.User.current()); + showInvite() { + showModal('invite', Discourse.User.current()); this.controllerFor('invite').reset(); }, - uploadSuccess: function(filename) { + uploadSuccess(filename) { bootbox.alert(I18n.t("user.invited.bulk_invite.success", { filename: filename })); }, - uploadError: function(filename, message) { + uploadError(filename, message) { bootbox.alert(I18n.t("user.invited.bulk_invite.error", { filename: filename, message: message })); } } diff --git a/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs b/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs new file mode 100644 index 00000000000..30d9bc49fe8 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs @@ -0,0 +1,5 @@ +{{#if selected}} +
+ {{d-button action="showBulkActions" icon="wrench" class="no-text"}} +
+{{/if}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 3916464e275..41c9ecd37f2 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -14,11 +14,7 @@ {{/if}} -{{#if selected}} -
- {{d-button action="showBulkActions" icon="wrench" class="no-text"}} -
-{{/if}} +{{bulk-select-button selected=selected refreshTarget=controller}}
{{#if top}} diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 156eae70bbf..e2e63bd42e6 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -49,7 +49,8 @@ //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button //= require ./discourse/views/composer -//= require ./discourse/routes/discourse_route +//= require ./discourse/lib/show-modal +//= require ./discourse/routes/discourse //= require ./discourse/routes/build-topic-route //= require ./discourse/routes/restricted-user //= require ./discourse/routes/user-topic-list diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index c11daa489a8..b5cc2a0a7f6 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -106,6 +106,7 @@ module Tilt # HAX result = "Controller" if result == "ControllerController" + result = "Route" if result == "DiscourseRoute" result.gsub!(/Mixin$/, '') result.gsub!(/Model$/, '')